from art import UV_FLIPX, UV_FLIPY, UV_NORMAL, UV_ROTATE90, UV_ROTATE180, UV_ROTATE270 from renderable_line import SwatchSelectionBoxRenderable from ui_button import TEXT_CENTER, UIButton from ui_colors import UIColors from ui_element import UIArt, UIElement from ui_file_chooser_dialog import CharSetChooserDialog, PaletteChooserDialog from ui_swatch import MIN_CHARSET_WIDTH, CharacterSetSwatch, PaletteSwatch from ui_tool import FILL_BOUND_BG_COLOR, FILL_BOUND_CHAR, FILL_BOUND_FG_COLOR, FillTool 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 = " {}".format(tool.button_caption) tool_button_name = "{}_tool_button".format(tool.name) setattr(self, tool_button_name, tool_button) cb_name = "{}_pressed".format(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 = "{}_button".format(button_dict[button_class]) setattr(self, button_name, button) cb_name = "{}_pressed".format(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 = " {} {} ".format( CharSetChooserButton.caption, charset.name, ) self.choose_charset_button.width = len(self.choose_charset_button.caption) # charset scale charset_scale = "{:.2f}x".format(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 = " {} {} ".format( 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 = "{} {}".format( 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 += "{}".format(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, "{}".format(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()