playscii/input_handler.py

1367 lines
51 KiB
Python

import ctypes
import os
import platform
from sys import exit
import sdl2
from art import ART_DIR, ART_FILE_EXTENSION
from collision import CT_NONE
from key_shifts import NUMLOCK_OFF_MAP, NUMLOCK_ON_MAP
from renderable import LAYER_VIS_DIM, LAYER_VIS_FULL, LAYER_VIS_NONE
from ui import OIS_FILL, OIS_HEIGHT, OIS_WIDTH, SCALE_INCREMENT
from ui_art_dialog import (
AddFrameDialog,
AddLayerDialog,
CloseUnsavedChangesDialog,
DuplicateFrameDialog,
DuplicateLayerDialog,
ExportFileDialog,
ExportOptionsDialog,
FrameDelayAllDialog,
FrameDelayDialog,
FrameIndexDialog,
ImportFileDialog,
NewArtDialog,
OverlayImageOpacityDialog,
QuitUnsavedChangesDialog,
ResizeArtDialog,
RevertChangesDialog,
SaveAsDialog,
SetCameraZoomDialog,
SetLayerNameDialog,
SetLayerZDialog,
)
from ui_file_chooser_dialog import (
ArtChooserDialog,
CharSetChooserDialog,
OverlayImageFileChooserDialog,
PaletteChooserDialog,
PaletteFromImageChooserDialog,
RunArtScriptDialog,
)
from ui_game_dialog import (
AddRoomDialog,
NewGameDirDialog,
RenameRoomDialog,
SaveGameStateDialog,
SetRoomBoundsObjDialog,
SetRoomCamDialog,
SetRoomEdgeWarpsDialog,
)
from ui_list_operations import (
LO_LOAD_STATE,
LO_OPEN_GAME_DIR,
LO_SELECT_OBJECTS,
LO_SET_OBJECT_ROOMS,
LO_SET_ROOM,
LO_SET_ROOM_CAMERA,
LO_SET_ROOM_EDGE_OBJ,
LO_SET_ROOM_EDGE_WARP,
LO_SET_ROOM_OBJECTS,
LO_SET_SPAWN_CLASS,
)
BINDS_FILENAME = "binds.cfg"
BINDS_TEMPLATE_FILENAME = "binds.cfg.default"
class InputLord:
"sets up key binds and handles input"
wheel_zoom_amount = 3.0
keyboard_zoom_amount = 1.0
def __init__(self, app):
self.app = app
self.ui = self.app.ui
# read from binds.cfg file or create it from template
# exec results in edit_binds, a dict whose keys are keys+mods
# and whose values are bound functions
self.edit_bind_src = None
# bad probs if a command isn't in binds.cfg, so just blow it away
# if the template is newer than it
# TODO: better solution is find any binds in template but not binds.cfg
# and add em
binds_filename = self.app.config_dir + BINDS_FILENAME
binds_outdated = not os.path.exists(binds_filename) or os.path.getmtime(
binds_filename
) < os.path.getmtime(BINDS_TEMPLATE_FILENAME)
if not binds_outdated and os.path.exists(binds_filename):
exec(open(binds_filename).read())
self.app.log("Loaded key binds from {}".format(binds_filename))
else:
default_data = open(BINDS_TEMPLATE_FILENAME).readlines()[1:]
new_binds = open(binds_filename, "w")
new_binds.writelines(default_data)
new_binds.close()
self.app.log("Created new key binds file {}".format(binds_filename))
exec("".join(default_data))
if not self.edit_bind_src:
self.app.log("No bind data found, Is binds.cfg.default present?")
exit()
# associate key + mod combos with methods
self.edit_binds = {}
for bind_string in self.edit_bind_src:
bind = self.parse_key_bind(bind_string)
if not bind:
continue
# bind data could be a single item (string) or a list/tuple
bind_data = self.edit_bind_src[bind_string]
if type(bind_data) is str:
bind_fnames = ["BIND_{}".format(bind_data)]
else:
bind_fnames = ["BIND_{}".format(s) for s in bind_data]
bind_functions = []
for bind_fname in bind_fnames:
if not hasattr(self, bind_fname):
continue
bind_functions.append(getattr(self, bind_fname))
self.edit_binds[bind] = bind_functions
# get controller(s)
# TODO: use kewl SDL2 gamepad system
js_init = sdl2.SDL_InitSubSystem(sdl2.SDL_INIT_JOYSTICK)
if js_init != 0:
self.app.log(
"SDL2: Couldn't initialize joystick subsystem, code {}".format(js_init)
)
return
sticks = sdl2.SDL_NumJoysticks()
# self.app.log('%s gamepads found' % sticks)
self.gamepad = None
self.gamepad_left_x, self.gamepad_left_y = 0, 0
# for now, just grab first pad
if sticks > 0:
pad = sdl2.SDL_JoystickOpen(0)
pad_name = sdl2.SDL_JoystickName(pad).decode("utf-8")
pad_axes = sdl2.SDL_JoystickNumAxes(pad)
pad_buttons = sdl2.SDL_JoystickNumButtons(pad)
self.app.log(
"Gamepad found: {} with {} axes, {} buttons".format(
pad_name, pad_axes, pad_buttons
)
)
self.gamepad = pad
# before main loop begins, set initial mouse position -
# SDL_GetMouseState returns 0,0 if the mouse hasn't yet moved
# in the new window!
wx, wy = ctypes.c_int(0), ctypes.c_int(0)
sdl2.SDL_GetWindowPosition(self.app.window, wx, wy)
wx, wy = int(wx.value), int(wy.value)
mx, my = ctypes.c_int(0), ctypes.c_int(0)
sdl2.mouse.SDL_GetGlobalMouseState(mx, my)
mx, my = int(mx.value), int(my.value)
self.app.mouse_x, self.app.mouse_y = mx - wx, my - wy
# set flag so we know whether handle_input's SDL_GetMouseState result
# is accurate :/
self.mouse_has_moved = False
def parse_key_bind(self, in_string):
"returns a tuple of (key, mod1, mod2) key bind data from given string"
shift = False
alt = False
ctrl = False
key = None
for i in in_string.split():
if i.lower() == "shift":
shift = True
elif i.lower() == "alt":
alt = True
elif i.lower() == "ctrl":
ctrl = True
else:
key = i
return (key, shift, alt, ctrl)
def get_bind_functions(self, keysym, shift, alt, ctrl):
"returns a list of methods for the given key + mods if one exists"
keystr = sdl2.SDL_GetKeyName(keysym).decode().lower()
key_data = (keystr, shift, alt, ctrl)
return self.edit_binds.get(key_data, [])
def get_keysym(self, event):
"get SDL2 keysym from event; right now only used to check numlock variants"
numlock_on = bool(event.key.keysym.mod & sdl2.KMOD_NUM)
keysym = event.key.keysym.sym
# if numlock is on, treat numpad keys like numbers
if numlock_on and keysym in NUMLOCK_ON_MAP:
return NUMLOCK_ON_MAP[keysym]
elif not numlock_on and keysym in NUMLOCK_OFF_MAP:
return NUMLOCK_OFF_MAP[keysym]
return keysym
def get_command_shortcut(self, command_function):
for bind in self.edit_bind_src:
if command_function == self.edit_bind_src[bind]:
return bind
return ""
def get_menu_items_for_command_function(self, function):
# search both menus for items; command checks
buttons = self.ui.art_menu_bar.menu_buttons + self.ui.game_menu_bar.menu_buttons
items = []
for button in buttons:
# skip eg playscii button
if not hasattr(button, "menu_data"):
continue
for item in button.menu_data.items:
if function.__name__ == "BIND_{}".format(item.command):
items.append(item)
return items
def is_command_function_allowed(self, function):
"returns True if given function's menu bar item is available"
items = self.get_menu_items_for_command_function(function)
if not items:
return True
# return True if ANY items are active
for item in items:
if not item.always_active and item.should_dim(self.app):
continue
if item.art_mode_allowed and not self.app.game_mode:
return True
if item.game_mode_allowed and self.app.game_mode:
return True
return False
def handle_input(self):
app = self.app
# get and store mouse state
# (store everything in parent app object so stuff can access it easily)
mx, my = ctypes.c_int(0), ctypes.c_int(0)
mouse = sdl2.mouse.SDL_GetMouseState(mx, my)
app.left_mouse = bool(mouse & sdl2.SDL_BUTTON(sdl2.SDL_BUTTON_LEFT))
app.middle_mouse = bool(mouse & sdl2.SDL_BUTTON(sdl2.SDL_BUTTON_MIDDLE))
app.right_mouse = bool(mouse & sdl2.SDL_BUTTON(sdl2.SDL_BUTTON_RIGHT))
mx, my = int(mx.value), int(my.value)
# if mouse hasn't moved since init, disregard SDL_GetMouseState
if self.mouse_has_moved:
app.mouse_x, app.mouse_y = mx, my
elif mx != 0 or my != 0:
self.mouse_has_moved = True
# relative mouse move state
mdx, mdy = ctypes.c_int(0), ctypes.c_int(0)
sdl2.mouse.SDL_GetRelativeMouseState(mdx, mdy)
if self.mouse_has_moved:
app.mouse_dx, app.mouse_dy = int(mdx.value), int(mdy.value)
if app.mouse_dx != 0 or app.mouse_dy != 0:
app.keyboard_editing = False
# dragging a dialog?
if app.left_mouse and self.ui.active_dialog in self.ui.hovered_elements:
self.ui.active_dialog.update_drag(app.mouse_dx, app.mouse_dy)
# get keyboard state so later we can directly query keys
ks = sdl2.SDL_GetKeyboardState(None)
# get modifier states
self.shift_pressed, self.alt_pressed, self.ctrl_pressed = False, False, False
if ks[sdl2.SDL_SCANCODE_LSHIFT] or ks[sdl2.SDL_SCANCODE_RSHIFT]:
self.shift_pressed = True
if ks[sdl2.SDL_SCANCODE_LALT] or ks[sdl2.SDL_SCANCODE_RALT]:
self.alt_pressed = True
if ks[sdl2.SDL_SCANCODE_LCTRL] or ks[sdl2.SDL_SCANCODE_RCTRL]:
self.ctrl_pressed = True
# check and remember capslock as a special mod state
# (currently only used for text tool entry)
ms = sdl2.SDL_GetModState()
self.capslock_on = bool(ms & sdl2.KMOD_CAPS)
# macOS: treat command as interchangeable with control, is this kosher?
if platform.system() == "Darwin" and (
ks[sdl2.SDL_SCANCODE_LGUI] or ks[sdl2.SDL_SCANCODE_RGUI]
):
self.ctrl_pressed = True
if app.capslock_is_ctrl and ks[sdl2.SDL_SCANCODE_CAPSLOCK]:
self.ctrl_pressed = True
# pack mods into a tuple to save listing em all out repeatedly
mods = self.shift_pressed, self.alt_pressed, self.ctrl_pressed
# get controller state
if self.gamepad:
self.gamepad_left_x = (
sdl2.SDL_JoystickGetAxis(self.gamepad, sdl2.SDL_CONTROLLER_AXIS_LEFTX)
/ 32768
)
self.gamepad_left_y = (
sdl2.SDL_JoystickGetAxis(self.gamepad, sdl2.SDL_CONTROLLER_AXIS_LEFTY)
/ -32768
)
for event in sdl2.ext.get_events():
if event.type == sdl2.SDL_QUIT:
app.should_quit = True
elif event.type == sdl2.SDL_WINDOWEVENT:
if event.window.event == sdl2.SDL_WINDOWEVENT_RESIZED:
# test window we create on init to detect resolution makes
# SDL think we've resized main app window on first tick!
if app.updates > 0:
app.resize_window(event.window.data1, event.window.data2)
elif event.type == sdl2.SDL_JOYBUTTONDOWN:
if not app.gw.paused and app.gw.player:
app.gw.player.button_pressed(event.jbutton.button)
elif event.type == sdl2.SDL_JOYBUTTONUP:
if not app.gw.paused and app.gw.player:
self.app.gw.player.button_unpressed(event.jbutton.button)
elif event.type == sdl2.SDL_KEYDOWN:
keysym = self.get_keysym(event)
# if console is up, pass input to it
if self.ui.console.visible:
self.ui.console.handle_input(keysym, *mods)
# same with dialog box
elif (
self.ui.active_dialog
and self.ui.active_dialog is self.ui.keyboard_focus_element
):
self.ui.active_dialog.handle_input(keysym, *mods)
# bail, process no further input
# sdl2.SDL_PumpEvents()
# return
# handle text input if text tool is active
elif (
self.ui.selected_tool is self.ui.text_tool
and self.ui.text_tool.input_active
):
self.ui.text_tool.handle_keyboard_input(keysym, *mods)
# see if there's a function for this bind and run it
else:
flist = self.get_bind_functions(keysym, *mods)
for f in flist:
# don't run any command whose menu bar item's dimmed / not allowed (ie wrong mode)
if self.is_command_function_allowed(f):
f()
# if game mode active, pass to world as well as any binds
if self.app.game_mode:
self.app.gw.handle_input(event, *mods)
# for key up events, use the same binds but handle them special case
# TODO: once there are enough key up events, figure out a more
# elegant way than this
elif event.type == sdl2.SDL_KEYUP:
keysym = self.get_keysym(event)
if self.app.game_mode:
self.app.gw.handle_input(event, *mods)
# dismiss selector popup
flist = self.get_bind_functions(keysym, *mods)
if not flist:
pass
elif self.ui.active_dialog:
# keyup shouldn't have any special meaning in a dialog
pass
elif self.BIND_game_grab in flist:
if (
self.app.game_mode
and not self.ui.active_dialog
and self.app.gw.player
):
self.app.gw.player.button_unpressed(0)
return
elif self.BIND_toggle_picker in flist:
# ..but only for default hold-to-show setting
if self.ui.popup_hold_to_show:
self.ui.popup.hide()
elif self.BIND_select_or_paint in flist:
app.keyboard_editing = True
if (
self.ui.selected_tool is not self.ui.text_tool
and not self.ui.text_tool.input_active
):
self.app.cursor.finish_paint()
#
# mouse events aren't handled by bind table for now
#
elif event.type == sdl2.SDL_MOUSEWHEEL:
ui_wheeled = self.ui.wheel_moved(event.wheel.y)
if not ui_wheeled:
if self.app.can_edit:
if event.wheel.y > 0:
# only zoom in should track towards cursor
app.camera.zoom(
-self.wheel_zoom_amount, towards_cursor=True
)
elif event.wheel.y < 0:
app.camera.zoom(self.wheel_zoom_amount)
else:
self.app.gw.mouse_wheeled(event.wheel.y)
elif event.type == sdl2.SDL_MOUSEBUTTONUP:
# "consume" input if UI handled it
ui_unclicked = self.ui.unclicked(event.button.button)
if ui_unclicked:
sdl2.SDL_PumpEvents()
return
if self.app.game_mode:
self.app.gw.unclicked(event.button.button)
# LMB up: finish paint for most tools, end select drag
if event.button.button == sdl2.SDL_BUTTON_LEFT:
if (
self.ui.selected_tool is self.ui.select_tool
and self.ui.select_tool.selection_in_progress
):
self.ui.select_tool.finish_select(
self.shift_pressed, self.ctrl_pressed
)
elif (
self.ui.selected_tool is not self.ui.text_tool
and not self.ui.text_tool.input_active
):
app.cursor.finish_paint()
elif event.type == sdl2.SDL_MOUSEBUTTONDOWN:
ui_clicked = self.ui.clicked(event.button.button)
# don't register edit commands if a menu is up
if (
ui_clicked
or self.ui.menu_bar.active_menu_name
or self.ui.active_dialog
):
sdl2.SDL_PumpEvents()
if self.app.game_mode:
self.app.gw.last_click_on_ui = True
return
# pass clicks through to game world
if self.app.game_mode:
if not ui_clicked:
self.app.gw.clicked(event.button.button)
# LMB down: start text entry, start select drag, or paint
elif event.button.button == sdl2.SDL_BUTTON_LEFT:
if not self.ui.active_art:
return
elif self.ui.selected_tool is self.ui.text_tool:
# text tool: only start entry if click is outside popup
if (
not self.ui.text_tool.input_active
and self.ui.popup not in self.ui.hovered_elements
):
self.ui.text_tool.start_entry()
elif self.ui.selected_tool is self.ui.select_tool:
# select tool: accept clicks if they're outside the popup
if not self.ui.select_tool.selection_in_progress and (
not self.ui.keyboard_focus_element
or (
self.ui.keyboard_focus_element is self.ui.popup
and self.ui.popup not in self.ui.hovered_elements
)
):
self.ui.select_tool.start_select()
else:
app.cursor.start_paint()
elif event.button.button == sdl2.SDL_BUTTON_RIGHT:
if self.app.ui.active_art:
self.ui.quick_grab()
# none of the below applies to cases where a dialog is up
if self.ui.active_dialog:
sdl2.SDL_PumpEvents()
return
# directly query keys we don't want affected by OS key repeat delay
# TODO: these are hard-coded for the moment, think of a good way
# to expose this functionality to the key bind system
def pressing_up(ks):
return (
ks[sdl2.SDL_SCANCODE_W]
or ks[sdl2.SDL_SCANCODE_UP]
or ks[sdl2.SDL_SCANCODE_KP_8]
)
def pressing_down(ks):
return (
ks[sdl2.SDL_SCANCODE_S]
or ks[sdl2.SDL_SCANCODE_DOWN]
or ks[sdl2.SDL_SCANCODE_KP_2]
)
def pressing_left(ks):
return (
ks[sdl2.SDL_SCANCODE_A]
or ks[sdl2.SDL_SCANCODE_LEFT]
or ks[sdl2.SDL_SCANCODE_KP_4]
)
def pressing_right(ks):
return (
ks[sdl2.SDL_SCANCODE_D]
or ks[sdl2.SDL_SCANCODE_RIGHT]
or ks[sdl2.SDL_SCANCODE_KP_6]
)
# prevent camera move if: console is up, text input is active, editing
# is not allowed
if (
self.shift_pressed
and not self.alt_pressed
and not self.ctrl_pressed
and not self.ui.console.visible
and not self.ui.text_tool.input_active
and self.app.can_edit
and self.ui.keyboard_focus_element is None
):
if pressing_up(ks):
app.camera.pan(0, 1, True)
if pressing_down(ks):
app.camera.pan(0, -1, True)
if pressing_left(ks):
app.camera.pan(-1, 0, True)
if pressing_right(ks):
app.camera.pan(1, 0, True)
if ks[sdl2.SDL_SCANCODE_X]:
app.camera.zoom(
-self.keyboard_zoom_amount, keyboard=True, towards_cursor=True
)
if ks[sdl2.SDL_SCANCODE_Z]:
app.camera.zoom(self.keyboard_zoom_amount, keyboard=True)
if (
self.app.can_edit
and app.middle_mouse
and (app.mouse_dx != 0 or app.mouse_dy != 0)
):
app.camera.mouse_pan(app.mouse_dx, app.mouse_dy)
# game mode: arrow keys and left gamepad stick move player
if (
self.app.game_mode
and not self.ui.console.visible
and not self.ui.active_dialog
and self.ui.keyboard_focus_element is None
):
if pressing_up(ks):
# shift = move selected
if self.shift_pressed and self.app.can_edit:
app.gw.move_selected(0, 1, 0)
elif not self.ctrl_pressed and app.gw.player:
app.gw.player.move(0, 1)
if pressing_down(ks):
if self.shift_pressed and self.app.can_edit:
app.gw.move_selected(0, -1, 0)
elif not self.ctrl_pressed and app.gw.player:
app.gw.player.move(0, -1)
if pressing_left(ks):
if self.shift_pressed and self.app.can_edit:
app.gw.move_selected(-1, 0, 0)
elif not self.ctrl_pressed and app.gw.player:
app.gw.player.move(-1, 0)
if pressing_right(ks):
if self.shift_pressed and self.app.can_edit:
app.gw.move_selected(1, 0, 0)
elif not self.ctrl_pressed and app.gw.player:
app.gw.player.move(1, 0)
if abs(self.gamepad_left_x) > 0.15 and app.gw.player:
app.gw.player.move(self.gamepad_left_x, 0)
if abs(self.gamepad_left_y) > 0.15 and app.gw.player:
app.gw.player.move(0, self.gamepad_left_y)
sdl2.SDL_PumpEvents()
def is_key_pressed(self, key):
"returns True if given key is pressed"
key = bytes(key, encoding="utf-8")
scancode = sdl2.keyboard.SDL_GetScancodeFromName(key)
return sdl2.SDL_GetKeyboardState(None)[scancode]
#
# bind functions
#
# function names correspond with key values in binds.cfg
def BIND_quit(self):
for art in self.app.art_loaded_for_edit:
if art.unsaved_changes and self.app.can_edit:
if self.app.game_mode:
self.app.exit_game_mode()
self.ui.set_active_art(art)
self.ui.open_dialog(QuitUnsavedChangesDialog)
return
self.app.should_quit = True
def BIND_toggle_console(self):
self.ui.console.toggle()
def BIND_import_file(self):
self.ui.open_dialog(ImportFileDialog)
def BIND_export_file(self):
self.ui.open_dialog(ExportFileDialog)
def BIND_export_file_last(self):
# if user hasn't exported this session, pick an exporter
if self.ui.app.exporter:
# redo export with appropriate filename & last options if they have
out_filename = self.ui.active_art.filename
out_filename = os.path.basename(out_filename)
out_filename = os.path.splitext(out_filename)[0]
ExportOptionsDialog.do_export(
self.ui.app, out_filename, self.ui.app.last_export_options
)
else:
self.ui.open_dialog(ExportFileDialog)
def BIND_decrease_ui_scale(self):
if self.ui.scale > SCALE_INCREMENT * 2:
self.ui.set_scale(self.ui.scale - SCALE_INCREMENT)
def BIND_increase_ui_scale(self):
# cap UI scale at 2
if self.ui.scale + SCALE_INCREMENT < 2.0:
self.ui.set_scale(self.ui.scale + SCALE_INCREMENT)
def BIND_toggle_fullscreen(self):
self.app.toggle_fullscreen()
def BIND_decrease_brush_size(self):
self.ui.selected_tool.decrease_brush_size()
self.ui.menu_bar.refresh_active_menu()
def BIND_increase_brush_size(self):
self.ui.selected_tool.increase_brush_size()
self.ui.menu_bar.refresh_active_menu()
def BIND_cycle_char_forward(self):
self.ui.select_char(self.ui.selected_char + 1)
def BIND_cycle_char_backward(self):
self.ui.select_char(self.ui.selected_char - 1)
def BIND_cycle_fg_forward(self):
self.ui.select_fg(self.ui.selected_fg_color + 1)
def BIND_cycle_fg_backward(self):
self.ui.select_fg(self.ui.selected_fg_color - 1)
def BIND_cycle_bg_forward(self):
self.ui.select_bg(self.ui.selected_bg_color + 1)
def BIND_cycle_bg_backward(self):
self.ui.select_bg(self.ui.selected_bg_color - 1)
def BIND_cycle_xform_forward(self):
self.ui.cycle_selected_xform()
def BIND_cycle_xform_backward(self):
self.ui.cycle_selected_xform(True)
def BIND_toggle_affects_char(self):
self.ui.selected_tool.toggle_affects_char()
self.ui.menu_bar.refresh_active_menu()
def BIND_toggle_affects_fg(self):
self.ui.selected_tool.toggle_affects_fg()
self.ui.menu_bar.refresh_active_menu()
def BIND_toggle_affects_bg(self):
self.ui.selected_tool.toggle_affects_bg()
self.ui.menu_bar.refresh_active_menu()
def BIND_toggle_affects_xform(self):
self.ui.selected_tool.toggle_affects_xform()
self.ui.menu_bar.refresh_active_menu()
def BIND_toggle_crt(self):
self.app.fb.toggle_crt()
self.ui.menu_bar.refresh_active_menu()
def BIND_select_pencil_tool(self):
self.ui.set_selected_tool(self.ui.pencil_tool)
def BIND_select_erase_tool(self):
self.ui.set_selected_tool(self.ui.erase_tool)
def BIND_select_rotate_tool(self):
self.ui.set_selected_tool(self.ui.rotate_tool)
def BIND_select_grab_tool(self):
self.ui.set_selected_tool(self.ui.grab_tool)
def BIND_select_text_tool(self):
self.ui.set_selected_tool(self.ui.text_tool)
def BIND_select_select_tool(self):
self.ui.set_selected_tool(self.ui.select_tool)
def BIND_select_fill_tool(self):
self.ui.set_selected_tool(self.ui.fill_tool)
def BIND_cycle_fill_boundary_mode(self):
self.ui.cycle_fill_tool_mode()
self.ui.menu_bar.refresh_active_menu()
def BIND_toggle_art_toolbar(self):
self.ui.art_toolbar.visible = not self.ui.art_toolbar.visible
self.ui.menu_bar.refresh_active_menu()
def BIND_cut_selection(self):
self.ui.cut_selection()
# switch to PasteTool
self.ui.set_selected_tool(self.ui.paste_tool)
# clear selection
self.ui.select_none()
self.ui.tool_settings_changed = True
def BIND_copy_selection(self):
self.ui.copy_selection()
# switch to PasteTool
self.ui.set_selected_tool(self.ui.paste_tool)
# clear selection
self.ui.select_none()
self.ui.tool_settings_changed = True
def BIND_select_paste_tool(self):
self.ui.set_selected_tool(self.ui.paste_tool)
def BIND_select_none(self):
if self.app.game_mode:
self.app.gw.deselect_all()
else:
self.ui.select_none()
def BIND_cancel(self):
# context-dependent:
# game mode: deselect
# normal painting mode: cancel current selection
# menu bar active: bail out of current menu
# either way: bail on image conversion if it's happening
if self.app.converter:
self.app.converter.finish(True)
if self.ui.menu_bar.active_menu_name:
self.ui.menu_bar.close_active_menu()
elif self.app.game_mode:
# bail out of list if it's active
if self.ui.keyboard_focus_element is self.ui.edit_list_panel:
self.ui.edit_list_panel.cancel()
else:
self.app.gw.deselect_all()
else:
self.ui.select_none()
def BIND_select_all(self):
self.ui.select_all()
def BIND_select_invert(self):
self.ui.invert_selection()
def BIND_edit_cfg(self):
self.ui.menu_bar.close_active_menu()
self.app.edit_cfg()
def BIND_erase_selection_or_art(self):
# if in game mode, delete selected objects
if self.app.game_mode:
# operate on a copy of selected objects list,
# as obj.destroy() removes itself from original
for obj in self.app.gw.selected_objects[:]:
# some objects can't be deleted
if obj.deleteable:
obj.destroy()
else:
self.ui.erase_selection_or_art()
def BIND_toggle_game_mode(self):
if not self.app.can_edit:
return
if not self.app.game_mode:
self.app.enter_game_mode()
else:
self.app.exit_game_mode()
self.app.update_window_title()
def BIND_new_game_dir(self):
self.ui.open_dialog(NewGameDirDialog)
def BIND_set_game_dir(self):
if self.app.can_edit:
# show available games in list panel
self.ui.edit_list_panel.set_list_operation(LO_OPEN_GAME_DIR)
def BIND_load_game_state(self):
self.ui.edit_list_panel.set_list_operation(LO_LOAD_STATE)
def BIND_save_game_state(self):
self.ui.open_dialog(SaveGameStateDialog)
def BIND_reset_game(self):
if self.app.game_mode and self.app.gw.game_dir:
self.app.gw.reset_game()
def BIND_toggle_picker(self):
if not self.ui.active_art:
return
if self.ui.popup_hold_to_show:
self.ui.popup.show()
else:
self.ui.popup.toggle()
def BIND_toggle_picker_hold(self):
self.ui.popup_hold_to_show = not self.ui.popup_hold_to_show
self.ui.menu_bar.refresh_active_menu()
def BIND_swap_fg_bg_colors(self):
if self.ui.active_art:
self.ui.swap_fg_bg_colors()
def BIND_save_current(self):
# save current game state in game mode, else save current art
if self.app.game_mode and self.app.gw.game_dir:
# as with reset, save over last loaded state
self.app.gw.save_last_state()
elif self.ui.active_art:
# if new document, ask for a name
default_name = ART_DIR + "new." + ART_FILE_EXTENSION
if self.ui.active_art.filename == default_name:
self.ui.open_dialog(SaveAsDialog)
else:
self.ui.active_art.save_to_file()
def BIND_toggle_ui_visibility(self):
self.ui.visible = not self.ui.visible
def BIND_toggle_grid_visibility(self):
self.app.grid.visible = not self.app.grid.visible
self.ui.menu_bar.refresh_active_menu()
def BIND_toggle_bg_texture(self):
self.app.show_bg_texture = not self.app.show_bg_texture
self.ui.menu_bar.refresh_active_menu()
def BIND_previous_frame(self):
self.ui.set_active_frame(self.ui.active_art.active_frame - 1)
def BIND_next_frame(self):
self.ui.set_active_frame(self.ui.active_art.active_frame + 1)
def BIND_toggle_anim_playback(self):
# if game mode, pause/unpause
if self.app.game_mode:
self.toggle_pause()
return
for r in self.ui.active_art.renderables:
if r.animating:
r.stop_animating()
else:
r.start_animating()
self.ui.menu_bar.refresh_active_menu()
def toggle_pause(self):
self.app.gw.toggle_pause()
self.ui.menu_bar.refresh_active_menu()
def BIND_previous_layer(self):
self.ui.set_active_layer(self.ui.active_art.active_layer - 1)
self.ui.menu_bar.refresh_active_menu()
def BIND_next_layer(self):
self.ui.set_active_layer(self.ui.active_art.active_layer + 1)
self.ui.menu_bar.refresh_active_menu()
def BIND_previous_art(self):
self.ui.previous_active_art()
self.ui.menu_bar.refresh_active_menu()
def BIND_next_art(self):
if len(self.app.art_loaded_for_edit) == 0:
return
self.ui.next_active_art()
self.ui.menu_bar.refresh_active_menu()
def BIND_undo(self):
self.ui.undo()
def BIND_redo(self):
self.ui.redo()
def BIND_quick_grab(self):
if not self.ui.active_art:
return
self.app.keyboard_editing = True
self.ui.quick_grab()
def BIND_set_camera_zoom(self):
self.ui.open_dialog(SetCameraZoomDialog)
def BIND_camera_zoom_in_proportional(self):
self.app.camera.zoom_proportional(1)
def BIND_camera_zoom_out_proportional(self):
self.app.camera.zoom_proportional(-1)
def BIND_toggle_zoom_extents(self):
self.app.camera.toggle_zoom_extents()
self.ui.menu_bar.refresh_active_menu()
def BIND_toggle_camera_tilt(self):
if self.app.camera.y_tilt == 2:
self.app.camera.y_tilt = 0
self.ui.message_line.post_line("Camera tilt disengaged.")
else:
self.app.camera.y_tilt = 2
self.ui.message_line.post_line("Camera tilt engaged.")
self.ui.menu_bar.refresh_active_menu()
def BIND_select_overlay_image(self):
self.ui.open_dialog(OverlayImageFileChooserDialog)
def BIND_toggle_overlay_image(self):
self.app.draw_overlay = not self.app.draw_overlay
self.ui.menu_bar.refresh_active_menu()
def BIND_set_overlay_image_opacity(self):
self.ui.open_dialog(OverlayImageOpacityDialog)
def BIND_set_overlay_image_scaling(self):
if self.app.overlay_scale_type == OIS_WIDTH:
self.app.overlay_scale_type = OIS_HEIGHT
elif self.app.overlay_scale_type == OIS_HEIGHT:
self.app.overlay_scale_type = OIS_FILL
elif self.app.overlay_scale_type == OIS_FILL:
self.app.overlay_scale_type = OIS_WIDTH
self.ui.size_and_position_overlay_image()
self.ui.menu_bar.refresh_active_menu()
def BIND_add_to_list_selection(self):
if not self.ui.edit_list_panel.is_visible():
return
self.ui.edit_list_panel.keyboard_select_item()
def BIND_remove_from_list_selection(self):
if not self.ui.edit_list_panel.is_visible():
return
self.ui.edit_list_panel.keyboard_select_item()
def BIND_select_or_paint(self):
if self.ui.keyboard_focus_element:
# save current focus element because kb_select_item might change it!
selected_element = self.ui.keyboard_focus_element
# get button pressed in case we need its item
button = self.ui.keyboard_focus_element.keyboard_select_item()
if selected_element is self.ui.pulldown:
# mirror behavior from MenuItemButton.click: close on select if needed
if button and button.item.close_on_select:
self.ui.menu_bar.close_active_menu()
return
if not self.ui.active_art:
return
elif (
self.ui.selected_tool is self.ui.text_tool
and not self.ui.text_tool.input_active
):
self.ui.text_tool.start_entry()
elif self.ui.selected_tool is self.ui.select_tool:
if self.ui.select_tool.selection_in_progress:
# pass in shift/alt for add/subtract
self.ui.select_tool.finish_select(self.shift_pressed, self.ctrl_pressed)
else:
self.ui.select_tool.start_select()
else:
self.app.cursor.start_paint()
def BIND_screenshot(self):
self.app.screenshot()
def BIND_run_test_mutate(self):
if self.ui.active_art.is_script_running("conway"):
self.ui.active_art.stop_script("conway")
else:
self.ui.active_art.run_script_every("conway", 0.05)
def BIND_arrow_up(self):
if self.ui.keyboard_focus_element:
self.ui.keyboard_navigate(0, -1)
else:
self.app.cursor.keyboard_move(0, 1)
def BIND_arrow_down(self):
if self.ui.keyboard_focus_element:
self.ui.keyboard_navigate(0, 1)
else:
self.app.cursor.keyboard_move(0, -1)
def BIND_arrow_left(self):
# navigate popup, menu bar etc
if self.ui.keyboard_focus_element:
self.ui.keyboard_navigate(-1, 0)
else:
self.app.cursor.keyboard_move(-1, 0)
def BIND_arrow_right(self):
if self.ui.keyboard_focus_element:
self.ui.keyboard_navigate(1, 0)
else:
self.app.cursor.keyboard_move(1, 0)
def BIND_cycle_inactive_layer_visibility(self):
if not self.ui.active_art:
return
if self.ui.active_art.layers == 1:
return
message_text = "Non-active layers: "
if self.app.inactive_layer_visibility == LAYER_VIS_FULL:
self.app.inactive_layer_visibility = LAYER_VIS_DIM
message_text += "dim"
elif self.app.inactive_layer_visibility == LAYER_VIS_DIM:
self.app.inactive_layer_visibility = LAYER_VIS_NONE
message_text += "invisible"
else:
self.app.inactive_layer_visibility = LAYER_VIS_FULL
message_text += "visible"
self.ui.message_line.post_line(message_text)
self.ui.menu_bar.refresh_active_menu()
def BIND_open_file_menu(self):
self.ui.menu_bar.open_menu_by_name("file")
def BIND_open_edit_menu(self):
self.ui.menu_bar.open_menu_by_name("edit")
def BIND_open_tool_menu(self):
self.ui.menu_bar.open_menu_by_name("tool")
def BIND_open_view_menu(self):
self.ui.menu_bar.open_menu_by_name("view")
def BIND_open_art_menu(self):
self.ui.menu_bar.open_menu_by_name("art")
def BIND_open_frame_menu(self):
if self.app.game_mode:
self.ui.menu_bar.open_menu_by_name("room")
else:
self.ui.menu_bar.open_menu_by_name("frame")
def BIND_open_layer_menu(self):
self.ui.menu_bar.open_menu_by_name("layer")
def BIND_open_char_color_menu(self):
self.ui.menu_bar.open_menu_by_name("char_color")
def BIND_open_help_menu(self):
self.ui.menu_bar.open_menu_by_name("help")
def BIND_open_game_menu(self):
self.ui.menu_bar.open_menu_by_name("game")
def BIND_open_state_menu(self):
self.ui.menu_bar.open_menu_by_name("state")
def BIND_open_world_menu(self):
self.ui.menu_bar.open_menu_by_name("world")
def BIND_open_object_menu(self):
self.ui.menu_bar.open_menu_by_name("object")
def BIND_new_art(self):
self.ui.open_dialog(NewArtDialog)
def BIND_open_art(self):
self.ui.open_dialog(ArtChooserDialog)
def BIND_save_art_as(self):
if self.app.game_mode:
self.ui.open_dialog(SaveGameStateDialog)
elif not self.ui.active_art:
return
else:
self.ui.open_dialog(SaveAsDialog)
def BIND_revert_art(self):
if not self.ui.active_art:
return
if self.ui.active_art.unsaved_changes:
self.ui.open_dialog(RevertChangesDialog)
def BIND_close_art(self):
if not self.ui.active_art:
return
if self.ui.active_art.unsaved_changes:
self.ui.open_dialog(CloseUnsavedChangesDialog)
return
self.app.close_art(self.ui.active_art)
# dismiss popup if no more arts are open
if self.ui.popup.visible and len(self.app.art_loaded_for_edit) == 0:
self.ui.popup.hide()
def BIND_open_help_docs(self):
self.app.open_help_docs()
def BIND_generate_docs(self):
self.app.generate_docs()
def BIND_open_website(self):
self.app.open_website()
def BIND_crop_to_selection(self):
self.ui.crop_to_selection(self.ui.active_art)
def BIND_resize_art(self):
self.ui.open_dialog(ResizeArtDialog)
def BIND_art_flip_horizontal(self):
self.ui.active_art.flip_horizontal(
self.ui.active_art.active_frame, self.ui.active_art.active_layer
)
def BIND_art_flip_vertical(self):
self.ui.active_art.flip_vertical(
self.ui.active_art.active_frame, self.ui.active_art.active_layer
)
def BIND_art_toggle_flip_affects_xforms(self):
self.ui.flip_affects_xforms = not self.ui.flip_affects_xforms
self.ui.menu_bar.refresh_active_menu()
def BIND_run_art_script(self):
self.ui.open_dialog(RunArtScriptDialog)
def BIND_run_art_script_last(self):
# if user hasn't run a script this session, pick one
if not self.ui.app.last_art_script:
self.BIND_run_art_script()
else:
self.ui.active_art.run_script(self.ui.app.last_art_script, log=False)
def BIND_art_switch_to(self, art_filename):
self.ui.set_active_art_by_filename(art_filename)
self.ui.menu_bar.refresh_active_menu()
def BIND_add_frame(self):
self.ui.open_dialog(AddFrameDialog)
def BIND_duplicate_frame(self):
self.ui.open_dialog(DuplicateFrameDialog)
def BIND_change_frame_delay(self):
self.ui.open_dialog(FrameDelayDialog)
def BIND_change_frame_delay_all(self):
self.ui.open_dialog(FrameDelayAllDialog)
def BIND_delete_frame(self):
self.ui.active_art.delete_frame_at(self.ui.active_art.active_frame)
# if we're now down to 1 frame, refresh to dim this item!
# FIXME: this doesn't dim it - why? - but it is unselectable
self.ui.menu_bar.refresh_active_menu()
def BIND_change_frame_index(self):
self.ui.open_dialog(FrameIndexDialog)
def BIND_add_layer(self):
self.ui.open_dialog(AddLayerDialog)
def BIND_duplicate_layer(self):
self.ui.open_dialog(DuplicateLayerDialog)
def BIND_layer_switch_to(self, layer_number):
self.ui.set_active_layer(layer_number)
self.ui.menu_bar.refresh_active_menu()
def BIND_change_layer_name(self):
self.ui.open_dialog(SetLayerNameDialog)
def BIND_change_layer_z(self):
self.ui.open_dialog(SetLayerZDialog)
def BIND_toggle_layer_visibility(self):
art = self.ui.active_art
is_visible = art.layers_visibility[art.active_layer]
art.layers_visibility[art.active_layer] = not is_visible
art.set_unsaved_changes(True)
self.ui.menu_bar.refresh_active_menu()
def BIND_toggle_hidden_layers_visible(self):
self.app.show_hidden_layers = not self.app.show_hidden_layers
self.ui.menu_bar.refresh_active_menu()
def BIND_delete_layer(self):
self.ui.active_art.delete_layer(self.ui.active_art.active_layer)
self.ui.menu_bar.refresh_active_menu()
def BIND_choose_charset(self):
self.ui.open_dialog(CharSetChooserDialog)
def BIND_choose_palette(self):
self.ui.open_dialog(PaletteChooserDialog)
def BIND_palette_from_file(self):
self.ui.open_dialog(PaletteFromImageChooserDialog)
def BIND_toggle_onion_visibility(self):
self.app.onion_frames_visible = not self.app.onion_frames_visible
if self.app.onion_frames_visible:
self.ui.reset_onion_frames()
self.ui.menu_bar.refresh_active_menu()
def BIND_cycle_onion_frames(self):
self.app.onion_show_frames += 1
self.app.onion_show_frames %= self.app.max_onion_frames + 1
# start cycle at 1, not 0
self.app.onion_show_frames = max(1, self.app.onion_show_frames)
self.ui.menu_bar.refresh_active_menu()
def BIND_cycle_onion_ahead_behind(self):
# cycle between next, previous, next & previous
if self.app.onion_show_frames_behind and self.app.onion_show_frames_ahead:
self.app.onion_show_frames_behind = False
elif not self.app.onion_show_frames_behind and self.app.onion_show_frames_ahead:
self.app.onion_show_frames_behind = True
self.app.onion_show_frames_ahead = False
else:
self.app.onion_show_frames_ahead = True
self.app.onion_show_frames_ahead = True
self.ui.menu_bar.refresh_active_menu()
def BIND_toggle_debug_text(self):
self.ui.debug_text.visible = not self.ui.debug_text.visible
def BIND_toggle_fps_counter(self):
self.ui.fps_counter.visible = not self.ui.fps_counter.visible
def BIND_open_all_game_assets(self):
for game_obj in self.app.gw.objects.values():
for art_filename in game_obj.get_all_art():
self.app.load_art_for_edit(art_filename)
# open all hud assets too
for art in self.app.gw.hud.arts:
self.app.load_art_for_edit(art.filename)
self.ui.menu_bar.refresh_active_menu()
def BIND_toggle_all_collision_viz(self):
self.app.gw.toggle_all_collision_viz()
self.ui.menu_bar.refresh_active_menu()
def BIND_toggle_all_bounds_viz(self):
self.app.gw.toggle_all_bounds_viz()
self.ui.menu_bar.refresh_active_menu()
def BIND_toggle_all_origin_viz(self):
self.app.gw.toggle_all_origin_viz()
self.ui.menu_bar.refresh_active_menu()
def BIND_toggle_collision_on_selected(self):
for obj in self.app.gw.selected_objects:
if obj.orig_collision_type and obj.collision_type == CT_NONE:
obj.enable_collision()
self.ui.message_line.post_line(
"Collision enabled for {}".format(obj.name)
)
elif obj.collision_type != CT_NONE:
obj.disable_collision()
self.ui.message_line.post_line(
"Collision disabled for {}".format(obj.name)
)
def BIND_toggle_game_edit_ui(self):
self.ui.toggle_game_edit_ui()
#
# game mode binds
#
def accept_normal_game_input(self):
return (
self.app.game_mode
and self.app.gw.player
and not self.ui.active_dialog
and not self.ui.pulldown.visible
)
# TODO: generalize these two somehow
def BIND_game_frob(self):
if self.accept_normal_game_input():
self.app.gw.player.button_pressed(1)
def BIND_game_grab(self):
if self.accept_normal_game_input():
self.app.gw.player.button_pressed(0)
def BIND_center_cursor_in_art(self):
self.app.cursor.center_in_art()
def BIND_choose_spawn_object_class(self):
if self.app.game_mode and self.app.gw.game_dir:
self.ui.edit_list_panel.set_list_operation(LO_SET_SPAWN_CLASS)
def BIND_duplicate_selected_objects(self):
self.app.gw.duplicate_selected_objects()
def BIND_select_objects(self):
if self.app.game_mode and self.app.gw.game_dir:
self.ui.edit_list_panel.set_list_operation(LO_SELECT_OBJECTS)
def BIND_edit_art_for_selected_objects(self):
self.app.gw.edit_art_for_selected()
def BIND_edit_world_properties(self):
self.app.gw.deselect_all()
self.app.gw.select_object(self.app.gw.properties, force=True)
def BIND_change_current_room(self):
self.ui.edit_list_panel.set_list_operation(LO_SET_ROOM)
def BIND_change_current_room_to(self, new_room_name):
self.app.gw.change_room(new_room_name)
self.ui.menu_bar.refresh_active_menu()
def BIND_add_room(self):
self.ui.open_dialog(AddRoomDialog)
def BIND_remove_current_room(self):
self.app.gw.remove_room(self.app.gw.current_room.name)
def BIND_set_room_objects(self):
self.ui.edit_list_panel.set_list_operation(LO_SET_ROOM_OBJECTS)
def BIND_set_object_rooms(self):
self.ui.edit_list_panel.set_list_operation(LO_SET_OBJECT_ROOMS)
def BIND_toggle_all_rooms_visible(self):
self.app.gw.show_all_rooms = not self.app.gw.show_all_rooms
self.ui.menu_bar.refresh_active_menu()
def BIND_toggle_room_camera_changes(self):
self.app.gw.properties.set_object_property(
"room_camera_changes_enabled", not self.app.gw.room_camera_changes_enabled
)
self.ui.menu_bar.refresh_active_menu()
def BIND_set_room_camera_marker(self):
self.ui.open_dialog(SetRoomCamDialog)
self.ui.edit_list_panel.set_list_operation(LO_SET_ROOM_CAMERA)
def BIND_objects_to_camera(self):
cam = self.app.gw.camera
for obj in self.app.gw.selected_objects:
obj.set_loc(cam.x, cam.y, cam.z)
def BIND_camera_to_objects(self):
if len(self.app.gw.selected_objects) == 0:
return
obj = self.app.gw.selected_objects[0]
self.app.gw.camera.set_loc_from_obj(obj)
def BIND_add_selected_to_room(self):
if not self.app.gw.current_room:
return
for obj in self.app.gw.selected_objects:
self.app.gw.current_room.add_object(obj)
def BIND_remove_selected_from_room(self):
if not self.app.gw.current_room:
return
for obj in self.app.gw.selected_objects:
self.app.gw.current_room.remove_object(obj)
def BIND_switch_edit_panel_focus(self):
self.ui.switch_edit_panel_focus()
def BIND_switch_edit_panel_focus_reverse(self):
self.ui.switch_edit_panel_focus(reverse=True)
def BIND_set_room_edge_warps(self):
# bring up dialog before setting list so list knows about it
self.ui.open_dialog(SetRoomEdgeWarpsDialog)
self.ui.edit_list_panel.set_list_operation(LO_SET_ROOM_EDGE_WARP)
def BIND_set_room_bounds_obj(self):
self.ui.open_dialog(SetRoomBoundsObjDialog)
self.ui.edit_list_panel.set_list_operation(LO_SET_ROOM_EDGE_OBJ)
def BIND_toggle_list_only_room_objects(self):
self.app.gw.list_only_current_room_objects = (
not self.app.gw.list_only_current_room_objects
)
self.ui.menu_bar.refresh_active_menu()
def BIND_rename_current_room(self):
self.ui.open_dialog(RenameRoomDialog)
def BIND_toggle_debug_objects(self):
if not self.app.gw.properties:
return
self.app.gw.properties.set_object_property(
"draw_debug_objects", not self.app.gw.draw_debug_objects
)
self.ui.menu_bar.refresh_active_menu()