playscii/ui.py

879 lines
34 KiB
Python

import numpy as np
import sdl2
from OpenGL import GL
from PIL import Image
from art import (
UV_FLIP270,
UV_NORMAL,
uv_names,
)
from edit_command import EditCommand, EditCommandTile, EntireArtCommand
from texture import Texture
from ui_colors import UIColors
from ui_console import ConsoleUI
from ui_edit_panel import EditListPanel
from ui_element import (
DebugTextUI,
FPSCounterUI,
GameHoverLabel,
GameSelectionLabel,
MessageLineUI,
ToolTip,
UIArt,
)
from ui_menu_bar import ArtMenuBar, GameMenuBar
from ui_menu_pulldown import PulldownMenu
from ui_object_panel import EditObjectPanel
from ui_popup import ToolPopup
from ui_status_bar import StatusBarUI
from ui_tool import (
EraseTool,
FillTool,
GrabTool,
PasteTool,
PencilTool,
RotateTool,
SelectTool,
TextTool,
)
from ui_toolbar import ArtToolBar
UI_ASSET_DIR = "ui/"
SCALE_INCREMENT = 0.25
# spacing factor of each non-active document's scale from active document
MDI_MARGIN = 1.1
# overlay image scale types
OIS_WIDTH = 0
OIS_HEIGHT = 1
OIS_FILL = 2
class UI:
# user-configured UI scale factor
scale = 1.0
max_onion_alpha = 0.5
charset_name = "ui"
palette_name = "c64_original"
# red color for warnings
error_color_index = UIColors.brightred
# low-contrast background texture that distinguishes UI from flat color
grain_texture_path = UI_ASSET_DIR + "bgnoise_alpha.png"
# expose to classes that don't want to import this module
asset_dir = UI_ASSET_DIR
visible = True
logg = False
popup_hold_to_show = False
flip_affects_xforms = True
tool_classes = [
PencilTool,
EraseTool,
GrabTool,
RotateTool,
TextTool,
SelectTool,
PasteTool,
FillTool,
]
tool_selected_log = "tool selected"
art_selected_log = "Now editing"
frame_selected_log = "Now editing frame %s (hold time %ss)"
layer_selected_log = "Now editing layer: %s"
swap_color_log = "Swapped FG/BG colors"
affects_char_on_log = "will affect characters"
affects_char_off_log = "will not affect characters"
affects_fg_on_log = "will affect foreground colors"
affects_fg_off_log = "will not affect foreground colors"
affects_bg_on_log = "will affect background colors"
affects_bg_off_log = "will not affect background colors"
affects_xform_on_log = "will affect character rotation/flip"
affects_xform_off_log = "will not affect character rotation/flip"
xform_selected_log = "Selected character transform:"
show_edit_ui_log = "Edit UI hidden, press %s to unhide."
def __init__(self, app, active_art):
self.app = app
# the current art being edited
self.active_art = active_art
# dialog box set here
self.active_dialog = None
# keyboard-navigabnle element with current focus
self.keyboard_focus_element = None
# easy color index lookups
self.colors = UIColors()
# for UI, view /and/ projection matrix are identity
# (aspect correction is done in set_scale)
self.view_matrix = np.eye(4, 4, dtype=np.float32)
self.charset = self.app.load_charset(self.charset_name, False)
self.palette = self.app.load_palette(self.palette_name, False)
# currently selected char, fg color, bg color, xform - from art
self.selected_char = self.active_art.selected_char
self.selected_fg_color = self.active_art.selected_fg_color
self.selected_bg_color = self.active_art.selected_bg_color
self.selected_xform = self.active_art.selected_xform
self.selected_tool, self.previous_tool = None, None
# set True when tool settings change, cleared after update, used by
# cursor to determine if cursor update needed
self.tool_settings_changed = False
self.tools = []
# create tools
for t in self.tool_classes:
new_tool = t(self)
tool_name = f"{new_tool.name}_tool"
setattr(self, tool_name, new_tool)
# stick in a list for popup tool tab
self.tools.append(new_tool)
self.selected_tool = self.pencil_tool
# clipboard: list of EditCommandTiles, set by cut/copy, used by paste
self.clipboard = []
# track clipboard contents' size so we don't have to recompute it every
# cursor preview update
self.clipboard_width = 0
self.clipboard_height = 0
# create elements
self.elements = []
self.hovered_elements = []
# set geo sizes, force scale update
self.set_scale(self.scale)
self.fps_counter = FPSCounterUI(self)
self.console = ConsoleUI(self)
self.status_bar = StatusBarUI(self)
self.popup = ToolPopup(self)
self.message_line = MessageLineUI(self)
self.debug_text = DebugTextUI(self)
self.pulldown = PulldownMenu(self)
self.tooltip = ToolTip(self)
self.menu_bar = None
self.art_menu_bar = ArtMenuBar(self)
self.game_menu_bar = GameMenuBar(self)
self.menu_bar = self.art_menu_bar
self.art_toolbar = ArtToolBar(self)
self.edit_list_panel = EditListPanel(self)
self.edit_object_panel = EditObjectPanel(self)
self.game_selection_label = GameSelectionLabel(self)
self.game_hover_label = GameHoverLabel(self)
self.elements += [
self.fps_counter,
self.status_bar,
self.popup,
self.message_line,
self.debug_text,
self.pulldown,
self.art_menu_bar,
self.game_menu_bar,
self.tooltip,
self.art_toolbar,
self.edit_list_panel,
self.edit_object_panel,
self.game_hover_label,
self.game_selection_label,
]
# add console last so it draws last
self.elements.append(self.console)
# grain texture
img = Image.open(self.grain_texture_path)
img = img.convert("RGBA")
width, height = img.size
self.grain_texture = Texture(img.tobytes(), width, height)
self.grain_texture.set_wrap(True)
self.grain_texture.set_filter(GL.GL_LINEAR, GL.GL_LINEAR_MIPMAP_LINEAR)
# update elements that weren't created when UI scale was determined
self.set_elements_scale()
# if editing is disallowed, hide game mode UI
if not self.app.can_edit:
self.set_game_edit_ui_visibility(False)
def set_scale(self, new_scale):
old_scale = self.scale
self.scale = new_scale
# update UI renderable geo sizes for new scale
# determine width and height of current window in chars
# use floats, window might be a fractional # of chars wide/tall
aspect = float(self.app.window_width) / self.app.window_height
inv_aspect = float(self.app.window_height) / self.app.window_width
# MAYBE-TODO: this math is correct but hard to follow, rewrite for clarity
width = self.app.window_width / (
self.charset.char_width * self.scale * inv_aspect
)
height = self.app.window_height / (
self.charset.char_height * self.scale * inv_aspect
)
# any new UI elements created should use new scale
UIArt.quad_width = 2 / width * aspect
UIArt.quad_height = 2 / height * aspect
self.width_tiles = width * (inv_aspect / self.scale)
self.height_tiles = height / self.scale
# tell elements to refresh
self.set_elements_scale()
if self.scale != old_scale:
self.message_line.post_line(
f"UI scale is now {self.scale} ({self.width_tiles:.3f} x {self.height_tiles:.3f})"
)
def set_elements_scale(self):
for e in self.elements:
e.art.quad_width, e.art.quad_height = UIArt.quad_width, UIArt.quad_height
# Art dimensions may well need to change
e.reset_art()
e.reset_loc()
e.art.geo_changed = True
def window_resized(self):
# recalc renderables' quad size (same scale, different aspect)
self.set_scale(self.scale)
def size_and_position_overlay_image(self):
# called any time active art changes, or active art changes size
r = self.app.overlay_renderable
if not r:
return
# scale aspect based on user setting
aspect = float(r.texture.width) / r.texture.height
if self.app.overlay_scale_type == OIS_WIDTH:
r.scale_x = self.active_art.width * self.active_art.quad_width
r.scale_y = r.scale_x / aspect
elif self.app.overlay_scale_type == OIS_HEIGHT:
r.scale_y = self.active_art.height * self.active_art.quad_height
r.scale_x = r.scale_y * aspect
elif self.app.overlay_scale_type == OIS_FILL:
r.scale_x = self.active_art.width * self.active_art.quad_width
r.scale_y = self.active_art.height * self.active_art.quad_height
r.y = -r.scale_y
r.z = self.active_art.layers_z[self.active_art.active_layer]
def set_active_art(self, new_art):
self.active_art = new_art
new_charset = self.active_art.charset
new_palette = self.active_art.palette
# make sure selection isn't out of bounds in new art
old_selection = self.select_tool.selected_tiles.copy()
for tile in old_selection:
x, y = tile[0], tile[1]
if x >= new_art.width or y >= new_art.height:
self.select_tool.selected_tiles.pop(tile, None)
# keep cursor in bounds
self.app.cursor.clamp_to_active_art()
# set camera bounds based on art size
self.app.camera.set_for_art(new_art)
# set for popup
self.popup.set_active_charset(new_charset)
self.popup.set_active_palette(new_palette)
# if popup up eg toggled, redraw it completely
if self.popup.visible:
self.popup.reset_art()
self.popup.reset_loc()
# set to art's selected tile attributes
self.selected_char = self.active_art.selected_char
self.selected_fg_color = self.active_art.selected_fg_color
self.selected_bg_color = self.active_art.selected_bg_color
self.selected_xform = self.active_art.selected_xform
self.reset_onion_frames()
self.reset_edit_renderables()
# now that renderables are moved, rescale/reposition grid
self.app.grid.reset()
# rescale/reposition overlay image
self.size_and_position_overlay_image()
# tell select tool renderables
for r in [self.select_tool.select_renderable, self.select_tool.drag_renderable]:
r.quad_size_ref = new_art
r.rebuild_geo(self.select_tool.selected_tiles)
self.app.update_window_title()
if self.app.can_edit:
self.message_line.post_line(
f"{self.art_selected_log} {self.active_art.filename}"
)
def set_active_art_by_filename(self, art_filename):
for idx, art in enumerate(self.app.art_loaded_for_edit):
if art_filename == art.filename:
i = idx
break
else:
i = 0
new_active_art = self.app.art_loaded_for_edit.pop(i)
self.app.art_loaded_for_edit.insert(0, new_active_art)
new_active_renderable = self.app.edit_renderables.pop(i)
self.app.edit_renderables.insert(0, new_active_renderable)
self.set_active_art(new_active_art)
def previous_active_art(self):
"cycles to next art in app.art_loaded_for_edit"
if len(self.app.art_loaded_for_edit) == 1:
return
next_active_art = self.app.art_loaded_for_edit.pop(-1)
self.app.art_loaded_for_edit.insert(0, next_active_art)
next_active_renderable = self.app.edit_renderables.pop(-1)
self.app.edit_renderables.insert(0, next_active_renderable)
self.set_active_art(self.app.art_loaded_for_edit[0])
def next_active_art(self):
if len(self.app.art_loaded_for_edit) == 1:
return
last_active_art = self.app.art_loaded_for_edit.pop(0)
self.app.art_loaded_for_edit.append(last_active_art)
last_active_renderable = self.app.edit_renderables.pop(0)
self.app.edit_renderables.append(last_active_renderable)
self.set_active_art(self.app.art_loaded_for_edit[0])
def set_selected_tool(self, new_tool):
if self.app.game_mode:
return
# don't re-select same tool, except to cycle fill tool (see below)
if new_tool == self.selected_tool and type(new_tool) is not FillTool:
return
# bail out of text entry if active
if self.selected_tool is self.text_tool:
self.text_tool.finish_entry()
self.previous_tool = self.selected_tool
self.selected_tool = new_tool
self.popup.reset_art()
self.tool_settings_changed = True
# special case:
# if we're selecting the fill tool and it's already selected,
# cycle through its 3 modes (char/fg/bg boundary)
cycled_fill = False
if (
type(self.selected_tool) is FillTool
and type(self.previous_tool) is FillTool
):
self.selected_tool.boundary_mode = self.selected_tool.next_boundary_modes[
self.selected_tool.boundary_mode
]
# TODO: do we need a message line message for this?
# self.app.log(self.selected_tool.boundary_mode)
cycled_fill = True
# close menu if we selected tool from it
if self.menu_bar.active_menu_name and not cycled_fill:
self.menu_bar.close_active_menu()
self.message_line.post_line(
f"{self.selected_tool.get_button_caption()} {self.tool_selected_log}"
)
def cycle_fill_tool_mode(self):
self.set_selected_tool(self.fill_tool)
def get_longest_tool_name_length(self):
"VERY specific function to help status bar draw its buttons"
longest = 0
for tool in self.tools:
if len(tool.button_caption) > longest:
longest = len(tool.button_caption)
return longest
def cycle_selected_tool(self, back=False):
if not self.active_art:
return
tool_index = self.tools.index(self.selected_tool)
if back:
tool_index -= 1
else:
tool_index += 1
tool_index %= len(self.tools)
self.set_selected_tool(self.tools[tool_index])
def set_selected_xform(self, new_xform):
self.selected_xform = new_xform
self.popup.set_xform(new_xform)
self.tool_settings_changed = True
line = f"{self.xform_selected_log} {uv_names[self.selected_xform]}"
self.message_line.post_line(line)
def cycle_selected_xform(self, back=False):
if self.app.game_mode:
return
xform = self.selected_xform
if back:
xform -= 1
else:
xform += 1
xform %= UV_FLIP270 + 1
self.set_selected_xform(xform)
def reset_onion_frames(self, new_art=None):
"set correct visibility, frame, and alpha for all onion renderables"
new_art = new_art or self.active_art
alpha = self.max_onion_alpha
total_onion_frames = 0
def set_onion(r, new_frame, alpha):
# scale back if fewer than MAX_ONION_FRAMES in either direction
if total_onion_frames >= new_art.frames:
r.visible = False
return
r.visible = True
if new_art is not r.art:
r.set_art(new_art)
r.set_frame(new_frame)
r.alpha = alpha
# make BG dimmer so it's easier to see
r.bg_alpha = alpha / 2
# populate "next" frames first
for i, r in enumerate(self.app.onion_renderables_next):
total_onion_frames += 1
new_frame = new_art.active_frame + i + 1
set_onion(r, new_frame, alpha)
alpha /= 2
# print('next onion %s set to frame %s alpha %s' % (i, new_frame, alpha))
alpha = self.max_onion_alpha
for i, r in enumerate(self.app.onion_renderables_prev):
total_onion_frames += 1
new_frame = new_art.active_frame - (i + 1)
set_onion(r, new_frame, alpha)
# each successive onion layer is dimmer
alpha /= 2
# print('previous onion %s set to frame %s alpha %s' % (i, new_frame, alpha))
def set_active_frame(self, new_frame):
if not self.active_art.set_active_frame(new_frame):
return
self.reset_onion_frames()
self.tool_settings_changed = True
frame = self.active_art.active_frame
delay = self.active_art.frame_delays[frame]
if self.app.can_edit:
self.message_line.post_line(self.frame_selected_log % (frame + 1, delay))
def set_active_layer(self, new_layer):
self.active_art.set_active_layer(new_layer)
z = self.active_art.layers_z[self.active_art.active_layer]
self.app.grid.z = z
self.select_tool.select_renderable.z = z
self.select_tool.drag_renderable.z = z
self.app.cursor.z = z
self.app.update_window_title()
self.tool_settings_changed = True
layer_name = self.active_art.layer_names[self.active_art.active_layer]
if self.app.can_edit:
self.message_line.post_line(self.layer_selected_log % layer_name)
self.size_and_position_overlay_image()
def select_char(self, new_char_index):
if not self.active_art:
return
# wrap at last valid index
self.selected_char = new_char_index % self.active_art.charset.last_index
# only update char tooltip if it was already up; avoid stomping others
char_cycle_button = self.status_bar.button_map["char_cycle"]
char_toggle_button = self.status_bar.button_map["char_toggle"]
if (
char_cycle_button in self.status_bar.hovered_buttons
or char_toggle_button in self.status_bar.hovered_buttons
):
char_toggle_button.update_tooltip()
self.tool_settings_changed = True
def select_color(self, new_color_index, fg):
"common code for select_fg/bg"
if not self.active_art:
return
new_color_index %= len(self.active_art.palette.colors)
if fg:
self.selected_fg_color = new_color_index
else:
self.selected_bg_color = new_color_index
# same don't-stomp-another-tooltip check as above
toggle_button = (
self.status_bar.button_map["fg_toggle"]
if fg
else self.status_bar.button_map["bg_toggle"]
)
cycle_button = (
self.status_bar.button_map["fg_cycle"]
if fg
else self.status_bar.button_map["bg_cycle"]
)
if (
toggle_button in self.status_bar.hovered_buttons
or cycle_button in self.status_bar.hovered_buttons
):
toggle_button.update_tooltip()
self.tool_settings_changed = True
def select_fg(self, new_fg_index):
self.select_color(new_fg_index, True)
def select_bg(self, new_bg_index):
self.select_color(new_bg_index, False)
def swap_fg_bg_colors(self):
if self.app.game_mode:
return
fg, bg = self.selected_fg_color, self.selected_bg_color
self.selected_fg_color, self.selected_bg_color = bg, fg
self.tool_settings_changed = True
self.message_line.post_line(self.swap_color_log)
def cut_selection(self):
self.copy_selection()
self.erase_tiles_in_selection()
def erase_selection_or_art(self):
if len(self.select_tool.selected_tiles) > 0:
self.erase_tiles_in_selection()
return
self.select_all()
self.erase_tiles_in_selection()
self.select_none()
def erase_tiles_in_selection(self):
# create and commit command group to clear all tiles in selection
frame, layer = self.active_art.active_frame, self.active_art.active_layer
new_command = EditCommand(self.active_art)
for tile in self.select_tool.selected_tiles:
new_tile_command = EditCommandTile(self.active_art)
new_tile_command.set_tile(frame, layer, *tile)
b_char, b_fg, b_bg, b_xform = self.active_art.get_tile_at(
frame, layer, *tile
)
new_tile_command.set_before(b_char, b_fg, b_bg, b_xform)
a_char = a_fg = 0
a_xform = UV_NORMAL
# clear to current BG
a_bg = self.selected_bg_color
new_tile_command.set_after(a_char, a_fg, a_bg, a_xform)
new_command.add_command_tiles([new_tile_command])
new_command.apply()
self.active_art.command_stack.commit_commands([new_command])
self.active_art.set_unsaved_changes(True)
def copy_selection(self):
# convert current selection tiles (active frame+layer) into
# EditCommandTiles for Cursor.preview_edits
# (via PasteTool get_paint_commands)
self.clipboard = []
frame, layer = self.active_art.active_frame, self.active_art.active_layer
min_x, min_y = 9999, 9999
max_x, max_y = -1, -1
for tile in self.select_tool.selected_tiles:
x, y = tile[0], tile[1]
if x < min_x:
min_x = x
elif x > max_x:
max_x = x
if y < min_y:
min_y = y
elif y > max_y:
max_y = y
art = self.active_art
new_tile_command = EditCommandTile(art)
new_tile_command.set_tile(frame, layer, x, y)
a_char, a_fg, a_bg, a_xform = art.get_tile_at(frame, layer, x, y)
# set data as "after" state, before will be set by cursor hover
new_tile_command.set_after(a_char, a_fg, a_bg, a_xform)
self.clipboard.append(new_tile_command)
# rebase tiles at top left corner of clipboard tiles
for tile_command in self.clipboard:
x = tile_command.x - min_x
y = tile_command.y - min_y
tile_command.set_tile(frame, layer, x, y)
self.clipboard_width = max_x - min_x
self.clipboard_height = max_y - min_y
def crop_to_selection(self, art):
# ignore non-rectangular selection features, use top left and bottom
# right corners
if len(self.select_tool.selected_tiles) == 0:
return
min_x, max_x = 99999, -1
min_y, max_y = 99999, -1
for tile in self.select_tool.selected_tiles:
x, y = tile[0], tile[1]
if x < min_x:
min_x = x
elif x > max_x:
max_x = x
if y < min_y:
min_y = y
elif y > max_y:
max_y = y
w = max_x - min_x + 1
h = max_y - min_y + 1
# create command for undo/redo
command = EntireArtCommand(art, min_x, min_y)
command.save_tiles(before=True)
art.resize(w, h, min_x, min_y)
self.app.log(f"Resized {art.filename} to {w} x {h}")
art.set_unsaved_changes(True)
# clear selection to avoid having tiles we know are OoB selected
self.select_tool.selected_tiles = {}
self.adjust_for_art_resize(art)
# commit command
command.save_tiles(before=False)
art.command_stack.commit_commands([command])
def reset_edit_renderables(self):
# reposition all art renderables and change their opacity
x, y = 0, 0
for i, r in enumerate(self.app.edit_renderables):
# always put active art at 0,0
if r in self.active_art.renderables:
r.alpha = 1
# if game mode, don't lerp
if self.app.game_mode:
r.snap_to(0, 0, 0)
else:
r.move_to(0, 0, 0, 0.2)
else:
r.alpha = 0.5
if self.app.game_mode:
# shift arts progressively further back
r.snap_to(x, y, -i)
else:
r.move_to(x * MDI_MARGIN, 0, -i, 0.2)
x += r.art.width * r.art.quad_width
y -= r.art.height * r.art.quad_height
def adjust_for_art_resize(self, art):
if art is not self.active_art:
return
# update grid, camera, cursor, overlay
self.app.camera.set_for_art(art)
self.app.camera.toggle_zoom_extents(override=True)
self.size_and_position_overlay_image()
self.reset_edit_renderables()
self.app.grid.reset()
if self.app.cursor.x > art.width:
self.app.cursor.x = art.width
if self.app.cursor.y > art.height:
self.app.cursor.y = art.height
self.app.cursor.moved = True
def resize_art(self, art, new_width, new_height, origin_x, origin_y, bg_fill):
# create command for undo/redo
command = EntireArtCommand(art, origin_x, origin_y)
command.save_tiles(before=True)
# resize
art.resize(new_width, new_height, origin_x, origin_y, bg_fill)
self.adjust_for_art_resize(art)
# commit command
command.save_tiles(before=False)
art.command_stack.commit_commands([command])
art.set_unsaved_changes(True)
def select_none(self):
self.select_tool.selected_tiles = {}
def select_all(self):
self.select_tool.selected_tiles = {}
for y in range(self.active_art.height):
for x in range(self.active_art.width):
self.select_tool.selected_tiles[(x, y)] = True
def invert_selection(self):
old_selection = self.select_tool.selected_tiles.copy()
self.select_tool.selected_tiles = {}
for y in range(self.active_art.height):
for x in range(self.active_art.width):
if not old_selection.get((x, y), False):
self.select_tool.selected_tiles[(x, y)] = True
def get_screen_coords(self, window_x, window_y):
x = (2 * window_x) / self.app.window_width - 1
y = (-2 * window_y) / self.app.window_height + 1
return x, y
def update(self):
self.select_tool.update()
# window coordinates -> OpenGL coordinates
mx, my = self.get_screen_coords(self.app.mouse_x, self.app.mouse_y)
# test elements for hover
was_hovering = self.hovered_elements[:]
self.hovered_elements = []
for e in self.elements:
# don't hover anything while console is up
if self.console.visible:
continue
# only check visible elements
if (
self.app.has_mouse_focus
and e.is_visible()
and e.can_hover
and e.is_inside(mx, my)
):
self.hovered_elements.append(e)
# only hover if we weren't last update
if e not in was_hovering:
e.hovered()
for e in was_hovering:
# unhover if app window loses mouse focus
if not self.app.has_mouse_focus or e not in self.hovered_elements:
e.unhovered()
# update all elements, regardless of whether they're being hovered etc
for e in self.elements:
# don't update invisible items
if e.is_visible() or e.update_when_invisible:
e.update()
# art update: tell renderables to refresh buffers
e.art.update()
self.tool_settings_changed = False
def clicked(self, mouse_button):
handled = False
# return True if any button handled the input
for e in self.hovered_elements:
if not e.is_visible():
continue
if e.clicked(mouse_button):
handled = True
# close pulldown if clicking outside it / the menu bar
if (
self.pulldown.visible
and self.pulldown not in self.hovered_elements
and self.menu_bar not in self.hovered_elements
):
self.menu_bar.close_active_menu()
return handled
def unclicked(self, mouse_button):
handled = False
for e in self.hovered_elements:
if e.unclicked(mouse_button):
handled = True
return handled
def wheel_moved(self, wheel_y):
handled = False
# use wheel to scroll chooser dialogs
# TODO: look up "up arrow" bind instead? how to get
# an SDL keycode from that?
if self.active_dialog:
keycode = sdl2.SDLK_UP if wheel_y > 0 else sdl2.SDLK_DOWN
self.active_dialog.handle_input(
keycode,
self.app.il.shift_pressed,
self.app.il.alt_pressed,
self.app.il.ctrl_pressed,
)
handled = True
elif len(self.hovered_elements) > 0:
for e in self.hovered_elements:
if e.wheel_moved(wheel_y):
handled = True
return handled
def quick_grab(self):
if self.app.game_mode:
return
if self.console.visible or self.popup in self.hovered_elements:
return
self.grab_tool.grab()
self.tool_settings_changed = True
def undo(self):
# if still painting, finish
if self.app.cursor.current_command:
self.app.cursor.finish_paint()
self.active_art.command_stack.undo()
self.active_art.set_unsaved_changes(True)
def redo(self):
self.active_art.command_stack.redo()
def open_dialog(self, dialog_class, options={}):
if self.app.game_mode and not dialog_class.game_mode_visible:
return
dialog = dialog_class(self, options)
self.active_dialog = dialog
self.keyboard_focus_element = self.active_dialog
# insert dialog at index 0 so it draws first instead of last
# self.elements.insert(0, dialog)
self.elements.remove(self.console)
self.elements.append(dialog)
self.elements.append(self.console)
def is_game_edit_ui_visible(self):
return self.game_menu_bar.visible
def set_game_edit_ui_visibility(self, visible, show_message=True):
self.game_menu_bar.visible = visible
self.edit_list_panel.visible = visible
self.edit_object_panel.visible = visible
if not visible:
# relinquish keyboard focus in play mode
self.keyboard_focus_element = None
if show_message and self.app.il:
bind = self.app.il.get_command_shortcut("toggle_game_edit_ui")
bind = bind.title()
self.message_line.post_line(self.show_edit_ui_log % bind, 10)
else:
self.message_line.post_line("")
self.app.update_window_title()
def object_selection_changed(self):
if len(self.app.gw.selected_objects) == 0:
self.keyboard_focus_element = None
self.refocus_keyboard()
def switch_edit_panel_focus(self, reverse=False):
# only allow tabbing away if list panel is in allowed mode
lp = self.edit_list_panel
if (
self.keyboard_focus_element is lp
and lp.list_operation in lp.list_operations_allow_kb_focus
and self.active_dialog
):
self.keyboard_focus_element = self.active_dialog
# prevent any other tabbing away from active dialog
if self.active_dialog:
return
# cycle keyboard focus between possible panels
focus_elements = [None]
if self.edit_list_panel.is_visible():
focus_elements.append(self.edit_list_panel)
if self.edit_object_panel.is_visible():
focus_elements.append(self.edit_object_panel)
if len(focus_elements) == 1:
return
focus_elements.append(None)
# handle shift-tab
if reverse:
focus_elements.reverse()
for i, element in enumerate(focus_elements[:-1]):
if self.keyboard_focus_element is element:
self.keyboard_focus_element = focus_elements[i + 1]
break
# update keyboard hover for both
self.edit_object_panel.update_keyboard_hover()
self.edit_list_panel.update_keyboard_hover()
def refocus_keyboard(self):
"called when an element closes, sets new keyboard_focus_element"
if self.active_dialog:
self.keyboard_focus_element = self.active_dialog
if self.keyboard_focus_element:
return
if self.popup.visible:
self.keyboard_focus_element = self.popup
elif self.pulldown.visible:
self.keyboard_focus_element = self.pulldown
elif (
self.edit_list_panel.is_visible()
and not self.edit_object_panel.is_visible()
):
self.keyboard_focus_element = self.edit_list_panel
elif (
self.edit_object_panel.is_visible()
and not self.edit_list_panel.is_visible()
):
self.keyboard_focus_element = self.edit_object_panel
def keyboard_navigate(self, move_x, move_y):
self.keyboard_focus_element.keyboard_navigate(move_x, move_y)
def toggle_game_edit_ui(self):
# if editing is disallowed, only run this once to disable UI
if not self.app.can_edit or not self.app.game_mode:
return
self.set_game_edit_ui_visibility(not self.game_menu_bar.visible)
def destroy(self):
for e in self.elements:
e.destroy()
self.grain_texture.destroy()
def render(self):
for e in self.elements:
if e.is_visible():
e.render()