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(f"Loaded key binds from {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(f"Created new key binds file {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 = [f"BIND_{bind_data}"] else: bind_fnames = [f"BIND_{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( f"SDL2: Couldn't initialize joystick subsystem, code {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( f"Gamepad found: {pad_name} with {pad_axes} axes, {pad_buttons} 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__ == f"BIND_{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(f"Collision enabled for {obj.name}") elif obj.collision_type != CT_NONE: obj.disable_collision() self.ui.message_line.post_line(f"Collision disabled for {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()