import os.path import time from math import ceil from art import uv_names from renderable_line import UIRenderableX from ui_button import TEXT_CENTER, TEXT_RIGHT, UIButton from ui_colors import UIColors from ui_element import UIArt, UIElement, UIRenderable # buttons to toggle "affects" status / cycle through choices, respectively class StatusBarToggleButton(UIButton): caption_justify = TEXT_RIGHT class StatusBarCycleButton(UIButton): # do different stuff for left vs right click pass_mouse_button = True should_draw_caption = False width = 3 class CharToggleButton(StatusBarToggleButton): x = 0 caption = "ch:" width = len(caption) + 1 tooltip_on_hover = True def get_tooltip_text(self): return f"character index: {self.element.ui.selected_char}" def get_tooltip_location(self): return 1, self.element.get_tile_y() - 1 class CharCycleButton(StatusBarCycleButton): x = CharToggleButton.width tooltip_on_hover = True # reuse above def get_tooltip_text(self): return CharToggleButton.get_tooltip_text(self) def get_tooltip_location(self): return CharToggleButton.get_tooltip_location(self) class FGToggleButton(StatusBarToggleButton): x = CharCycleButton.x + CharCycleButton.width caption = "fg:" width = len(caption) + 1 tooltip_on_hover = True def get_tooltip_text(self): return f"foreground color index: {self.element.ui.selected_fg_color}" def get_tooltip_location(self): return 8, self.element.get_tile_y() - 1 class FGCycleButton(StatusBarCycleButton): x = FGToggleButton.x + FGToggleButton.width tooltip_on_hover = True def get_tooltip_text(self): return FGToggleButton.get_tooltip_text(self) def get_tooltip_location(self): return FGToggleButton.get_tooltip_location(self) class BGToggleButton(StatusBarToggleButton): x = FGCycleButton.x + FGCycleButton.width caption = "bg:" width = len(caption) + 1 tooltip_on_hover = True def get_tooltip_text(self): return f"background color index: {self.element.ui.selected_bg_color}" def get_tooltip_location(self): return 15, self.element.get_tile_y() - 1 class BGCycleButton(StatusBarCycleButton): x = BGToggleButton.x + BGToggleButton.width tooltip_on_hover = True def get_tooltip_text(self): return BGToggleButton.get_tooltip_text(self) def get_tooltip_location(self): return BGToggleButton.get_tooltip_location(self) class XformToggleButton(StatusBarToggleButton): x = BGCycleButton.x + BGCycleButton.width caption = "xform:" width = len(caption) + 1 # class for things like xform and tool whose captions you can cycle through class StatusBarTextCycleButton(StatusBarCycleButton): should_draw_caption = True caption_justify = TEXT_CENTER normal_fg_color = UIColors.lightgrey normal_bg_color = UIColors.black hovered_fg_color = UIColors.lightgrey hovered_bg_color = UIColors.black clicked_fg_color = UIColors.black clicked_bg_color = UIColors.white class XformCycleButton(StatusBarTextCycleButton): x = XformToggleButton.x + XformToggleButton.width width = len("Rotate 180") caption = uv_names[0] class ToolCycleButton(StatusBarTextCycleButton): x = XformCycleButton.x + XformCycleButton.width + len("tool:") + 1 # width and caption are set during status bar init after button is created class FileCycleButton(StatusBarTextCycleButton): caption = "[nothing]" class LayerCycleButton(StatusBarTextCycleButton): caption = "X/Y" width = len(caption) class FrameCycleButton(StatusBarTextCycleButton): caption = "X/Y" width = len(caption) class ZoomSetButton(StatusBarTextCycleButton): caption = "100.0" width = len(caption) class StatusBarUI(UIElement): snap_bottom = True snap_left = True always_consume_input = True dim_color = 12 swatch_width = 3 char_swatch_x = CharCycleButton.x fg_swatch_x = FGCycleButton.x bg_swatch_x = BGCycleButton.x tool_label = "tool:" tool_label_x = XformCycleButton.x + XformCycleButton.width + 1 tile_label = "tile:" layer_label = "layer:" frame_label = "frame:" zoom_label = "%" right_items_width = ( len(tile_label) + len(layer_label) + len(frame_label) + (len("X/Y") + 2) * 2 + len("XX/YY") + 2 + len(zoom_label) + 10 ) button_names = { CharToggleButton: "char_toggle", CharCycleButton: "char_cycle", FGToggleButton: "fg_toggle", FGCycleButton: "fg_cycle", BGToggleButton: "bg_toggle", BGCycleButton: "bg_cycle", XformToggleButton: "xform_toggle", XformCycleButton: "xform_cycle", ToolCycleButton: "tool_cycle", FileCycleButton: "file_cycle", LayerCycleButton: "layer_cycle", FrameCycleButton: "frame_cycle", ZoomSetButton: "zoom_set", } def __init__(self, ui): art = ui.active_art self.ui = ui # create 3 custom Arts w/ source charset and palette, renderables for each art_name = f"{int(time.time())}_{self.__class__.__name__}" self.char_art = UIArt( art_name, ui.app, art.charset, art.palette, self.swatch_width, 1 ) self.char_renderable = UIRenderable(ui.app, self.char_art) self.fg_art = UIArt( art_name, ui.app, art.charset, art.palette, self.swatch_width, 1 ) self.fg_renderable = UIRenderable(ui.app, self.fg_art) self.bg_art = UIArt( art_name, ui.app, art.charset, art.palette, self.swatch_width, 1 ) self.bg_renderable = UIRenderable(ui.app, self.bg_art) # "dimmed out" box self.dim_art = UIArt( art_name, ui.app, ui.charset, ui.palette, self.swatch_width + self.char_swatch_x, 1, ) self.dim_renderable = UIRenderable(ui.app, self.dim_art) self.dim_renderable.alpha = 0.75 # separate dimmed out box for xform, easier this way xform_width = XformToggleButton.width + XformCycleButton.width self.dim_xform_art = UIArt( art_name, ui.app, ui.charset, ui.palette, xform_width, 1 ) self.dim_xform_renderable = UIRenderable(ui.app, self.dim_xform_art) self.dim_xform_renderable.alpha = 0.75 # create clickable buttons self.buttons = [] self.button_map = {} for button_class, button_name in self.button_names.items(): button = button_class(self) setattr(self, button_name + "_button", button) cb_name = f"{button_name}_button_pressed" button.callback = getattr(self, cb_name) self.buttons.append(button) # keep a mapping of button names to buttons, for eg tooltip updates self.button_map[button_name] = button # some button captions, widths, locations will be set in reset_art # determine total width of left-justified items self.left_items_width = ( self.tool_cycle_button.x + self.tool_cycle_button.width + 15 ) # set some properties in bulk self.renderables = [] for r in [ self.char_renderable, self.fg_renderable, self.bg_renderable, self.dim_renderable, self.dim_xform_renderable, ]: r.ui = ui r.grain_strength = 0 # add to list of renderables to manage eg destroyed on quit self.renderables.append(r) # red X for transparent colors self.x_renderable = UIRenderableX(ui.app, self.char_art) # give it a special reference to this element self.x_renderable.status_bar = self self.renderables.append(self.x_renderable) UIElement.__init__(self, ui) # button callbacks def char_toggle_button_pressed(self): if self.ui.active_dialog: return self.ui.selected_tool.toggle_affects_char() def char_cycle_button_pressed(self, mouse_button): if self.ui.active_dialog: return if mouse_button == 1: self.ui.select_char(self.ui.selected_char + 1) elif mouse_button == 3: self.ui.select_char(self.ui.selected_char - 1) def fg_toggle_button_pressed(self): if self.ui.active_dialog: return self.ui.selected_tool.toggle_affects_fg() def fg_cycle_button_pressed(self, mouse_button): if self.ui.active_dialog: return if mouse_button == 1: self.ui.select_fg(self.ui.selected_fg_color + 1) elif mouse_button == 3: self.ui.select_fg(self.ui.selected_fg_color - 1) def bg_toggle_button_pressed(self): if self.ui.active_dialog: return self.ui.selected_tool.toggle_affects_bg() def bg_cycle_button_pressed(self, mouse_button): if self.ui.active_dialog: return if mouse_button == 1: self.ui.select_bg(self.ui.selected_bg_color + 1) elif mouse_button == 3: self.ui.select_bg(self.ui.selected_bg_color - 1) def xform_toggle_button_pressed(self): if self.ui.active_dialog: return self.ui.selected_tool.toggle_affects_xform() def xform_cycle_button_pressed(self, mouse_button): if self.ui.active_dialog: return if mouse_button == 1: self.ui.cycle_selected_xform() elif mouse_button == 3: self.ui.cycle_selected_xform(True) # update caption with new xform self.xform_cycle_button.caption = uv_names[self.ui.selected_xform] def tool_cycle_button_pressed(self, mouse_button): if self.ui.active_dialog: return if mouse_button == 1: self.ui.cycle_selected_tool() elif mouse_button == 3: self.ui.cycle_selected_tool(True) self.tool_cycle_button.caption = self.ui.selected_tool.get_button_caption() def file_cycle_button_pressed(self, mouse_button): if not self.ui.active_art: return if self.ui.active_dialog: return if mouse_button == 1: self.ui.next_active_art() elif mouse_button == 3: self.ui.previous_active_art() def layer_cycle_button_pressed(self, mouse_button): if not self.ui.active_art: return if self.ui.active_dialog: return if mouse_button == 1: self.ui.set_active_layer(self.ui.active_art.active_layer + 1) elif mouse_button == 3: self.ui.set_active_layer(self.ui.active_art.active_layer - 1) def frame_cycle_button_pressed(self, mouse_button): if not self.ui.active_art: return if self.ui.active_dialog: return if mouse_button == 1: self.ui.set_active_frame(self.ui.active_art.active_frame + 1) elif mouse_button == 3: self.ui.set_active_frame(self.ui.active_art.active_frame - 1) def zoom_set_button_pressed(self, mouse_button): if not self.ui.active_art: return if self.ui.active_dialog: return if mouse_button == 1: self.ui.app.camera.zoom_proportional(1) elif mouse_button == 3: self.ui.app.camera.zoom_proportional(-1) def reset_art(self): UIElement.reset_art(self) self.tile_width = ceil(self.ui.width_tiles * self.ui.scale) # must resize here, as window width will vary self.art.resize(self.tile_width, self.tile_height) # write chars/colors to the art self.rewrite_art() self.x_renderable.scale_x = self.char_art.width self.x_renderable.scale_y = -self.char_art.height # dim box self.dim_art.clear_frame_layer(0, 0, self.ui.colors.white) self.dim_art.update() self.dim_xform_art.clear_frame_layer(0, 0, self.ui.colors.white) self.dim_xform_art.update() # rebuild geo, elements may be new dimensions self.dim_art.geo_changed = True self.dim_xform_art.geo_changed = True self.char_art.geo_changed = True self.fg_art.geo_changed = True self.bg_art.geo_changed = True def rewrite_art(self): bg = self.ui.colors.white self.art.clear_frame_layer(0, 0, bg) # if user is making window reeeeally skinny, bail if self.tile_width < self.left_items_width: return # draw tool label self.art.write_string( 0, 0, self.tool_label_x, 0, self.tool_label, self.ui.palette.darkest_index ) # only draw right side info if the window is wide enough if self.art.width > self.left_items_width + self.right_items_width: self.file_cycle_button.visible = True self.layer_cycle_button.visible = True self.frame_cycle_button.visible = True self.zoom_set_button.visible = True self.write_right_elements() else: self.file_cycle_button.visible = False self.layer_cycle_button.visible = False self.frame_cycle_button.visible = False self.zoom_set_button.visible = False def set_active_charset(self, new_charset): self.char_art.charset = self.fg_art.charset = self.bg_art.charset = new_charset self.reset_art() def set_active_palette(self, new_palette): self.char_art.palette = self.fg_art.palette = self.bg_art.palette = new_palette self.reset_art() def get_tile_y(self): "returns tile coordinate Y position of bar" return ( int( self.ui.app.window_height / (self.ui.charset.char_height * self.ui.scale) ) - 1 ) def update_button_captions(self): "set captions for buttons that change from selections" art = self.ui.active_art self.xform_cycle_button.caption = uv_names[self.ui.selected_xform] self.tool_cycle_button.caption = self.ui.selected_tool.get_button_caption() self.tool_cycle_button.width = len(self.tool_cycle_button.caption) + 2 # right edge elements self.file_cycle_button.caption = ( os.path.basename(art.filename) if art else FileCycleButton.caption ) self.file_cycle_button.width = len(self.file_cycle_button.caption) + 2 # NOTE: button X offsets will be set in write_right_elements null = "---" layers = art.layers if art else 0 layer = f"{art.active_layer + 1}/{layers}" if art else null self.layer_cycle_button.caption = layer self.layer_cycle_button.width = len(self.layer_cycle_button.caption) frames = art.frames if art else 0 frame = f"{art.active_frame + 1}/{frames}" if art else null self.frame_cycle_button.caption = frame self.frame_cycle_button.width = len(self.frame_cycle_button.caption) # zoom % zoom = f"{self.ui.app.camera.get_current_zoom_pct():.1f}" if art else null self.zoom_set_button.caption = zoom[:5] # maintain size def update(self): if not self.ui.active_art: return # update buttons UIElement.update(self) # set color swatches for i in range(self.swatch_width): self.char_art.set_color_at(0, 0, i, 0, self.ui.selected_bg_color, False) self.fg_art.set_color_at(0, 0, i, 0, self.ui.selected_fg_color, False) self.bg_art.set_color_at(0, 0, i, 0, self.ui.selected_bg_color, False) # set char w/ correct FG color and xform self.char_art.set_char_index_at(0, 0, 1, 0, self.ui.selected_char) self.char_art.set_color_at(0, 0, 1, 0, self.ui.selected_fg_color, True) self.char_art.set_char_transform_at(0, 0, 1, 0, self.ui.selected_xform) # position elements self.position_swatch(self.char_renderable, self.char_swatch_x) self.position_swatch(self.fg_renderable, self.fg_swatch_x) self.position_swatch(self.bg_renderable, self.bg_swatch_x) # update buttons before redrawing art (ie non-interactive bits) self.update_button_captions() for art in [self.char_art, self.fg_art, self.bg_art]: art.update() self.rewrite_art() self.draw_buttons() def position_swatch(self, renderable, x_offset): renderable.x = (self.char_art.quad_width * x_offset) - 1 renderable.y = self.char_art.quad_height - 1 def reset_loc(self): UIElement.reset_loc(self) def write_right_elements(self): """ fills in right-justified parts of status bar, eg current frame/layer/tile labels (buttons positioned but drawn separately) """ dark = self.ui.colors.black light = self.ui.colors.white art = self.ui.active_art padding = 2 # position file button x = self.tile_width - (self.file_cycle_button.width + 1) self.file_cycle_button.x = x x -= padding # zoom self.art.write_string(0, 0, x, 0, self.zoom_label, dark, light, True) x -= len(self.zoom_label) + self.zoom_set_button.width self.zoom_set_button.x = x x -= padding # tile tile = "X/Y" color = light if self.ui.app.cursor and art: tile_x, tile_y = self.ui.app.cursor.get_tile() tile_y = int(tile_y) # user-facing coordinates are always base 1 tile_x += 1 tile_y += 1 if tile_x <= 0 or tile_x > art.width: color = self.dim_color if tile_y <= 0 or tile_y > art.height: color = self.dim_color tile_x = str(tile_x).rjust(3) tile_y = str(tile_y).rjust(3) tile = f"{tile_x},{tile_y}" self.art.write_string(0, 0, x, 0, tile, color, dark, True) # tile label x -= len(tile) self.art.write_string(0, 0, x, 0, self.tile_label, dark, light, True) # position layer button x -= padding + len(self.tile_label) + self.layer_cycle_button.width self.layer_cycle_button.x = x # layer label self.art.write_string(0, 0, x, 0, self.layer_label, dark, light, True) # position frame button x -= padding + len(self.layer_label) + self.frame_cycle_button.width self.frame_cycle_button.x = x # frame label self.art.write_string(0, 0, x, 0, self.frame_label, dark, light, True) def render(self): if not self.ui.active_art: return UIElement.render(self) # draw wireframe red X /behind/ char if BG transparent if self.ui.selected_bg_color == 0: self.x_renderable.x = self.char_renderable.x self.x_renderable.y = self.char_renderable.y self.x_renderable.render() self.char_renderable.render() self.fg_renderable.render() self.bg_renderable.render() # draw red X for transparent FG or BG if self.ui.selected_fg_color == 0: self.x_renderable.x = self.fg_renderable.x self.x_renderable.y = self.fg_renderable.y self.x_renderable.render() if self.ui.selected_bg_color == 0: self.x_renderable.x = self.bg_renderable.x self.x_renderable.y = self.bg_renderable.y self.x_renderable.render() # dim out items if brush is set to not affect them self.dim_renderable.y = self.char_renderable.y swatch_width = self.art.quad_width * StatusBarCycleButton.width if not self.ui.selected_tool.affects_char: self.dim_renderable.x = self.char_renderable.x - swatch_width self.dim_renderable.render() if not self.ui.selected_tool.affects_fg_color: self.dim_renderable.x = self.fg_renderable.x - swatch_width self.dim_renderable.render() if not self.ui.selected_tool.affects_bg_color: self.dim_renderable.x = self.bg_renderable.x - swatch_width self.dim_renderable.render() if not self.ui.selected_tool.affects_xform: # separate dimmer renderable for xform's wider size self.dim_xform_renderable.y = self.char_renderable.y self.dim_xform_renderable.x = XformToggleButton.x * self.art.quad_width - 1 self.dim_xform_renderable.render()