 
from ui_element import UIElement, UIArt, UIRenderable
from ui_button import UIButton, TEXT_LEFT, TEXT_CENTER, TEXT_RIGHT
from ui_swatch import CharacterSetSwatch, PaletteSwatch, MIN_CHARSET_WIDTH
from ui_colors import UIColors
from ui_tool import FillTool, FILL_BOUND_CHAR, FILL_BOUND_FG_COLOR, FILL_BOUND_BG_COLOR
from renderable_line import LineRenderable, SwatchSelectionBoxRenderable
from art import UV_NORMAL, UV_ROTATE90, UV_ROTATE180, UV_ROTATE270, UV_FLIPX, UV_FLIPY
from ui_file_chooser_dialog import CharSetChooserDialog, PaletteChooserDialog

TOOL_PANE_WIDTH = 10

class ToolTabButton(UIButton):
    x, y = 0, 0
    caption_y = 1
    # width is set on the fly by popup size in reset_art
    height = 3
    caption_justify = TEXT_CENTER
    caption = 'Tools'

class CharColorTabButton(UIButton):
    caption_y = 1
    height = ToolTabButton.height
    caption_justify = TEXT_CENTER
    caption = 'Chars/Colors'

# charset view scale up/down buttons

class CharSetScaleUpButton(UIButton):
    width, height = 3, 1
    x, y = -width, ToolTabButton.height + 1
    caption = '+'
    caption_justify = TEXT_CENTER

class CharSetScaleDownButton(CharSetScaleUpButton):
    x = -CharSetScaleUpButton.width + CharSetScaleUpButton.x
    caption = '-'

# charset flip / rotate buttons

class CharXformButton(UIButton):
    hovered_fg_color = UIColors.white
    hovered_bg_color = UIColors.medgrey

class CharFlipNoButton(CharXformButton):
    x = 3 + len('Flip:') + 1
    y = CharSetScaleUpButton.y + 1
    caption = 'None'
    width = len(caption) + 2
    caption_justify = TEXT_CENTER

class CharFlipXButton(CharFlipNoButton):
    x = CharFlipNoButton.x + CharFlipNoButton.width + 1
    width = 3
    caption = 'X'

class CharFlipYButton(CharFlipXButton):
    x = CharFlipXButton.x + CharFlipXButton.width + 1
    caption = 'Y'

class CharRot0Button(CharXformButton):
    x = 3 + len('Rotation:') + 1
    y = CharFlipNoButton.y + 1
    width = 3
    caption = '0'
    caption_justify = TEXT_CENTER

class CharRot90Button(CharRot0Button):
    x = CharRot0Button.x + CharRot0Button.width + 1
    width = 4
    caption = '90'

class CharRot180Button(CharRot0Button):
    x = CharRot90Button.x + CharRot90Button.width + 1
    width = 5
    caption = '180'

class CharRot270Button(CharRot0Button):
    x = CharRot180Button.x + CharRot180Button.width + 1
    width = 5
    caption = '270'

# tool and tool settings buttons

class ToolButton(UIButton):
    "a tool entry in the tool tab's left hand pane. populated from UI.tools"
    width = TOOL_PANE_WIDTH
    caption = 'TOOLZ'
    y = ToolTabButton.height + 2

class BrushSizeUpButton(UIButton):
    width = 3
    y = ToolTabButton.height + 3
    caption = '+'
    caption_justify = TEXT_CENTER
    normal_fg_color = UIColors.white
    normal_bg_color = UIColors.medgrey

class BrushSizeDownButton(BrushSizeUpButton):
    caption = '-'

class AffectCharToggleButton(UIButton):
    width = 3
    x = TOOL_PANE_WIDTH + 2
    y = BrushSizeUpButton.y + 3
    # don't paint caption from string
    should_draw_caption = False
    normal_fg_color = UIColors.white
    normal_bg_color = UIColors.medgrey

class AffectFgToggleButton(AffectCharToggleButton):
    y = AffectCharToggleButton.y + 1

class AffectBgToggleButton(AffectCharToggleButton):
    y = AffectCharToggleButton.y + 2

class AffectXformToggleButton(AffectCharToggleButton):
    y = AffectCharToggleButton.y + 3

# fill boundary mode items
class FillBoundaryModeCharButton(AffectCharToggleButton):
    y = AffectXformToggleButton.y + 3

class FillBoundaryModeFGButton(AffectCharToggleButton):
    y = FillBoundaryModeCharButton.y + 1

class FillBoundaryModeBGButton(AffectCharToggleButton):
    y = FillBoundaryModeCharButton.y + 2


# charset / palette chooser buttons

class CharSetChooserButton(UIButton):
    caption = 'Set:'
    x = 1
    normal_fg_color = UIColors.black
    normal_bg_color = UIColors.white
    hovered_fg_color = UIColors.white
    hovered_bg_color = UIColors.medgrey

class PaletteChooserButton(CharSetChooserButton):
    caption = 'Palette:'


TAB_TOOLS = 0
TAB_CHAR_COLOR = 1


class ToolPopup(UIElement):
    
    visible = False
    # actual width will be based on character set + palette size and scale
    tile_width, tile_height = 20, 15
    tab_height = ToolTabButton.height
    swatch_margin = 0.05
    fg_color = UIColors.black
    bg_color = UIColors.lightgrey
    highlight_color = UIColors.white
    tool_settings_label = 'Tool Settings:'
    brush_size_label = 'Brush size:'
    affects_heading_label = 'Affects:'
    affects_char_label = 'Character'
    affects_fg_label = 'Foreground Color'
    affects_bg_label = 'Background Color'
    affects_xform_label = 'Rotation/Flip'
    fill_boundary_modes_label = 'Fill boundary mode:'
    fill_boundary_char_label = affects_char_label
    fill_boundary_fg_label = affects_fg_label
    fill_boundary_bg_label = affects_bg_label
    flip_label = 'Flip:'
    rotation_label = 'Rotation:'
    # index of check mark character in UI charset
    check_char_index = 131
    # index of off and on radio button characters in UI charset
    radio_char_0_index = 126
    radio_char_1_index = 127
    # map classes to member names / callbacks
    button_names = {
        ToolTabButton: 'tool_tab',
        CharColorTabButton: 'char_color_tab',
    }
    char_color_tab_button_names = {
        CharSetScaleUpButton: 'scale_charset_up',
        CharSetScaleDownButton: 'scale_charset_down',
        CharSetChooserButton: 'choose_charset',
        CharFlipNoButton: 'xform_normal',
        CharFlipXButton: 'xform_flipX',
        CharFlipYButton: 'xform_flipY',
        CharRot0Button: 'xform_0',
        CharRot90Button: 'xform_90',
        CharRot180Button: 'xform_180',
        CharRot270Button: 'xform_270',
        PaletteChooserButton: 'choose_palette',
    }
    tool_tab_button_names = {
        BrushSizeUpButton: 'brush_size_up',
        BrushSizeDownButton: 'brush_size_down',
        AffectCharToggleButton: 'toggle_affect_char',
        AffectFgToggleButton: 'toggle_affect_fg',
        AffectBgToggleButton: 'toggle_affect_bg',
        AffectXformToggleButton: 'toggle_affect_xform',
    }
    fill_boundary_mode_button_names = {
        FillBoundaryModeCharButton: 'set_fill_boundary_char',
        FillBoundaryModeFGButton: 'set_fill_boundary_fg',
        FillBoundaryModeBGButton: 'set_fill_boundary_bg'
    }
    
    def __init__(self, ui):
        self.ui = ui
        self.charset_swatch = CharacterSetSwatch(ui, self)
        self.palette_swatch = PaletteSwatch(ui, self)
        self.cursor_box = SwatchSelectionBoxRenderable(ui.app, self.charset_swatch.art)
        self.renderables = [self.cursor_box]
        # set by swatch.set_cursor_loc based on selection validity
        self.cursor_char = -1
        self.cursor_color = -1
        self.active_tab = TAB_CHAR_COLOR
        # create buttons from button:name map, button & callback names generated
        # group these into lists that can be combined into self.buttons
        self.common_buttons = self.create_buttons_from_map(self.button_names)
        self.char_color_tab_buttons = self.create_buttons_from_map(self.char_color_tab_button_names)
        self.fill_boundary_mode_buttons = self.create_buttons_from_map(self.fill_boundary_mode_button_names)
        self.tool_tab_buttons = self.create_buttons_from_map(self.tool_tab_button_names) + self.fill_boundary_mode_buttons
        # populate more tool tab buttons from UI's list of tools
        # similar to create_buttons_from_map, but class name isn't known
        # MAYBE-TODO: is there a way to unify this?
        for tool in self.ui.tools:
            tool_button = ToolButton(self)
            # caption: 1-space padding from left
            tool_button.caption = ' %s' % tool.button_caption
            tool_button_name = '%s_tool_button' % tool.name
            setattr(self, tool_button_name, tool_button)
            cb_name = '%s_pressed' % tool_button_name
            tool_button.callback = getattr(self, cb_name)
            # set a special property UI can refer to
            tool_button.tool_name = tool.name
            self.tool_tab_buttons.append(tool_button)
        UIElement.__init__(self, ui)
        # set initial tab state
        self.char_color_tab_button_pressed()
        self.xform_0_button.normal_bg_color = self.xform_normal_button.normal_bg_color = self.highlight_color
    
    def create_buttons_from_map(self, button_dict):
        buttons = []
        for button_class in button_dict:
            button = button_class(self)
            button_name = '%s_button' % button_dict[button_class]
            setattr(self, button_name, button)
            cb_name = '%s_pressed' % button_name
            button.callback = getattr(self, cb_name)
            buttons.append(button)
        return buttons
    
    def tool_tab_button_pressed(self):
        self.active_tab = TAB_TOOLS
        self.char_color_tab_button.can_hover = True
        self.char_color_tab_button.dimmed = True
        self.tool_tab_button.can_hover = False
        self.tool_tab_button.dimmed = False
        self.buttons = self.common_buttons + self.tool_tab_buttons
        self.draw_tool_tab()
        self.draw_buttons()
    
    def char_color_tab_button_pressed(self):
        self.active_tab = TAB_CHAR_COLOR
        self.tool_tab_button.can_hover = True
        self.tool_tab_button.dimmed = True
        self.char_color_tab_button.can_hover = False
        self.char_color_tab_button.dimmed = False
        self.buttons = self.common_buttons + self.char_color_tab_buttons
        self.draw_char_color_tab()
        self.draw_buttons()
    
    def scale_charset_up_button_pressed(self):
        self.charset_swatch.increase_scale()
        self.reset_art()
        self.charset_swatch.reset_loc()
        self.palette_swatch.reset_loc()
    
    def scale_charset_down_button_pressed(self):
        self.charset_swatch.decrease_scale()
        self.reset_art()
        self.charset_swatch.reset_loc()
        self.palette_swatch.reset_loc()
    
    def brush_size_up_button_pressed(self):
        # any changes to tool's setting will force redraw of settings tab
        self.ui.selected_tool.increase_brush_size()
    
    def brush_size_down_button_pressed(self):
        self.ui.selected_tool.decrease_brush_size()
    
    def toggle_affect_char_button_pressed(self):
        self.ui.selected_tool.toggle_affects_char()
    
    def toggle_affect_fg_button_pressed(self):
        self.ui.selected_tool.toggle_affects_fg()
    
    def toggle_affect_bg_button_pressed(self):
        self.ui.selected_tool.toggle_affects_bg()
    
    def toggle_affect_xform_button_pressed(self):
        self.ui.selected_tool.toggle_affects_xform()
    
    def set_fill_boundary_char_button_pressed(self):
        self.ui.fill_tool.boundary_mode = FILL_BOUND_CHAR
        self.ui.tool_settings_changed = True
    
    def set_fill_boundary_fg_button_pressed(self):
        self.ui.fill_tool.boundary_mode = FILL_BOUND_FG_COLOR
        self.ui.tool_settings_changed = True
    
    def set_fill_boundary_bg_button_pressed(self):
        self.ui.fill_tool.boundary_mode = FILL_BOUND_BG_COLOR
        self.ui.tool_settings_changed = True
    
    def pencil_tool_button_pressed(self):
        self.ui.set_selected_tool(self.ui.pencil_tool)
    
    def erase_tool_button_pressed(self):
        self.ui.set_selected_tool(self.ui.erase_tool)
    
    def grab_tool_button_pressed(self):
        self.ui.set_selected_tool(self.ui.grab_tool)
    
    def rotate_tool_button_pressed(self):
        self.ui.set_selected_tool(self.ui.rotate_tool)
    
    def text_tool_button_pressed(self):
        self.ui.set_selected_tool(self.ui.text_tool)
    
    def select_tool_button_pressed(self):
        self.ui.set_selected_tool(self.ui.select_tool)
    
    def paste_tool_button_pressed(self):
        self.ui.set_selected_tool(self.ui.paste_tool)
    
    def fill_tool_button_pressed(self):
        self.ui.set_selected_tool(self.ui.fill_tool)
    
    def set_xform(self, new_xform):
        "tells UI elements to respect new xform"
        self.charset_swatch.set_xform(new_xform)
        self.update_xform_buttons()
    
    def update_xform_buttons(self):
        # light up button for current selected option
        button_map = {
            UV_NORMAL: self.xform_normal_button,
            UV_ROTATE90: self.xform_90_button,
            UV_ROTATE180: self.xform_180_button,
            UV_ROTATE270: self.xform_270_button,
            UV_FLIPX: self.xform_flipX_button,
            UV_FLIPY: self.xform_flipY_button
        }
        for b in button_map:
            if b == self.ui.selected_xform:
                button_map[b].normal_bg_color = self.highlight_color
            else:
                button_map[b].normal_bg_color = self.bg_color
        self.xform_0_button.normal_bg_color = self.xform_normal_button.normal_bg_color
        self.draw_buttons()
    
    def xform_normal_button_pressed(self):
        self.ui.set_selected_xform(UV_NORMAL)
    
    def xform_flipX_button_pressed(self):
        self.ui.set_selected_xform(UV_FLIPX)
    
    def xform_flipY_button_pressed(self):
        self.ui.set_selected_xform(UV_FLIPY)
    
    def xform_0_button_pressed(self):
        self.ui.set_selected_xform(UV_NORMAL)
    
    def xform_90_button_pressed(self):
        self.ui.set_selected_xform(UV_ROTATE90)
    
    def xform_180_button_pressed(self):
        self.ui.set_selected_xform(UV_ROTATE180)
    
    def xform_270_button_pressed(self):
        self.ui.set_selected_xform(UV_ROTATE270)
    
    def choose_charset_button_pressed(self):
        self.hide()
        self.ui.open_dialog(CharSetChooserDialog)
    
    def choose_palette_button_pressed(self):
        self.hide()
        self.ui.open_dialog(PaletteChooserDialog)
    
    def draw_char_color_tab(self):
        "draw non-button bits of this tab"
        # charset renderable location will be set in update()
        charset = self.ui.active_art.charset
        palette = self.ui.active_art.palette
        cqw, cqh = self.charset_swatch.art.quad_width, self.charset_swatch.art.quad_height
        self.art.clear_frame_layer(0, 0, self.bg_color, self.fg_color)
        # position & caption charset button
        y = self.tab_height + 1
        self.choose_charset_button.y = y
        self.choose_charset_button.caption = ' %s %s ' % (CharSetChooserButton.caption, charset.name)
        self.choose_charset_button.width = len(self.choose_charset_button.caption)
        # charset scale
        charset_scale = '%.2fx' % self.charset_swatch.char_scale
        x = -self.scale_charset_up_button.width * 2
        self.art.write_string(0, 0, x, y, charset_scale, None, None, True)
        # transform labels and buttons, eg
        # Transform: [Normal] [Flip X] [Flip Y]
        # Rotation: [ 0 ] [ 90] [180] [270]
        x = 3
        y += 1
        self.art.write_string(0, 0, x, y, self.flip_label)
        y += 1
        self.art.write_string(0, 0, x, y, self.rotation_label)
        # position & caption palette button
        pal_caption_y = (cqh * charset.map_height) / self.art.quad_height
        pal_caption_y += self.tab_height + 5
        self.choose_palette_button.y = int(pal_caption_y)
        self.choose_palette_button.caption = ' %s %s ' % (PaletteChooserButton.caption, palette.name)
        self.choose_palette_button.width = len(self.choose_palette_button.caption)
        # set button states so captions draw properly
        tab_width = int(self.tile_width / 2)
        self.tool_tab_button.width = tab_width
        self.char_color_tab_button.width = int(self.tile_width) - tab_width
        self.char_color_tab_button.x = tab_width
    
    def draw_tool_tab(self):
        self.art.clear_frame_layer(0, 0, self.bg_color, self.fg_color)
        # fill tool bar with dimmer color, highlight selected tool
        for y in range(self.art.height):
            for x in range(TOOL_PANE_WIDTH):
                self.art.set_color_at(0, 0, x, y, self.ui.colors.medgrey, False)
        # set selected tool BG lighter
        y = self.tab_height + 1
        for i,tool in enumerate(self.ui.tools):
            tool_button = None
            for button in self.tool_tab_buttons:
                try:
                    if button.tool_name == tool.name:
                        tool_button = button
                except:
                    pass
            tool_button.y = y+i
            if tool == self.ui.selected_tool:
                tool_button.normal_bg_color = self.ui.colors.lightgrey
            else:
                tool_button.normal_bg_color = self.ui.colors.medgrey
        # draw current tool settings
        x = TOOL_PANE_WIDTH + 1
        y = self.tab_height + 1
        label = '%s %s' % (self.ui.selected_tool.button_caption, self.tool_settings_label)
        self.art.write_string(0, 0, x, y, label)
        x += 1
        y += 2
        # brush size (if applicable)
        if self.ui.selected_tool.brush_size:
            self.brush_size_down_button.visible = True
            self.brush_size_up_button.visible = True
            label = self.brush_size_label
            # calculate X of + and - buttons based on size string
            self.brush_size_down_button.x = TOOL_PANE_WIDTH + len(label) + 2
            label += ' ' * (self.brush_size_down_button.width + 1)
            label += '%s' % self.ui.selected_tool.brush_size
            self.brush_size_up_button.x = TOOL_PANE_WIDTH + len(label) + 3
            self.art.write_string(0, 0, x, y, label)
        else:
            # if inapplicable, hide those controls
            self.brush_size_down_button.visible = False
            self.brush_size_up_button.visible = False
        if self.ui.selected_tool.affects_masks:
            # affects char/fg/bg settings
            self.toggle_affect_char_button.visible = True
            self.toggle_affect_fg_button.visible = True
            self.toggle_affect_bg_button.visible = True
            self.toggle_affect_xform_button.visible = True
            y += 2
            self.art.write_string(0, 0, x, y, self.affects_heading_label)
            y += 1
            # set affects-* button labels AND captions
            def get_affects_char(affects):
                return [0, self.check_char_index][affects]
            w = self.toggle_affect_char_button.width
            label_toggle_pairs = []
            label_toggle_pairs += [(self.affects_char_label, self.ui.selected_tool.affects_char)]
            label_toggle_pairs += [(self.affects_fg_label, self.ui.selected_tool.affects_fg_color)]
            label_toggle_pairs += [(self.affects_bg_label, self.ui.selected_tool.affects_bg_color)]
            label_toggle_pairs += [(self.affects_xform_label, self.ui.selected_tool.affects_xform)]
            for label,toggle in label_toggle_pairs:
                self.art.write_string(0, 0, x+w+1, y, '%s' % label)
                #self.art.set_tile_at(0, 0, x, y, get_affects_char(toggle), 4, 2)
                self.art.set_char_index_at(0, 0, x+1, y, get_affects_char(toggle))
                y += 1
        else:
            self.toggle_affect_char_button.visible = False
            self.toggle_affect_fg_button.visible = False
            self.toggle_affect_bg_button.visible = False
            self.toggle_affect_xform_button.visible = False
        # custom setting for fill tool: boundary mode
        if type(self.ui.selected_tool) is FillTool:
            y += 1
            self.art.write_string(0, 0, x, y, self.fill_boundary_modes_label)
            y += 1
            # boundary mode buttons + labels
            #x += 
            labels = [self.fill_boundary_char_label,
                      self.fill_boundary_fg_label,
                      self.fill_boundary_bg_label]
            for i,button in enumerate(self.fill_boundary_mode_buttons):
                button.visible = True
                char = [self.radio_char_0_index, self.radio_char_1_index][i == self.ui.fill_tool.boundary_mode]
                #self.ui.app.log(char)
                self.art.set_char_index_at(0, 0, x+1, y, char)
                self.art.write_string(0, 0, x + FillBoundaryModeCharButton.width + 1, y, labels[i])
                y += 1
        else:
            for button in self.fill_boundary_mode_buttons:
                button.visible = False
    
    def reset_art(self):
        if not self.ui.active_art:
            return
        self.charset_swatch.reset_art()
        self.palette_swatch.reset_art()
        # set panel size based on charset size
        margin = self.swatch_margin * 2
        charset = self.ui.active_art.charset
        cqw, cqh = self.charset_swatch.art.quad_width, self.charset_swatch.art.quad_height
        old_width, old_height = self.tile_width, self.tile_height
        # min width in case of tiny charsets
        charset_tile_width = max(charset.map_width, MIN_CHARSET_WIDTH)
        self.tile_width = (cqw * charset_tile_width + margin) / UIArt.quad_width
        # tile height = height of charset + distance from top of popup
        self.tile_height = (cqh * charset.map_height) / UIArt.quad_height + margin
        # account for popup info lines etc: charset name + palette name + 1 padding each
        extra_lines = 7
        # account for size of palette + bottom margin
        palette_height = ((self.palette_swatch.art.height * self.palette_swatch.art.quad_height) + self.swatch_margin) / UIArt.quad_height
        self.tile_height += self.tab_height + palette_height + extra_lines
        if old_width != self.tile_width or old_height != self.tile_height:
            self.art.resize(int(self.tile_width), int(self.tile_height))
        # panel text - position different elements based on selected tab
        if self.active_tab == TAB_CHAR_COLOR:
            self.draw_char_color_tab()
        elif self.active_tab == TAB_TOOLS:
            self.draw_tool_tab()
        self.update_xform_buttons()
        # draw button captions
        UIElement.reset_art(self)
    
    def show(self):
        # if already visible, bail - key repeat probably triggered this
        if self.visible:
            return
        if self.ui.active_dialog:
            return
        self.visible = True
        # visible, grab keyboard focus
        self.ui.keyboard_focus_element = self
        # set cursor as starting point for keyboard navigation
        self.charset_swatch.set_cursor_selection_index(self.ui.selected_char)
        if self.ui.pulldown.visible:
            self.ui.menu_bar.close_active_menu()
        self.reset_loc()
    
    def toggle(self):
        if self.visible:
            self.hide()
        else:
            self.show()
    
    def reset_loc(self):
        if not self.ui.active_art:
            return
        x, y = self.ui.get_screen_coords(self.ui.app.mouse_x, self.ui.app.mouse_y)
        # center on mouse
        w, h = self.tile_width * self.art.quad_width, self.tile_height * self.art.quad_height
        x -= w / 2
        y += h / 2
        # clamp to edges of screen
        self.x = max(-1, min(1 - w, x))
        self.y = min(1, max(-1 + h, y))
        # set location for sub elements
        self.renderable.x, self.renderable.y = self.x, self.y
        self.charset_swatch.reset_loc()
        self.palette_swatch.reset_loc()
    
    def hide(self):
        self.visible = False
        self.ui.keyboard_focus_element = None
        self.ui.refocus_keyboard()
    
    def set_active_charset(self, new_charset):
        self.charset_swatch.art.charset = new_charset
        self.palette_swatch.art.charset = new_charset
        # make sure selected char isn't out of bounds w/ new set
        self.ui.selected_char %= new_charset.last_index
        self.ui.status_bar.set_active_charset(new_charset)
        self.charset_swatch.reset()
        # charset width drives palette swatch width
        self.palette_swatch.reset()
        self.reset_art()
    
    def set_active_palette(self, new_palette):
        self.charset_swatch.art.palette = new_palette
        self.palette_swatch.art.palette = new_palette
        # make sure selected colors aren't out of bounds w/ new palette
        self.ui.selected_fg_color %= len(new_palette.colors) - 1
        self.ui.selected_bg_color %= len(new_palette.colors) - 1
        self.ui.status_bar.set_active_palette(new_palette)
        self.palette_swatch.reset()
        self.reset_art()
    
    def update(self):
        UIElement.update(self)
        if not self.ui.active_art:
            return
        if self.active_tab == TAB_CHAR_COLOR:
            # bail if mouse didn't move, but also respect keyboard editing
            mouse_moved = self.ui.app.mouse_dx != 0 or self.ui.app.mouse_dy != 0
            if self.ui.app.keyboard_editing:
                self.cursor_box.visible = True
            elif mouse_moved and self in self.ui.hovered_elements:
                self.cursor_box.visible = False
                x, y = self.ui.get_screen_coords(self.ui.app.mouse_x, self.ui.app.mouse_y)
                for e in [self.charset_swatch, self.palette_swatch]:
                    if e.is_inside(x, y):
                        self.cursor_box.visible = True
                        e.set_cursor_loc_from_mouse(self.cursor_box, x, y)
                        break
            # note: self.cursor_box updates in charset_swatch.update
            self.charset_swatch.update()
            self.palette_swatch.update()
        elif self.active_tab == TAB_TOOLS and self.ui.tool_settings_changed:
            self.draw_tool_tab()
            self.draw_buttons()
    
    def keyboard_navigate(self, dx, dy):
        active_swatch = self.charset_swatch if self.cursor_char != -1 else self.palette_swatch
        # TODO: can't handle cross-swatch navigation properly, restrict to chars
        active_swatch = self.charset_swatch
        # reverse up/down direction
        active_swatch.move_cursor(self.cursor_box, dx, -dy)
    
    def keyboard_select_item(self):
        # called as ui.keyboard_focus_element
        # simulate left/right click in popup to select stuff
        self.select_key_pressed(self.ui.app.il.shift_pressed)
    
    def select_key_pressed(self, mod_pressed):
        mouse_button = [1, 3][mod_pressed]
        self.clicked(mouse_button)
    
    def clicked(self, mouse_button):
        handled = UIElement.clicked(self, mouse_button)
        if handled:
            return
        # if cursor is over a char or color, make it the ui's selected one
        if self.cursor_char != -1:
            self.ui.selected_char = self.cursor_char
            # update cursor, eg keyboard select when cursor isn't beneath popup
            self.ui.app.cursor.undo_preview_edits()
            self.ui.app.cursor.update_cursor_preview()
        elif self.cursor_color != -1:
            if mouse_button == 1:
                self.ui.selected_fg_color = self.cursor_color
            elif mouse_button == 3:
                self.ui.selected_bg_color = self.cursor_color
        return True
    
    def render(self):
        if not self.visible:
            return
        UIElement.render(self)
        if self.active_tab == TAB_CHAR_COLOR:
            self.charset_swatch.render()
            self.palette_swatch.render()
            if self.cursor_char != -1 or self.cursor_color != -1:
                self.cursor_box.render()
