def _setup_difficulties_menu(self, game: RandovaniaGame, menu: QtWidgets.QMenu): from randovania.game_description import default_database game = default_database.game_description_for(game) tricks_in_use = used_tricks(game) menu.clear() for trick in sorted(game.resource_database.trick, key=lambda _trick: _trick.long_name): if trick not in tricks_in_use: continue trick_menu = QtWidgets.QMenu(self) trick_menu.setTitle(trick.long_name) menu.addAction(trick_menu.menuAction()) used_difficulties = difficulties_for_trick(game, trick) for i, trick_level in enumerate(iterate_enum(LayoutTrickLevel)): if trick_level in used_difficulties: difficulty_action = QtWidgets.QAction(self) difficulty_action.setText(trick_level.long_name) trick_menu.addAction(difficulty_action) difficulty_action.triggered.connect( functools.partial(self._open_trick_details_popup, game, trick, trick_level))
def buildMenuFromTree(cls, tree: Tree, parentMenu: QtWidgets.QMenu = None): """returns qmenu corresponding to tree""" parentMenu = parentMenu or QtWidgets.QMenu(title=tree.name) parentMenu.clear() for i in tree.branches: added = None if i.branches: added = parentMenu.addMenu(TreeMenu.buildMenuFromTree(i)) elif isinstance(i.value, PartialAction): i.value.setParent(parentMenu) added = parentMenu.addAction(i.value) elif callable(i.value): added = parentMenu.addAction(i.name) added.triggered.connect(i.value) # check for list of multiple actions elif isinstance(i.value, (list, tuple)): added = parentMenu.addAction(i.name) for slot in i.value: added.triggered.connect(slot) if added is None: #tree.display() raise RuntimeError( "no action value found for branch {}".format(i)) if i.description: added.setToolTip(i.description) return parentMenu
def testMenu(self): self._actionDestroyed = False w = QWidget() menu = QMenu(w) act = menu.addAction("MENU") _ref = weakref.ref(act, self.actionDestroyed) act = None self.assertFalse(self._actionDestroyed) menu.clear() self.assertTrue(self._actionDestroyed)
class TrayIcon(QSystemTrayIcon): on_settings = Signal() host: "Host" def __init__(self, host: "Host"): global _tray_icon super().__init__(QIcon(":/runekit/ui/trayicon.png")) _tray_icon = self self.host = host self._setup_menu() self.setContextMenu(self.menu) def _setup_menu(self): if not hasattr(self, "menu"): self.menu = QMenu("RuneKit") self.menu.clear() self._setup_app_menu("", self.menu) self.menu.addSeparator() self.menu_settings = self.menu.addAction("Settings") self.menu_settings.triggered.connect(self.on_settings) self.menu.addAction( QIcon.fromTheme("application-exit"), "Exit", lambda: QCoreApplication.instance().quit(), ) def _setup_app_menu(self, path: str, menu: QMenu): for app_id, manifest in self.host.app_store.list_app(path): if manifest is None: # Folder submenu = menu.addMenu(QIcon.fromTheme("folder"), app_id) self._setup_app_menu(app_id, submenu) continue app_menu = menu.addAction(manifest["appName"]) app_menu.triggered.connect( lambda _=None, app_id=app_id: self.host.launch_app_id(app_id) ) icon = self.host.app_store.icon(app_id) if icon: app_menu.setIcon(icon) @Slot() def update_menu(self): self._setup_menu()
class DebugControlsWidget(QToolBar): def __init__(self, parent, name, data, debug_state): if not type(data) == binaryninja.binaryview.BinaryView: raise Exception('expected widget data to be a BinaryView') self.bv = data self.debug_state = debug_state QToolBar.__init__(self, parent) # TODO: Is there a cleaner way to do this? self.setStyleSheet(""" QToolButton{padding: 4px 14px 4px 14px; font-size: 14pt;} QToolButton:disabled{color: palette(alternate-base)} """) self.actionRun = QAction("Run", self) self.actionRun.triggered.connect(lambda: self.perform_run()) self.actionRestart = QAction("Restart", self) self.actionRestart.triggered.connect(lambda: self.perform_restart()) self.actionQuit = QAction("Quit", self) self.actionQuit.triggered.connect(lambda: self.perform_quit()) self.actionAttach = QAction("Attach... (todo)", self) self.actionAttach.triggered.connect(lambda: self.perform_attach()) self.actionDetach = QAction("Detach", self) self.actionDetach.triggered.connect(lambda: self.perform_detach()) self.actionSettings = QAction("Settings...", self) self.actionSettings.triggered.connect(lambda: self.perform_settings()) self.actionPause = QAction("Pause", self) self.actionPause.triggered.connect(lambda: self.perform_pause()) self.actionResume = QAction("Resume", self) self.actionResume.triggered.connect(lambda: self.perform_resume()) self.actionStepInto = QAction("Step Into", self) self.actionStepInto.triggered.connect(lambda: self.perform_step_into()) self.actionStepOver = QAction("Step Over", self) self.actionStepOver.triggered.connect(lambda: self.perform_step_over()) self.actionStepReturn = QAction("Step Return", self) self.actionStepReturn.triggered.connect( lambda: self.perform_step_return()) # session control menu self.controlMenu = QMenu("Process Control", self) self.controlMenu.addAction(self.actionRun) self.controlMenu.addAction(self.actionRestart) self.controlMenu.addAction(self.actionQuit) self.controlMenu.addSeparator() # TODO: Attach to running process # self.controlMenu.addAction(self.actionAttach) self.controlMenu.addAction(self.actionDetach) # TODO: Switch adapter/etc (could go in regular settings) self.controlMenu.addSeparator() self.controlMenu.addAction(self.actionSettings) self.btnControl = QToolButton(self) self.btnControl.setMenu(self.controlMenu) self.btnControl.setPopupMode(QToolButton.MenuButtonPopup) self.btnControl.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.btnControl.setDefaultAction(self.actionRun) self.addWidget(self.btnControl) # execution control buttons self.addAction(self.actionPause) self.addAction(self.actionResume) self.addAction(self.actionStepInto) self.addAction(self.actionStepOver) # TODO: Step until returning from current function self.addAction(self.actionStepReturn) self.threadMenu = QMenu("Threads", self) self.btnThreads = QToolButton(self) self.btnThreads.setMenu(self.threadMenu) self.btnThreads.setPopupMode(QToolButton.InstantPopup) self.btnThreads.setToolButtonStyle(Qt.ToolButtonTextOnly) self.addWidget(self.btnThreads) self.set_thread_list([]) self.editStatus = QLineEdit('INACTIVE', self) self.editStatus.setReadOnly(True) self.editStatus.setAlignment(QtCore.Qt.AlignCenter) self.addWidget(self.editStatus) # disable buttons self.set_actions_enabled(Run=True, Restart=False, Quit=False, Attach=True, Detach=False, Pause=False, Resume=False, StepInto=False, StepOver=False, StepReturn=False) self.set_resume_pause_action("Pause") def __del__(self): # TODO: Move this elsewhere # This widget is tasked with cleaning up the state after the view is closed # binjaplug.delete_state(self.bv) pass def perform_run(self): self.debug_state.run() self.state_stopped() self.debug_state.ui.context_display() def perform_restart(self): self.debug_state.restart() self.state_stopped() def perform_quit(self): self.debug_state.quit() self.state_inactive() def perform_attach(self): # TODO: Show dialog to select adapter/address/process pass def perform_detach(self): self.debug_state.detach() self.state_inactive() def perform_settings(self): dialog = AdapterSettingsDialog.AdapterSettingsDialog(self, self.bv) dialog.show() def perform_pause(self): self.debug_state.pause() def perform_resume(self): def perform_resume_thread(): (reason, data) = self.debug_state.go() execute_on_main_thread_and_wait( lambda: self.handle_stop_return(reason, data)) execute_on_main_thread_and_wait( lambda: self.debug_state.ui.context_display()) self.state_running() threading.Thread(target=perform_resume_thread).start() def perform_step_into(self): def perform_step_into_thread(): (reason, data) = self.debug_state.step_into() execute_on_main_thread_and_wait( lambda: self.handle_stop_return(reason, data)) execute_on_main_thread_and_wait( lambda: self.debug_state.ui.context_display()) self.state_busy("STEPPING") threading.Thread(target=perform_step_into_thread).start() def perform_step_over(self): def perform_step_over_thread(): (reason, data) = self.debug_state.step_over() execute_on_main_thread_and_wait( lambda: self.handle_stop_return(reason, data)) execute_on_main_thread_and_wait( lambda: self.debug_state.ui.context_display()) self.state_busy("STEPPING") threading.Thread(target=perform_step_over_thread).start() def perform_step_return(self): def perform_step_return_thread(): (reason, data) = self.debug_state.step_return() execute_on_main_thread_and_wait( lambda: self.handle_stop_return(reason, data)) execute_on_main_thread_and_wait( lambda: self.debug_state.ui.context_display()) self.state_busy("STEPPING") threading.Thread(target=perform_step_return_thread).start() def set_actions_enabled(self, **kwargs): def enable_starting(e): self.actionRun.setEnabled(e) self.actionAttach.setEnabled(e) def enable_stopping(e): self.actionRestart.setEnabled(e) self.actionQuit.setEnabled(e) self.actionDetach.setEnabled(e) def enable_stepping(e): self.actionStepInto.setEnabled(e) self.actionStepOver.setEnabled(e) self.actionStepReturn.setEnabled(e) actions = { "Run": lambda e: self.actionRun.setEnabled(e), "Restart": lambda e: self.actionRestart.setEnabled(e), "Quit": lambda e: self.actionQuit.setEnabled(e), "Attach": lambda e: self.actionAttach.setEnabled(e), "Detach": lambda e: self.actionDetach.setEnabled(e), "Pause": lambda e: self.actionPause.setEnabled(e), "Resume": lambda e: self.actionResume.setEnabled(e), "StepInto": lambda e: self.actionStepInto.setEnabled(e), "StepOver": lambda e: self.actionStepOver.setEnabled(e), "StepReturn": lambda e: self.actionStepReturn.setEnabled(e), "Threads": lambda e: self.btnThreads.setEnabled(e), "Starting": enable_starting, "Stopping": enable_stopping, "Stepping": enable_stepping, } for (action, enabled) in kwargs.items(): actions[action](enabled) def set_default_process_action(self, action): actions = { "Run": self.actionRun, "Restart": self.actionRestart, "Quit": self.actionQuit, "Attach": self.actionAttach, "Detach": self.actionDetach, } self.btnControl.setDefaultAction(actions[action]) def set_resume_pause_action(self, action): self.actionResume.setVisible(action == "Resume") self.actionPause.setVisible(action == "Pause") def set_thread_list(self, threads): def select_thread_fn(tid): def select_thread(tid): stateObj = binjaplug.get_state(self.bv) if stateObj.state == 'STOPPED': adapter = stateObj.adapter adapter.thread_select(tid) self.debug_state.ui.context_display() else: print('cannot set thread in state %s' % stateObj.state) return lambda: select_thread(tid) self.threadMenu.clear() if len(threads) > 0: for thread in threads: item_name = "Thread {} at {}".format(thread['tid'], hex(thread['rip'])) action = self.threadMenu.addAction( item_name, select_thread_fn(thread['tid'])) if thread['selected']: self.btnThreads.setDefaultAction(action) else: defaultThreadAction = self.threadMenu.addAction("Thread List") defaultThreadAction.setEnabled(False) self.btnThreads.setDefaultAction(defaultThreadAction) def state_inactive(self, msg=None): debug_state = binjaplug.get_state(self.bv) # clear breakpoints debug_state.breakpoint_tag_del() debug_state.breakpoints = {} debug_state.state = 'INACTIVE' self.editStatus.setText(msg or debug_state.state) self.set_actions_enabled(Starting=True, Stopping=False, Stepping=False, Pause=False, Resume=False, Threads=False) self.set_default_process_action("Run") self.set_thread_list([]) self.set_resume_pause_action("Pause") def state_stopped(self, msg=None): debug_state = binjaplug.get_state(self.bv) debug_state.state = 'STOPPED' self.editStatus.setText(msg or debug_state.state) self.set_actions_enabled(Starting=False, Stopping=True, Stepping=True, Pause=True, Resume=True, Threads=True) self.set_default_process_action("Quit") self.set_resume_pause_action("Resume") def state_stopped_extern(self, msg=None): debug_state = binjaplug.get_state(self.bv) debug_state.state = 'STOPPED (outside view)' self.editStatus.setText(msg or debug_state.state) self.set_actions_enabled(Starting=False, Stopping=True, Stepping=True, StepReturn=False, Pause=True, Resume=True, Threads=True) self.set_default_process_action("Quit") self.set_resume_pause_action("Resume") def state_running(self, msg=None): debug_state = binjaplug.get_state(self.bv) debug_state.state = 'RUNNING' self.editStatus.setText(msg or debug_state.state) self.set_actions_enabled(Starting=False, Stopping=True, Stepping=False, Pause=True, Resume=False, Threads=False) self.set_default_process_action("Quit") self.set_resume_pause_action("Pause") def state_busy(self, msg=None): debug_state = binjaplug.get_state(self.bv) debug_state.state = 'RUNNING' self.editStatus.setText(msg or debug_state.state) self.set_actions_enabled(Starting=False, Stopping=True, Stepping=False, Pause=True, Resume=False, Threads=False) self.set_default_process_action("Quit") self.set_resume_pause_action("Pause") def state_error(self, msg=None): debug_state = binjaplug.get_state(self.bv) debug_state.state = 'ERROR' self.editStatus.setText(msg or debug_state.state) self.set_actions_enabled(Run=True, Restart=True, Quit=True, Attach=True, Detach=True, Pause=True, Resume=True, StepInto=True, StepOver=True, StepReturn=True, Threads=True) self.set_default_process_action("Run") self.set_thread_list([]) self.set_resume_pause_action("Resume") def handle_stop_return(self, reason, data): if reason == DebugAdapter.STOP_REASON.STDOUT_MESSAGE: self.state_stopped('stdout: ' + data) elif reason == DebugAdapter.STOP_REASON.PROCESS_EXITED: self.debug_state.quit() self.state_inactive('process exited, return code=%d' % data) elif reason == DebugAdapter.STOP_REASON.BACKEND_DISCONNECTED: self.debug_state.quit() self.state_inactive('backend disconnected (process exited?)')
class DebugControlsWidget(QToolBar): def __init__(self, parent, name, data, debug_state): if not type(data) == BinaryView: raise Exception('expected widget data to be a BinaryView') self.bv = data self.debug_state = debug_state QToolBar.__init__(self, parent) # TODO: Is there a cleaner way to do this? self.setStyleSheet(""" QToolButton{padding: 4px 14px 4px 14px; font-size: 14pt;} QToolButton:disabled{color: palette(alternate-base)} """) self.actionRun = QAction("Run", self) self.actionRun.triggered.connect(lambda: self.perform_run()) self.actionRun.setIcon(load_icon('run.svg')) self.actionRestart = QAction("Restart", self) self.actionRestart.triggered.connect(lambda: self.perform_restart()) self.actionRestart.setIcon(load_icon('restart.svg')) self.actionQuit = QAction("Quit", self) self.actionQuit.triggered.connect(lambda: self.perform_quit()) self.actionQuit.setIcon(load_icon('cancel.svg')) self.actionAttach = QAction("Attach", self) self.actionAttach.triggered.connect(lambda: self.perform_attach()) self.actionAttach.setIcon(load_icon('connect.svg')) self.actionDetach = QAction("Detach", self) self.actionDetach.triggered.connect(lambda: self.perform_detach()) self.actionDetach.setIcon(load_icon('disconnect.svg')) self.actionSettings = QAction("Settings...", self) self.actionSettings.triggered.connect(lambda: self.perform_settings()) self.actionPause = QAction("Pause", self) self.actionPause.triggered.connect(lambda: self.perform_pause()) self.actionPause.setIcon(load_icon('pause.svg')) self.actionResume = QAction("Resume", self) self.actionResume.triggered.connect(lambda: self.perform_resume()) self.actionResume.setIcon(load_icon('resume.svg')) self.actionStepIntoAsm = QAction("Step Into (Assembly)", self) self.actionStepIntoAsm.triggered.connect( lambda: self.perform_step_into_asm()) self.actionStepIntoAsm.setIcon(load_icon('stepinto.svg')) self.actionStepIntoIL = QAction("Step Into", self) self.actionStepIntoIL.triggered.connect( lambda: self.perform_step_into_il()) self.actionStepIntoIL.setIcon(load_icon('stepinto.svg')) self.actionStepOverAsm = QAction("Step Over (Assembly)", self) self.actionStepOverAsm.triggered.connect( lambda: self.perform_step_over_asm()) self.actionStepOverAsm.setIcon(load_icon('stepover.svg')) self.actionStepOverIL = QAction("Step Over", self) self.actionStepOverIL.triggered.connect( lambda: self.perform_step_over_il()) self.actionStepOverIL.setIcon(load_icon('stepover.svg')) self.actionStepReturn = QAction("Step Return", self) self.actionStepReturn.triggered.connect( lambda: self.perform_step_return()) self.actionStepReturn.setIcon(load_icon('stepout.svg')) # session control menu self.controlMenu = QMenu("Process Control", self) self.controlMenu.addAction(self.actionRun) self.controlMenu.addAction(self.actionRestart) self.controlMenu.addAction(self.actionQuit) self.controlMenu.addSeparator() self.controlMenu.addAction(self.actionAttach) self.controlMenu.addAction(self.actionDetach) self.controlMenu.addSeparator() self.controlMenu.addAction(self.actionSettings) self.stepIntoMenu = QMenu("Step Into", self) self.stepIntoMenu.addAction(self.actionStepIntoIL) self.stepIntoMenu.addAction(self.actionStepIntoAsm) self.stepOverMenu = QMenu("Step Over", self) self.stepOverMenu.addAction(self.actionStepOverIL) self.stepOverMenu.addAction(self.actionStepOverAsm) self.btnControl = QToolButton(self) self.btnControl.setMenu(self.controlMenu) self.btnControl.setPopupMode(QToolButton.MenuButtonPopup) self.btnControl.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.btnControl.setDefaultAction(self.actionRun) self.addWidget(self.btnControl) # execution control buttons self.btnPauseResume = QToolButton(self) self.btnPauseResume.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.btnPauseResume.setDefaultAction(self.actionPause) self.addWidget(self.btnPauseResume) #self.addAction(self.actionPause) #self.addAction(self.actionResume) self.btnStepInto = QToolButton(self) self.btnStepInto.setMenu(self.stepIntoMenu) self.btnStepInto.setPopupMode(QToolButton.MenuButtonPopup) self.btnStepInto.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.btnStepInto.setDefaultAction(self.actionStepIntoIL) self.addWidget(self.btnStepInto) self.btnStepOver = QToolButton(self) self.btnStepOver.setMenu(self.stepOverMenu) self.btnStepOver.setPopupMode(QToolButton.MenuButtonPopup) self.btnStepOver.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.btnStepOver.setDefaultAction(self.actionStepOverIL) self.addWidget(self.btnStepOver) # TODO: Step until returning from current function self.btnStepReturn = QToolButton(self) self.btnStepReturn.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.btnStepReturn.setDefaultAction(self.actionStepReturn) self.addWidget(self.btnStepReturn) #self.addAction(self.actionStepReturn) self.threadMenu = QMenu("Threads", self) self.btnThreads = QToolButton(self) self.btnThreads.setMenu(self.threadMenu) self.btnThreads.setPopupMode(QToolButton.InstantPopup) self.btnThreads.setToolButtonStyle(Qt.ToolButtonTextOnly) self.addWidget(self.btnThreads) self.set_thread_list([]) self.editStatus = QLineEdit('INACTIVE', self) self.editStatus.setReadOnly(True) self.editStatus.setAlignment(QtCore.Qt.AlignCenter) self.addWidget(self.editStatus) # disable buttons self.set_actions_enabled(Run=self.can_exec(), Restart=False, Quit=False, Attach=self.can_connect(), Detach=False, Pause=False, Resume=False, StepInto=False, StepOver=False, StepReturn=False) self.set_resume_pause_action("Pause") self.set_default_process_action( "Attach" if self.can_connect() else "Run") def __del__(self): # TODO: Move this elsewhere # This widget is tasked with cleaning up the state after the view is closed # binjaplug.delete_state(self.bv) pass # ------------------------------------------------------------------------- # Helpers # ------------------------------------------------------------------------- def can_exec(self): return DebugAdapter.ADAPTER_TYPE.use_exec( self.debug_state.adapter_type) def can_connect(self): return DebugAdapter.ADAPTER_TYPE.use_connect( self.debug_state.adapter_type) def alert_need_install(self, proc): message = "Cannot start debugger: {} not found on {}.".format( proc, "the target machine" if self.can_connect() else "your machine") adapter_type = self.debug_state.adapter_type # TODO: detect remote os correctly, as gdb/lldb are compatible with both macos and linux if adapter_type == DebugAdapter.ADAPTER_TYPE.LOCAL_GDB or adapter_type == DebugAdapter.ADAPTER_TYPE.REMOTE_GDB: remote_os = "Linux" elif adapter_type == DebugAdapter.ADAPTER_TYPE.LOCAL_LLDB or adapter_type == DebugAdapter.ADAPTER_TYPE.REMOTE_LLDB: remote_os = "Darwin" elif adapter_type == DebugAdapter.ADAPTER_TYPE.LOCAL_DBGENG or adapter_type == DebugAdapter.ADAPTER_TYPE.REMOTE_DBGENG: remote_os = "Windows" else: # Uncertain remote_os = platform.system() if remote_os == "Linux": message += "\nYou can find this in your package manager or build it from source." elif remote_os == "Darwin": if proc == "lldb": message += "\nYou need to install it by running the following command in Terminal:\nxcode-select --install" elif proc == "gdbserver": message += "\nYou can find this in your package manager or build it from source." else: message += "\nYou need to install this manually." elif remote_os == "Windows": # TODO: dbgeng does not currently throw this message += "\nYou need to reinstall the debugger plugin." else: message += "\nYou need to install this manually." show_message_box("Cannot Start Debugger", message, icon=MessageBoxIcon.ErrorIcon) # ------------------------------------------------------------------------- # UIActions # ------------------------------------------------------------------------- def perform_run(self): def perform_run_thread(): while True: try: self.debug_state.run() execute_on_main_thread_and_wait(perform_run_after) except ConnectionRefusedError: execute_on_main_thread_and_wait( lambda: perform_run_error('ERROR: Connection Refused')) except DebugAdapter.ProcessStartError as e: execute_on_main_thread_and_wait( lambda: perform_run_error(str(e))) except DebugAdapter.NotExecutableError as e: fpath = e.args[0] if platform.system() != 'Windows': msg = '%s is not executable, would you like to set +x and retry?' % fpath res = show_message_box( 'Error', msg, MessageBoxButtonSet.YesNoButtonSet, MessageBoxIcon.ErrorIcon) if res == MessageBoxButtonResult.YesButton: os.chmod(fpath, os.stat(fpath).st_mode | 0o100) continue execute_on_main_thread_and_wait(lambda: perform_run_error( 'ERROR: Target Not Executable')) except DebugAdapter.NotInstalledError as e: execute_on_main_thread_and_wait( lambda: self.alert_need_install(e.args[0])) execute_on_main_thread_and_wait(lambda: perform_run_error( 'ERROR: Debugger Not Installed')) except DebugAdapter.PermissionDeniedError as e: execute_on_main_thread_and_wait( lambda: perform_run_error('ERROR: Permission denied')) if platform.system() == 'Darwin': res = show_message_box( 'Error', 'Developer tools need to be enabled to debug programs. This can be authorized either from here or by starting a debugger in Xcode.', MessageBoxButtonSet.OKButtonSet, MessageBoxIcon.ErrorIcon) except Exception as e: execute_on_main_thread_and_wait(lambda: perform_run_error( 'ERROR: ' + ' '.join(e.args))) traceback.print_exc(file=sys.stderr) break def perform_run_after(): self.state_stopped() self.debug_state.ui.on_step() def perform_run_error(e): self.state_error(e) self.state_starting('STARTING') threading.Thread(target=perform_run_thread).start() def perform_restart(self): def perform_restart_thread(): try: self.debug_state.restart() execute_on_main_thread_and_wait(perform_restart_after) except ConnectionRefusedError: execute_on_main_thread_and_wait( lambda: perform_restart_error('ERROR: Connection Refused')) except Exception as e: execute_on_main_thread_and_wait(lambda: perform_restart_error( 'ERROR: ' + ' '.join(e.args))) traceback.print_exc(file=sys.stderr) def perform_restart_after(): self.state_stopped() self.debug_state.ui.on_step() def perform_restart_error(e): self.state_error(e) self.state_starting('RESTARTING') threading.Thread(target=perform_restart_thread).start() def perform_quit(self): self.debug_state.quit() self.state_inactive() self.debug_state.ui.on_step() def perform_attach(self): def perform_attach_thread(): try: self.debug_state.attach() execute_on_main_thread_and_wait(perform_attach_after) except ConnectionRefusedError: execute_on_main_thread_and_wait( lambda: perform_attach_error('ERROR: Connection Refused')) except TimeoutError: execute_on_main_thread_and_wait( lambda: perform_attach_error('ERROR: Connection Refused')) except Exception as e: execute_on_main_thread_and_wait( lambda: perform_attach_error('ERROR: ' + ' '.join(e.args))) traceback.print_exc(file=sys.stderr) def perform_attach_after(): self.state_stopped() self.debug_state.ui.on_step() def perform_attach_error(e): self.state_error(e) self.state_starting('ATTACHING') threading.Thread(target=perform_attach_thread).start() def perform_detach(self): self.debug_state.detach() self.state_inactive() self.debug_state.ui.on_step() def perform_settings(self): def settings_finished(): if self.debug_state.running: self.state_running() elif self.debug_state.connected: local_rip = self.debug_state.local_ip if self.debug_state.bv.read(local_rip, 1) and len( self.debug_state.bv.get_functions_containing( local_rip)) > 0: self.state_stopped() else: self.state_stopped_extern() else: self.state_inactive() dialog = AdapterSettingsDialog.AdapterSettingsDialog(self, self.bv) dialog.show() dialog.finished.connect(settings_finished) def perform_pause(self): self.debug_state.pause() # Don't update state here-- one of the other buttons is running in a thread and updating for us def perform_resume(self): def perform_resume_thread(): (reason, data) = self.debug_state.go() execute_on_main_thread_and_wait( lambda: perform_resume_after(reason, data)) def perform_resume_after(reason, data): self.handle_stop_return(reason, data) self.debug_state.ui.on_step() self.state_running() threading.Thread(target=perform_resume_thread).start() def perform_step_into_asm(self): def perform_step_into_asm_thread(): (reason, data) = self.debug_state.step_into() execute_on_main_thread_and_wait( lambda: perform_step_into_asm_after(reason, data)) def perform_step_into_asm_after(reason, data): self.handle_stop_return(reason, data) self.debug_state.ui.on_step() self.state_busy("STEPPING") threading.Thread(target=perform_step_into_asm_thread).start() def perform_step_into_il(self): disasm = self.debug_state.ui.debug_view.binary_editor.getDisassembly() graph_type = disasm.getGraphType() def perform_step_into_il_thread(): (reason, data) = self.debug_state.step_into(graph_type) execute_on_main_thread_and_wait( lambda: perform_step_into_il_after(reason, data)) def perform_step_into_il_after(reason, data): self.handle_stop_return(reason, data) self.debug_state.ui.on_step() self.state_busy("STEPPING") threading.Thread(target=perform_step_into_il_thread).start() def perform_step_over_asm(self): def perform_step_over_asm_thread(): (reason, data) = self.debug_state.step_over() execute_on_main_thread_and_wait( lambda: perform_step_over_asm_after(reason, data)) def perform_step_over_asm_after(reason, data): self.handle_stop_return(reason, data) self.debug_state.ui.on_step() self.state_busy("STEPPING") threading.Thread(target=perform_step_over_asm_thread).start() def perform_step_over_il(self): disasm = self.debug_state.ui.debug_view.binary_editor.getDisassembly() graph_type = disasm.getGraphType() def perform_step_over_il_thread(): (reason, data) = self.debug_state.step_over(graph_type) execute_on_main_thread_and_wait( lambda: perform_step_over_il_after(reason, data)) def perform_step_over_il_after(reason, data): self.handle_stop_return(reason, data) self.debug_state.ui.on_step() self.state_busy("STEPPING") threading.Thread(target=perform_step_over_il_thread).start() def perform_step_return(self): def perform_step_return_thread(): (reason, data) = self.debug_state.step_return() execute_on_main_thread_and_wait( lambda: perform_step_return_after(reason, data)) def perform_step_return_after(reason, data): self.handle_stop_return(reason, data) self.debug_state.ui.on_step() self.state_busy("STEPPING") threading.Thread(target=perform_step_return_thread).start() # ------------------------------------------------------------------------- # Control state setters # ------------------------------------------------------------------------- def set_actions_enabled(self, **kwargs): def enable_step_into(e): self.actionStepIntoAsm.setEnabled(e) self.actionStepIntoIL.setEnabled(e) def enable_step_over(e): self.actionStepOverAsm.setEnabled(e) self.actionStepOverIL.setEnabled(e) def enable_starting(e): self.actionRun.setEnabled(e and self.can_exec()) self.actionAttach.setEnabled(e and self.can_connect()) def enable_stopping(e): self.actionRestart.setEnabled(e) self.actionQuit.setEnabled(e) self.actionDetach.setEnabled(e) def enable_stepping(e): self.actionStepIntoAsm.setEnabled(e) self.actionStepIntoIL.setEnabled(e) self.actionStepOverAsm.setEnabled(e) self.actionStepOverIL.setEnabled(e) self.actionStepReturn.setEnabled(e) actions = { "Run": lambda e: self.actionRun.setEnabled(e), "Restart": lambda e: self.actionRestart.setEnabled(e), "Quit": lambda e: self.actionQuit.setEnabled(e), "Attach": lambda e: self.actionAttach.setEnabled(e), "Detach": lambda e: self.actionDetach.setEnabled(e), "Pause": lambda e: self.actionPause.setEnabled(e), "Resume": lambda e: self.actionResume.setEnabled(e), "StepInto": enable_step_into, "StepOver": enable_step_over, "StepReturn": lambda e: self.actionStepReturn.setEnabled(e), "Threads": lambda e: self.btnThreads.setEnabled(e), "Starting": enable_starting, "Stopping": enable_stopping, "Stepping": enable_stepping, } for (action, enabled) in kwargs.items(): actions[action](enabled) def set_default_process_action(self, action): actions = { "Run": self.actionRun, "Restart": self.actionRestart, "Quit": self.actionQuit, "Attach": self.actionAttach, "Detach": self.actionDetach, } self.btnControl.setDefaultAction(actions[action]) def set_resume_pause_action(self, action): lookup = {'Resume': self.actionResume, 'Pause': self.actionPause} self.btnPauseResume.setDefaultAction(lookup[action]) def set_thread_list(self, threads): def select_thread_fn(tid): def select_thread(tid): stateObj = binjaplug.get_state(self.bv) if stateObj.connected and not stateObj.running: stateObj.threads.current = tid stateObj.ui.context_display() stateObj.ui.on_step() else: print('cannot set thread in current state') return lambda: select_thread(tid) self.threadMenu.clear() if len(threads) > 0: for thread in threads: item_name = "Thread {} at {}".format(thread['tid'], hex(thread['ip'])) action = self.threadMenu.addAction( item_name, select_thread_fn(thread['tid'])) if thread['selected']: self.btnThreads.setDefaultAction(action) else: defaultThreadAction = self.threadMenu.addAction("Thread List") defaultThreadAction.setEnabled(False) self.btnThreads.setDefaultAction(defaultThreadAction) # ------------------------------------------------------------------------- # State handling # ------------------------------------------------------------------------- def state_starting(self, msg=None): self.editStatus.setText(msg or 'INACTIVE') self.set_actions_enabled(Starting=False, Stopping=False, Stepping=False, Pause=False, Resume=False, Threads=False) self.set_default_process_action( "Attach" if self.can_connect() else "Run") self.set_thread_list([]) self.set_resume_pause_action("Pause") def state_inactive(self, msg=None): self.editStatus.setText(msg or 'INACTIVE') self.set_actions_enabled(Starting=True, Stopping=False, Stepping=False, Pause=False, Resume=False, Threads=False) self.set_default_process_action( "Attach" if self.can_connect() else "Run") self.set_thread_list([]) self.set_resume_pause_action("Pause") def state_stopped(self, msg=None): self.editStatus.setText(msg or 'STOPPED') self.set_actions_enabled(Starting=False, Stopping=True, Stepping=True, Pause=True, Resume=True, Threads=True) self.set_default_process_action("Quit") self.set_resume_pause_action("Resume") def state_stopped_extern(self, msg=None): self.editStatus.setText(msg or 'STOPPED') self.set_actions_enabled(Starting=False, Stopping=True, Stepping=True, StepReturn=False, Pause=True, Resume=True, Threads=True) self.set_default_process_action("Quit") self.set_resume_pause_action("Resume") def state_running(self, msg=None): self.editStatus.setText(msg or 'RUNNING') self.set_actions_enabled(Starting=False, Stopping=True, Stepping=False, Pause=True, Resume=False, Threads=False) self.set_default_process_action("Quit") self.set_resume_pause_action("Pause") def state_busy(self, msg=None): self.editStatus.setText(msg or 'RUNNING') self.set_actions_enabled(Starting=False, Stopping=True, Stepping=False, Pause=True, Resume=False, Threads=False) self.set_default_process_action("Quit") self.set_resume_pause_action("Pause") def state_error(self, msg=None): self.editStatus.setText(msg or 'ERROR') self.set_actions_enabled(Starting=True, Stopping=False, Pause=False, Resume=False, Stepping=False, Threads=False) self.set_default_process_action( "Attach" if self.can_connect() else "Run") self.set_thread_list([]) self.set_resume_pause_action("Resume") def handle_stop_return(self, reason, data): if reason == DebugAdapter.STOP_REASON.STDOUT_MESSAGE: self.state_stopped('stdout: ' + data) elif reason == DebugAdapter.STOP_REASON.PROCESS_EXITED: self.debug_state.quit() self.state_inactive('process exited, return code=%d' % data) elif reason == DebugAdapter.STOP_REASON.BACKEND_DISCONNECTED: self.debug_state.quit() self.state_inactive('backend disconnected (process exited?)')
class PlotImage(FigureCanvas): def __init__(self, model, parent, main_window): self.figure = Figure(dpi=main_window.logicalDpiX()) super().__init__(self.figure) FigureCanvas.setSizePolicy(self, QSizePolicy.Expanding, QSizePolicy.Expanding) FigureCanvas.updateGeometry(self) self.model = model self.main_window = main_window self.parent = parent self.rubber_band = QRubberBand(QRubberBand.Rectangle, self) self.band_origin = QtCore.QPoint() self.x_plot_origin = None self.y_plot_origin = None self.colorbar = None self.data_indicator = None self.tally_data_indicator = None self.image = None self.menu = QMenu(self) def enterEvent(self, event): self.setCursor(QtCore.Qt.CrossCursor) self.main_window.coord_label.show() def leaveEvent(self, event): self.main_window.coord_label.hide() self.main_window.statusBar().showMessage("") def mousePressEvent(self, event): self.main_window.coord_label.hide() position = event.pos() # Set rubber band absolute and relative position self.band_origin = position self.x_plot_origin, self.y_plot_origin = self.getPlotCoords(position) # Create rubber band self.rubber_band.setGeometry( QtCore.QRect(self.band_origin, QtCore.QSize())) def getPlotCoords(self, pos): x, y = self.mouseEventCoords(pos) # get the normalized axis coordinates from the event display units transform = self.ax.transAxes.inverted() xPlotCoord, yPlotCoord = transform.transform((x, y)) # flip the y-axis (its zero is in the upper left) # scale axes using the plot extents xPlotCoord = self.ax.dataLim.x0 + xPlotCoord * self.ax.dataLim.width yPlotCoord = self.ax.dataLim.y0 + yPlotCoord * self.ax.dataLim.height # set coordinate label if pointer is in the axes if self.parent.underMouse(): self.main_window.coord_label.show() self.main_window.showCoords(xPlotCoord, yPlotCoord) else: self.main_window.coord_label.hide() return (xPlotCoord, yPlotCoord) def _resize(self): z = self.main_window.zoom / 100.0 # manage scroll bars if z <= 1.0: self.parent.verticalScrollBar().hide() self.parent.horizontalScrollBar().hide() self.parent.cornerWidget().hide() self.parent.verticalScrollBar().setEnabled(False) self.parent.horizontalScrollBar().setEnabled(False) else: self.parent.verticalScrollBar().show() self.parent.horizontalScrollBar().show() self.parent.cornerWidget().show() self.parent.verticalScrollBar().setEnabled(True) self.parent.horizontalScrollBar().setEnabled(True) # resize plot self.resize(self.parent.width() * z, self.parent.height() * z) def getDataIndices(self, event): cv = self.model.currentView x, y = self.mouseEventCoords(event.pos()) # get origin in axes coordinates x0, y0 = self.ax.transAxes.transform((0.0, 0.0)) # get the extents of the axes box in axes coordinates bbox = self.ax.get_window_extent().transformed( self.figure.dpi_scale_trans.inverted()) # get dimensions and scale using dpi width, height = bbox.width, bbox.height width *= self.figure.dpi height *= self.figure.dpi # use factor to get proper x,y position in pixels factor = (width / cv.h_res, height / cv.v_res) xPos = int((x - x0 + 0.01) / factor[0]) # flip y-axis yPos = cv.v_res - int((y - y0 + 0.01) / factor[1]) return xPos, yPos def getTallyIndices(self, event): xPos, yPos = self.getPlotCoords(event.pos()) ext = self.model.tally_extents x0 = ext[0] y0 = ext[2] v_res, h_res = self.model.tally_data.shape dx = (ext[1] - ext[0]) / h_res dy = (ext[3] - ext[2]) / v_res i = int((xPos - x0) // dx) j = v_res - int((yPos - y0) // dy) - 1 return i, j def getTallyInfo(self, event): cv = self.model.currentView xPos, yPos = self.getTallyIndices(event) if self.model.tally_data is None: return -1, None if not cv.selectedTally or not cv.tallyDataVisible: return -1, None # don't look up mesh filter data (for now) tally = self.model.statepoint.tallies[cv.selectedTally] # check that the position is in the axes view v_res, h_res = self.model.tally_data.shape if 0 <= yPos < v_res and 0 <= xPos < h_res: value = self.model.tally_data[yPos][xPos] else: value = None return cv.selectedTally, value def getIDinfo(self, event): xPos, yPos = self.getDataIndices(event) # check that the position is in the axes view if 0 <= yPos < self.model.currentView.v_res \ and 0 <= xPos and xPos < self.model.currentView.h_res: id = self.model.ids[yPos][xPos] temp = "{:g}".format(self.model.properties[yPos][xPos][0]) density = "{:g}".format(self.model.properties[yPos][xPos][1]) else: id = _NOT_FOUND density = str(_NOT_FOUND) temp = str(_NOT_FOUND) if self.model.currentView.colorby == 'cell': domain = self.model.activeView.cells domain_kind = 'Cell' elif self.model.currentView.colorby == 'temperature': domain = self.model.activeView.materials domain_kind = 'Temperature' elif self.model.currentView.colorby == 'density': domain = self.model.activeView.materials domain_kind = 'Density' else: domain = self.model.activeView.materials domain_kind = 'Material' properties = {'density': density, 'temperature': temp} return id, properties, domain, domain_kind def mouseDoubleClickEvent(self, event): xCenter, yCenter = self.getPlotCoords(event.pos()) self.main_window.editPlotOrigin(xCenter, yCenter, apply=True) def mouseMoveEvent(self, event): cv = self.model.currentView # Show Cursor position relative to plot in status bar xPlotPos, yPlotPos = self.getPlotCoords(event.pos()) # Show Cell/Material ID, Name in status bar id, properties, domain, domain_kind = self.getIDinfo(event) domainInfo = "" tallyInfo = "" if self.parent.underMouse(): if domain_kind.lower() in _MODEL_PROPERTIES: line_val = float(properties[domain_kind.lower()]) line_val = max(line_val, 0.0) self.updateDataIndicatorValue(line_val) domain_kind = 'Material' temperature = properties['temperature'] density = properties['density'] if id == _VOID_REGION: domainInfo = ("VOID") elif id == _OVERLAP: domainInfo = ("OVERLAP") elif id != _NOT_FOUND and domain[id].name: domainInfo = ("{} {}: \"{}\"\t Density: {} g/cc\t" "Temperature: {} K".format( domain_kind, id, domain[id].name, density, temperature)) elif id != _NOT_FOUND: domainInfo = ("{} {}\t Density: {} g/cc\t" "Temperature: {} K".format( domain_kind, id, density, temperature)) else: domainInfo = "" if self.model.tally_data is not None: tid, value = self.getTallyInfo(event) if value is not None and value != np.nan: self.updateTallyDataIndicatorValue(value) tallyInfo = "Tally {} {}: {:.5E}".format( tid, cv.tallyValue, value) else: self.updateTallyDataIndicatorValue(0.0) else: self.updateTallyDataIndicatorValue(0.0) self.updateDataIndicatorValue(0.0) if domainInfo: self.main_window.statusBar().showMessage(" " + domainInfo + " " + tallyInfo) else: self.main_window.statusBar().showMessage(" " + tallyInfo) # Update rubber band and values if mouse button held down if event.buttons() == QtCore.Qt.LeftButton: self.rubber_band.setGeometry( QtCore.QRect(self.band_origin, event.pos()).normalized()) # Show rubber band if both dimensions > 10 pixels if self.rubber_band.width() > 10 and self.rubber_band.height( ) > 10: self.rubber_band.show() else: self.rubber_band.hide() # Update plot X Origin xCenter = (self.x_plot_origin + xPlotPos) / 2 yCenter = (self.y_plot_origin + yPlotPos) / 2 self.main_window.editPlotOrigin(xCenter, yCenter) modifiers = event.modifiers() # Zoom out if Shift held if modifiers == QtCore.Qt.ShiftModifier: cv = self.model.currentView bandwidth = abs(self.band_origin.x() - event.pos().x()) width = cv.width * (cv.h_res / max(bandwidth, .001)) bandheight = abs(self.band_origin.y() - event.pos().y()) height = cv.height * (cv.v_res / max(bandheight, .001)) # Zoom in else: width = max(abs(self.x_plot_origin - xPlotPos), 0.1) height = max(abs(self.y_plot_origin - yPlotPos), 0.1) self.main_window.editWidth(width) self.main_window.editHeight(height) def mouseReleaseEvent(self, event): if self.rubber_band.isVisible(): self.rubber_band.hide() self.main_window.applyChanges() else: self.main_window.revertDockControls() def wheelEvent(self, event): if event.delta() and event.modifiers() == QtCore.Qt.ShiftModifier: numDegrees = event.delta() / 8 if 24 < self.main_window.zoom + numDegrees < 5001: self.main_window.editZoom(self.main_window.zoom + numDegrees) def contextMenuEvent(self, event): self.menu.clear() self.main_window.undoAction.setText('&Undo ({})'.format( len(self.model.previousViews))) self.main_window.redoAction.setText('&Redo ({})'.format( len(self.model.subsequentViews))) id, properties, domain, domain_kind = self.getIDinfo(event) cv = self.model.currentView # always provide undo option self.menu.addSeparator() self.menu.addAction(self.main_window.undoAction) self.menu.addAction(self.main_window.redoAction) self.menu.addSeparator() if int(id) not in (_NOT_FOUND, _OVERLAP) and \ cv.colorby not in _MODEL_PROPERTIES: # Domain ID if domain[id].name: domainID = self.menu.addAction("{} {}: \"{}\"".format( domain_kind, id, domain[id].name)) else: domainID = self.menu.addAction("{} {}".format(domain_kind, id)) self.menu.addSeparator() colorAction = self.menu.addAction( 'Edit {} Color...'.format(domain_kind)) colorAction.setDisabled(cv.highlighting) colorAction.setToolTip('Edit {} color'.format(domain_kind)) colorAction.setStatusTip('Edit {} color'.format(domain_kind)) domain_color_connector = partial(self.main_window.editDomainColor, domain_kind, id) colorAction.triggered.connect(domain_color_connector) maskAction = self.menu.addAction('Mask {}'.format(domain_kind)) maskAction.setCheckable(True) maskAction.setChecked(domain[id].masked) maskAction.setDisabled(not cv.masking) maskAction.setToolTip('Toggle {} mask'.format(domain_kind)) maskAction.setStatusTip('Toggle {} mask'.format(domain_kind)) mask_connector = partial(self.main_window.toggleDomainMask, kind=domain_kind, id=id) maskAction.toggled.connect(mask_connector) highlightAction = self.menu.addAction( 'Highlight {}'.format(domain_kind)) highlightAction.setCheckable(True) highlightAction.setChecked(domain[id].highlight) highlightAction.setDisabled(not cv.highlighting) highlightAction.setToolTip( 'Toggle {} highlight'.format(domain_kind)) highlightAction.setStatusTip( 'Toggle {} highlight'.format(domain_kind)) highlight_connector = partial( self.main_window.toggleDomainHighlight, kind=domain_kind, id=id) highlightAction.toggled.connect(highlight_connector) else: self.menu.addAction(self.main_window.undoAction) self.menu.addAction(self.main_window.redoAction) if cv.colorby not in _MODEL_PROPERTIES: self.menu.addSeparator() if int(id) == _NOT_FOUND: bgColorAction = self.menu.addAction( 'Edit Background Color...') bgColorAction.setToolTip('Edit background color') bgColorAction.setStatusTip('Edit plot background color') connector = partial(self.main_window.editBackgroundColor, apply=True) bgColorAction.triggered.connect(connector) elif int(id) == _OVERLAP: olapColorAction = self.menu.addAction( 'Edit Overlap Color...') olapColorAction.setToolTip('Edit overlap color') olapColorAction.setStatusTip('Edit plot overlap color') connector = partial(self.main_window.editOverlapColor, apply=True) olapColorAction.triggered.connect(connector) self.menu.addSeparator() self.menu.addAction(self.main_window.saveImageAction) self.menu.addAction(self.main_window.saveViewAction) self.menu.addAction(self.main_window.openAction) self.menu.addSeparator() self.menu.addMenu(self.main_window.basisMenu) self.menu.addMenu(self.main_window.colorbyMenu) self.menu.addSeparator() if domain_kind.lower() not in ('density', 'temperature'): self.menu.addAction(self.main_window.maskingAction) self.menu.addAction(self.main_window.highlightingAct) self.menu.addAction(self.main_window.overlapAct) self.menu.addSeparator() self.menu.addAction(self.main_window.dockAction) self.main_window.maskingAction.setChecked(cv.masking) self.main_window.highlightingAct.setChecked(cv.highlighting) self.main_window.overlapAct.setChecked(cv.color_overlaps) if self.main_window.dock.isVisible(): self.main_window.dockAction.setText('Hide &Dock') else: self.main_window.dockAction.setText('Show &Dock') self.menu.exec_(event.globalPos()) def generatePixmap(self, update=False): self.model.generatePlot() if update: self.updatePixmap() def updatePixmap(self): # clear out figure self.figure.clear() cv = self.model.currentView # set figure bg color to match window window_bg = self.parent.palette().color(QtGui.QPalette.Background) self.figure.patch.set_facecolor(rgb_normalize(window_bg.getRgb())) # set data extents for automatic reporting of pointer location # in model units data_bounds = [ cv.origin[self.main_window.xBasis] - cv.width / 2., cv.origin[self.main_window.xBasis] + cv.width / 2., cv.origin[self.main_window.yBasis] - cv.height / 2., cv.origin[self.main_window.yBasis] + cv.height / 2. ] # make sure we have a domain image to load if not hasattr(self.model, 'image'): self.model.generatePlot() ### DRAW DOMAIN IMAGE ### # still generate the domain image if the geometric # plot isn't visible so mouse-over info can still # be shown alpha = cv.domainAlpha if cv.domainVisible else 0.0 if cv.colorby in ('material', 'cell'): self.image = self.figure.subplots().imshow(self.model.image, extent=data_bounds, alpha=alpha) else: cmap = cv.colormaps[cv.colorby] if cv.colorby == 'temperature': idx = 0 cmap_label = "Temperature (K)" else: idx = 1 cmap_label = "Density (g/cc)" norm = SymLogNorm(1E-10) if cv.color_scale_log[ cv.colorby] else None data = self.model.properties[:, :, idx] self.image = self.figure.subplots().imshow(data, cmap=cmap, norm=norm, extent=data_bounds, alpha=cv.domainAlpha) # add colorbar self.colorbar = self.figure.colorbar(self.image, anchor=(1.0, 0.0)) self.colorbar.set_label(cmap_label, rotation=-90, labelpad=15) # draw line on colorbar dl = self.colorbar.ax.dataLim.get_points() self.data_indicator = mlines.Line2D(dl[:][0], [0.0, 0.0], linewidth=3., color='blue', clip_on=True) self.colorbar.ax.add_line(self.data_indicator) self.colorbar.ax.margins(0.0, 0.0) self.updateDataIndicatorVisibility() self.updateColorMinMax(cv.colorby) self.ax = self.figure.axes[0] self.ax.margins(0.0, 0.0) # set axis labels axis_label_str = "{} (cm)" self.ax.set_xlabel(axis_label_str.format(cv.basis[0])) self.ax.set_ylabel(axis_label_str.format(cv.basis[1])) # generate tally image image_data, extents, data_min, data_max, units = self.model.create_tally_image( ) ### DRAW TALLY IMAGE ### # draw tally image if image_data is not None: if not cv.tallyDataUserMinMax: cv.tallyDataMin = data_min cv.tallyDataMax = data_max else: data_min = cv.tallyDataMin data_max = cv.tallyDataMax # always mask out negative values image_mask = image_data < 0.0 if cv.clipTallyData: image_mask |= image_data < data_min image_mask |= image_data > data_max if cv.tallyMaskZeroValues: image_mask |= image_data == 0.0 # mask out invalid values image_data = np.ma.masked_where(image_mask, image_data) if extents is None: extents = data_bounds self.model.tally_data = image_data self.model.tally_extents = extents if extents is not None else data_bounds norm = SymLogNorm(1E-30) if cv.tallyDataLogScale else None if cv.tallyContours: # parse the levels line levels = self.parseContoursLine(cv.tallyContourLevels) self.tally_image = self.ax.contour(image_data, origin='image', levels=levels, alpha=cv.tallyDataAlpha, cmap=cv.tallyDataColormap, norm=norm, extent=extents) else: self.tally_image = self.ax.imshow(image_data, alpha=cv.tallyDataAlpha, cmap=cv.tallyDataColormap, norm=norm, extent=extents) # add colorbar self.tally_colorbar = self.figure.colorbar(self.tally_image, anchor=(1.0, 0.0)) if cv.tallyContours: fmt = "%.2E" self.ax.clabel(self.tally_image, self.tally_image.levels, inline=True, fmt=fmt) # draw line on colorbar self.tally_data_indicator = mlines.Line2D([0.0, 1.0], [0.0, 0.0], linewidth=3., color='blue', clip_on=True) self.tally_colorbar.ax.add_line(self.tally_data_indicator) self.tally_colorbar.ax.margins(0.0, 0.0) self.tally_data_indicator.set_visible(cv.tallyDataIndicator) self.main_window.updateTallyMinMax() self.tally_colorbar.mappable.set_clim(data_min, data_max) self.tally_colorbar.set_label(units, rotation=-90, labelpad=15) # annotate outlines self.add_outlines() # always make sure the data bounds are set correctly self.ax.set_xbound(data_bounds[0], data_bounds[1]) self.ax.set_ybound(data_bounds[2], data_bounds[3]) self.ax.dataLim.x0 = data_bounds[0] self.ax.dataLim.x1 = data_bounds[1] self.ax.dataLim.y0 = data_bounds[2] self.ax.dataLim.y1 = data_bounds[3] self.draw() return "Done" def add_outlines(self): cv = self.model.currentView # draw outlines as isocontours if cv.outlines: # set data extents for automatic reporting of pointer location data_bounds = [ cv.origin[self.main_window.xBasis] - cv.width / 2., cv.origin[self.main_window.xBasis] + cv.width / 2., cv.origin[self.main_window.yBasis] - cv.height / 2., cv.origin[self.main_window.yBasis] + cv.height / 2. ] levels = np.unique(self.model.ids) self.contours = self.ax.contour(self.model.ids, origin='upper', colors='k', linestyles='solid', levels=levels, extent=data_bounds) @staticmethod def parseContoursLine(line): # if there are any commas in the line, treat as level values line = line.strip() if ',' in line: return [float(val) for val in line.split(",") if val != ''] else: return int(line) def updateColorbarScale(self): self.updatePixmap() def updateTallyDataIndicatorValue(self, y_val): cv = self.model.currentView if not cv.tallyDataVisible or not cv.tallyDataIndicator: return if self.tally_data_indicator is not None: data = self.tally_data_indicator.get_data() # use norm to get axis value if log scale if cv.tallyDataLogScale: y_val = self.tally_image.norm(y_val) self.tally_data_indicator.set_data([data[0], [y_val, y_val]]) dl_color = invert_rgb(self.tally_image.get_cmap()(y_val), True) self.tally_data_indicator.set_c(dl_color) self.draw() def updateDataIndicatorValue(self, y_val): cv = self.model.currentView if cv.colorby not in _MODEL_PROPERTIES or \ not cv.data_indicator_enabled[cv.colorby]: return if self.data_indicator: data = self.data_indicator.get_data() # use norm to get axis value if log scale if cv.color_scale_log[cv.colorby]: y_val = self.image.norm(y_val) self.data_indicator.set_data([data[0], [y_val, y_val]]) dl_color = invert_rgb(self.image.get_cmap()(y_val), True) self.data_indicator.set_c(dl_color) self.draw() def updateDataIndicatorVisibility(self): cv = self.model.currentView if self.data_indicator and cv.colorby in _MODEL_PROPERTIES: val = cv.data_indicator_enabled[cv.colorby] self.data_indicator.set_visible(val) self.draw() def updateColorMap(self, colormap_name, property_type): if self.colorbar and property_type == self.model.activeView.colorby: self.image.set_cmap(colormap_name) self.colorbar.draw_all() self.draw() def updateColorMinMax(self, property_type): av = self.model.activeView if self.colorbar and property_type == av.colorby: clim = av.getColorLimits(property_type) self.colorbar.mappable.set_clim(*clim) self.data_indicator.set_data(clim[:2], (0.0, 0.0)) self.colorbar.draw_all() self.draw()
class PlotImage(FigureCanvas): def __init__(self, model, parent, main): super(FigureCanvas, self).__init__(Figure()) FigureCanvas.setSizePolicy(self, QSizePolicy.Expanding, QSizePolicy.Expanding) FigureCanvas.updateGeometry(self) self.model = model self.mw = main self.parent = parent self.rubber_band = QRubberBand(QRubberBand.Rectangle, self) self.band_origin = QtCore.QPoint() self.x_plot_origin = None self.y_plot_origin = None self.menu = QMenu(self) def enterEvent(self, event): self.setCursor(QtCore.Qt.CrossCursor) self.mw.coord_label.show() def leaveEvent(self, event): self.mw.coord_label.hide() self.mw.statusBar().showMessage("") def mousePressEvent(self, event): self.mw.coord_label.hide() # Set rubber band absolute and relative position self.band_origin = event.pos() self.x_plot_origin, self.y_plot_origin = self.getPlotCoords( event.pos()) # Create rubber band self.rubber_band.setGeometry( QtCore.QRect(self.band_origin, QtCore.QSize())) FigureCanvas.mousePressEvent(self, event) def getPlotCoords(self, pos): cv = self.model.currentView # get the normalized axis coordinates from the event display units xPlotCoord, yPlotCoord = self.ax.transAxes.inverted().transform( (pos.x(), pos.y())) # flip the y-axis (its zero is in the upper left) yPlotCoord = 1 - yPlotCoord # scale axes using the plot extents xPlotCoord = self.ax.dataLim.x0 + xPlotCoord * self.ax.dataLim.width yPlotCoord = self.ax.dataLim.y0 + yPlotCoord * self.ax.dataLim.height # set coordinate label if pointer is in the axes if self.ax.contains_point((pos.x(), pos.y())): self.mw.coord_label.show() self.mw.showCoords(xPlotCoord, yPlotCoord) else: self.mw.coord_label.hide() return (xPlotCoord, yPlotCoord) def getIDinfo(self, event): cv = self.model.currentView # get origin in axes coordinates x0, y0 = self.ax.transAxes.transform((0., 0.)) # get the extents of the axes box in axes coordinates bbox = self.ax.get_window_extent().transformed( self.figure.dpi_scale_trans.inverted()) # get dimensions and scale using dpi width, height = bbox.width, bbox.height width *= self.figure.dpi height *= self.figure.dpi # use factor to get proper x,y position in pixels factor = (width / cv.h_res, height / cv.v_res) xPos = int((event.pos().x() - x0 + 0.05) / factor[0]) yPos = int((event.pos().y() - y0 + 0.05) / factor[1]) # check that the position is in the axes view if yPos < self.model.currentView.v_res \ and xPos < self.model.currentView.h_res: id = f"{self.model.ids[yPos][xPos]}" temp = f"{self.model.props[yPos][xPos][0]:g}" density = f"{self.model.props[yPos][xPos][1]:g}" else: id = '-1' density = '-1' temp = '-1' if self.model.currentView.colorby == 'cell': domain = self.model.activeView.cells domain_kind = 'Cell' else: domain = self.model.activeView.materials domain_kind = 'Material' properties = {'density': density, 'temperature': temp} return id, properties, domain, domain_kind def mouseDoubleClickEvent(self, event): xCenter, yCenter = self.getPlotCoords(event.pos()) self.mw.editPlotOrigin(xCenter, yCenter, apply=True) FigureCanvas.mouseDoubleClickEvent(self, event) def mouseMoveEvent(self, event): # Show Cursor position relative to plot in status bar xPlotPos, yPlotPos = self.getPlotCoords(event.pos()) # Show Cell/Material ID, Name in status bar id, properties, domain, domain_kind = self.getIDinfo(event) if self.ax.contains_point((event.pos().x(), event.pos().y())): if id != str(_NOT_FOUND_) and domain[id].name: domainInfo = (f"{domain_kind} {id}: \"{domain[id].name}\"\t " f"Density: {properties['density']} g/cm3\t" f"Temperature: {properties['temperature']} K") elif id != str(_NOT_FOUND_): domainInfo = (f"{domain_kind} {id}\t" f"Density: {properties['density']} g/cm3\t" f"Temperature: {properties['temperature']} K") else: domainInfo = "" else: domainInfo = "" self.mw.statusBar().showMessage(f" {domainInfo}") # Update rubber band and values if mouse button held down if event.buttons() == QtCore.Qt.LeftButton: self.rubber_band.setGeometry( QtCore.QRect(self.band_origin, event.pos()).normalized()) # Show rubber band if both dimensions > 10 pixels if self.rubber_band.width() > 10 and self.rubber_band.height( ) > 10: self.rubber_band.show() else: self.rubber_band.hide() # Update plot X Origin xCenter = (self.x_plot_origin + xPlotPos) / 2 yCenter = (self.y_plot_origin + yPlotPos) / 2 self.mw.editPlotOrigin(xCenter, yCenter) modifiers = event.modifiers() # Zoom out if Shift held if modifiers == QtCore.Qt.ShiftModifier: cv = self.model.currentView bandwidth = abs(self.band_origin.x() - event.pos().x()) width = cv.width * (cv.h_res / max(bandwidth, .001)) bandheight = abs(self.band_origin.y() - event.pos().y()) height = cv.height * (cv.v_res / max(bandheight, .001)) else: # Zoom in width = max(abs(self.x_plot_origin - xPlotPos), 0.1) height = max(abs(self.y_plot_origin - yPlotPos), 0.1) self.mw.editWidth(width) self.mw.editHeight(height) def mouseReleaseEvent(self, event): if self.rubber_band.isVisible(): self.rubber_band.hide() self.mw.applyChanges() else: self.mw.revertDockControls() def wheelEvent(self, event): if event.delta() and event.modifiers() == QtCore.Qt.ShiftModifier: numDegrees = event.delta() / 8 if 24 < self.mw.zoom + numDegrees < 5001: self.mw.editZoom(self.mw.zoom + numDegrees) def contextMenuEvent(self, event): self.menu.clear() self.mw.undoAction.setText(f'&Undo ({len(self.model.previousViews)})') self.mw.redoAction.setText( f'&Redo ({len(self.model.subsequentViews)})') id, properties, domain, domain_kind = self.getIDinfo(event) if id != '-1': # Domain ID domainID = self.menu.addAction(f"{domain_kind} {id}") domainID.setDisabled(True) # Domain Name (if any) if domain[id].name: domainName = self.menu.addAction(domain[id].name) domainName.setDisabled(True) self.menu.addSeparator() self.menu.addAction(self.mw.undoAction) self.menu.addAction(self.mw.redoAction) self.menu.addSeparator() colorAction = self.menu.addAction(f'Edit {domain_kind} Color...') colorAction.setDisabled(self.model.currentView.highlighting) colorAction.setToolTip(f'Edit {domain_kind} color') colorAction.setStatusTip(f'Edit {domain_kind} color') colorAction.triggered.connect( lambda: self.mw.editDomainColor(domain_kind, id)) maskAction = self.menu.addAction(f'Mask {domain_kind}') maskAction.setCheckable(True) maskAction.setChecked(domain[id].masked) maskAction.setDisabled(not self.model.currentView.masking) maskAction.setToolTip(f'Toggle {domain_kind} mask') maskAction.setStatusTip(f'Toggle {domain_kind} mask') maskAction.triggered[bool].connect( lambda bool=bool: self.mw.toggleDomainMask( bool, domain_kind, id)) highlightAction = self.menu.addAction(f'Highlight {domain_kind}') highlightAction.setCheckable(True) highlightAction.setChecked(domain[id].highlighted) highlightAction.setDisabled( not self.model.currentView.highlighting) highlightAction.setToolTip(f'Toggle {domain_kind} highlight') highlightAction.setStatusTip(f'Toggle {domain_kind} highlight') highlightAction.triggered[bool].connect( lambda bool=bool: self.mw.toggleDomainHighlight( bool, domain_kind, id)) else: self.menu.addAction(self.mw.undoAction) self.menu.addAction(self.mw.redoAction) self.menu.addSeparator() bgColorAction = self.menu.addAction('Edit Background Color...') bgColorAction.setToolTip('Edit background color') bgColorAction.setStatusTip('Edit plot background color') bgColorAction.triggered.connect( lambda: self.mw.editBackgroundColor(apply=True)) self.menu.addSeparator() self.menu.addAction(self.mw.saveImageAction) self.menu.addAction(self.mw.saveViewAction) self.menu.addAction(self.mw.openAction) self.menu.addSeparator() self.menu.addMenu(self.mw.basisMenu) self.menu.addMenu(self.mw.colorbyMenu) self.menu.addSeparator() self.menu.addAction(self.mw.maskingAction) self.menu.addAction(self.mw.highlightingAct) self.menu.addSeparator() self.menu.addAction(self.mw.dockAction) self.mw.maskingAction.setChecked(self.model.currentView.masking) self.mw.highlightingAct.setChecked(self.model.currentView.highlighting) if self.mw.dock.isVisible(): self.mw.dockAction.setText('Hide &Dock') else: self.mw.dockAction.setText('Show &Dock') self.menu.exec_(event.globalPos()) def setPixmap(self, w, h): # clear out figure self.figure.clear() cv = self.model.currentView # set figure bg color to match window window_background = self.parent.palette().color( QtGui.QPalette.Background) self.figure.patch.set_facecolor( rgb_normalize(window_background.getRgb())) # set figure width self.figure.set_figwidth(0.99 * w / self.figure.get_dpi()) self.figure.set_figheight(0.99 * h / self.figure.get_dpi()) # set data extents for automatic reporting of pointer location data_bounds = [ cv.origin[self.mw.xBasis] - cv.width / 2., cv.origin[self.mw.xBasis] + cv.width / 2., cv.origin[self.mw.yBasis] - cv.height / 2., cv.origin[self.mw.yBasis] + cv.height / 2. ] # make sure we have an image to load if not hasattr(self.model, 'image'): self.model.generatePlot() c = self.figure.subplots().imshow(self.model.image, extent=data_bounds, alpha=cv.plotAlpha) self.ax = self.figure.axes[0] self.ax.margins(0.0, 0.0) self.figure.set_tight_layout({'pad': 1.0}) # set axis labels axis_label_str = "{} (cm)" self.ax.set_xlabel(axis_label_str.format(cv.basis[0])) self.ax.set_ylabel(axis_label_str.format(cv.basis[1])) self.draw()
class TreeTaskList_supervisor(TreeTaskList_coordinator): def __init__(self, parent): super(TreeTaskList_supervisor, self).__init__(parent) self.setColumnCount(3) self.setColumnHidden(2, False) self.appendSupervisorConntectMenu() def appendSupervisorConntectMenu(self): self.processMenu = QMenu("Processes", self.menu) self.proceesAddSubMenu = QMenu("Add", self.processMenu) self.processMenu.addMenu(self.proceesAddSubMenu) self.processMenu.addAction("Remove", self.removeProcess) self.menu.addMenu(self.processMenu) def callContextMenu(self, pos): self.proceesAddSubMenu.clear() self.createProcessAddMenu(self.proceesAddSubMenu) self.menu.exec_(QCursor.pos()) def createProcessAddMenu(self, processMenu): items = self.itemUtils.getSelected_shotItems() processesList = self.getProcessList(items) sKeysList = [it.data(0, Qt.UserRole) for it in items] server = self.taskManagerWdg.userServerCore.server server.start() for process in processesList: processMenu.addAction( "{}".format(process), lambda x=process: self.addTaskAction(server, sKeysList, x)) server.finish() def getProcessList(self, items): def getItemType(sKey): idx0 = sKey.find('/') idx1 = sKey.find('?') return sKey[idx0:idx1] def compareItemTypes(code, types): idx = code.find('/') processType = code[idx:] return processType in types itemTypes = [] for item in items: sKey = item.data(0, Qt.UserRole) itemTypes.append(getItemType(sKey)) itemTypes = list(set(itemTypes)) processesData = self.taskManagerWdg.getProcessesData() filteredData = list( filter(lambda x: compareItemTypes(x['code'], itemTypes), processesData)) processesList = [] for data in filteredData: processesList += data.get("processes") return processesList def addTaskAction(self, server, sKeysList, process): for sKey in sKeysList: taskData = self.taskManagerWdg.userServerCore.taskData itemData = tacticDataProcess.getTaskElementBySearchField( taskData, "__search_key__", sKey) existingProcesses = tacticDataProcess.getActiveProcessesList( [itemData]) if process in existingProcesses: continue task = tacticPostUtils.createTask(server, sKey, process) tacticPostUtils.updateSobject( server, task.get('__search_key__'), {"status": configUtils.tctStatusElements.get('assignment')}) def removeProcess(self): items = self.itemUtils.getSelected_ProcessItems(getMultiple=True) if not isinstance(items, list): items = [items] server = self.taskManagerWdg.userServerCore.server for item in items: sKey = item.data(0, Qt.UserRole) if sKey.find("task") < 0: continue print(sKey) tacticPostUtils.deleteSObject(server, sKey, True)
class DebugControlsWidget(QToolBar): def __init__(self, parent, name, data, debug_state): if not type(data) == binaryninja.binaryview.BinaryView: raise Exception('expected widget data to be a BinaryView') self.bv = data self.debug_state = debug_state QToolBar.__init__(self, parent) # TODO: Is there a cleaner way to do this? self.setStyleSheet(""" QToolButton{padding: 4px 14px 4px 14px; font-size: 14pt;} QToolButton:disabled{color: palette(alternate-base)} """) self.actionRun = QAction("Run", self) self.actionRun.triggered.connect(lambda: self.perform_run()) self.actionRestart = QAction("Restart", self) self.actionRestart.triggered.connect(lambda: self.perform_restart()) self.actionQuit = QAction("Quit", self) self.actionQuit.triggered.connect(lambda: self.perform_quit()) self.actionAttach = QAction("Attach", self) self.actionAttach.triggered.connect(lambda: self.perform_attach()) self.actionDetach = QAction("Detach", self) self.actionDetach.triggered.connect(lambda: self.perform_detach()) self.actionSettings = QAction("Settings...", self) self.actionSettings.triggered.connect(lambda: self.perform_settings()) self.actionPause = QAction("Pause", self) self.actionPause.triggered.connect(lambda: self.perform_pause()) self.actionResume = QAction("Resume", self) self.actionResume.triggered.connect(lambda: self.perform_resume()) self.actionStepIntoAsm = QAction("Step Into (Assembly)", self) self.actionStepIntoAsm.triggered.connect( lambda: self.perform_step_into_asm()) self.actionStepIntoIL = QAction("Step Into", self) self.actionStepIntoIL.triggered.connect( lambda: self.perform_step_into_il()) self.actionStepOverAsm = QAction("Step Over (Assembly)", self) self.actionStepOverAsm.triggered.connect( lambda: self.perform_step_over_asm()) self.actionStepOverIL = QAction("Step Over", self) self.actionStepOverIL.triggered.connect( lambda: self.perform_step_over_il()) self.actionStepReturn = QAction("Step Return", self) self.actionStepReturn.triggered.connect( lambda: self.perform_step_return()) # session control menu self.controlMenu = QMenu("Process Control", self) self.controlMenu.addAction(self.actionRun) self.controlMenu.addAction(self.actionRestart) self.controlMenu.addAction(self.actionQuit) self.controlMenu.addSeparator() self.controlMenu.addAction(self.actionAttach) self.controlMenu.addAction(self.actionDetach) self.controlMenu.addSeparator() self.controlMenu.addAction(self.actionSettings) self.stepIntoMenu = QMenu("Step Into", self) self.stepIntoMenu.addAction(self.actionStepIntoIL) self.stepIntoMenu.addAction(self.actionStepIntoAsm) self.stepOverMenu = QMenu("Step Over", self) self.stepOverMenu.addAction(self.actionStepOverIL) self.stepOverMenu.addAction(self.actionStepOverAsm) self.btnControl = QToolButton(self) self.btnControl.setMenu(self.controlMenu) self.btnControl.setPopupMode(QToolButton.MenuButtonPopup) self.btnControl.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.btnControl.setDefaultAction(self.actionRun) self.addWidget(self.btnControl) # execution control buttons self.addAction(self.actionPause) self.addAction(self.actionResume) self.btnStepInto = QToolButton(self) self.btnStepInto.setMenu(self.stepIntoMenu) self.btnStepInto.setPopupMode(QToolButton.MenuButtonPopup) self.btnStepInto.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.btnStepInto.setDefaultAction(self.actionStepIntoIL) self.addWidget(self.btnStepInto) self.btnStepOver = QToolButton(self) self.btnStepOver.setMenu(self.stepOverMenu) self.btnStepOver.setPopupMode(QToolButton.MenuButtonPopup) self.btnStepOver.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.btnStepOver.setDefaultAction(self.actionStepOverIL) self.addWidget(self.btnStepOver) # TODO: Step until returning from current function self.addAction(self.actionStepReturn) self.threadMenu = QMenu("Threads", self) self.btnThreads = QToolButton(self) self.btnThreads.setMenu(self.threadMenu) self.btnThreads.setPopupMode(QToolButton.InstantPopup) self.btnThreads.setToolButtonStyle(Qt.ToolButtonTextOnly) self.addWidget(self.btnThreads) self.set_thread_list([]) self.editStatus = QLineEdit('INACTIVE', self) self.editStatus.setReadOnly(True) self.editStatus.setAlignment(QtCore.Qt.AlignCenter) self.addWidget(self.editStatus) # disable buttons self.set_actions_enabled(Run=self.can_exec(), Restart=False, Quit=False, Attach=self.can_connect(), Detach=False, Pause=False, Resume=False, StepInto=False, StepOver=False, StepReturn=False) self.set_resume_pause_action("Pause") self.set_default_process_action( "Attach" if self.can_connect() else "Run") def __del__(self): # TODO: Move this elsewhere # This widget is tasked with cleaning up the state after the view is closed # binjaplug.delete_state(self.bv) pass def can_exec(self): return DebugAdapter.ADAPTER_TYPE.use_exec( self.debug_state.adapter_type) def can_connect(self): return DebugAdapter.ADAPTER_TYPE.use_connect( self.debug_state.adapter_type) def perform_run(self): def perform_run_thread(): try: self.debug_state.run() execute_on_main_thread_and_wait(perform_run_after) except ConnectionRefusedError: execute_on_main_thread_and_wait( lambda: perform_run_error('ERROR: Connection Refused')) except Exception as e: execute_on_main_thread_and_wait( lambda: perform_run_error('ERROR: ' + ' '.join(e.args))) traceback.print_exc(file=sys.stderr) def perform_run_after(): self.state_stopped() self.debug_state.ui.on_step() def perform_run_error(e): self.state_error(e) self.state_starting('STARTING') threading.Thread(target=perform_run_thread).start() def perform_restart(self): def perform_restart_thread(): try: self.debug_state.restart() execute_on_main_thread_and_wait(perform_restart_after) except ConnectionRefusedError: execute_on_main_thread_and_wait( lambda: perform_restart_error('ERROR: Connection Refused')) except Exception as e: execute_on_main_thread_and_wait(lambda: perform_restart_error( 'ERROR: ' + ' '.join(e.args))) traceback.print_exc(file=sys.stderr) def perform_restart_after(): self.state_stopped() self.debug_state.ui.on_step() def perform_restart_error(e): self.state_error(e) self.state_starting('RESTARTING') threading.Thread(target=perform_restart_thread).start() def perform_quit(self): self.debug_state.quit() self.state_inactive() self.debug_state.ui.on_step() def perform_attach(self): def perform_attach_thread(): try: self.debug_state.attach() execute_on_main_thread_and_wait(perform_attach_after) except ConnectionRefusedError: execute_on_main_thread_and_wait( lambda: perform_attach_error('ERROR: Connection Refused')) except TimeoutError: execute_on_main_thread_and_wait( lambda: perform_attach_error('ERROR: Connection Refused')) except Exception as e: execute_on_main_thread_and_wait( lambda: perform_attach_error('ERROR: ' + ' '.join(e.args))) traceback.print_exc(file=sys.stderr) def perform_attach_after(): self.state_stopped() self.debug_state.ui.on_step() def perform_attach_error(e): self.state_error(e) self.state_starting('ATTACHING') threading.Thread(target=perform_attach_thread).start() def perform_detach(self): self.debug_state.detach() self.state_inactive() self.debug_state.ui.on_step() def perform_settings(self): def settings_finished(): if self.debug_state.running: self.state_running() elif self.debug_state.connected: local_rip = self.debug_state.local_ip if self.debug_state.bv.read(local_rip, 1) and len( self.debug_state.bv.get_functions_containing( local_rip)) > 0: self.state_stopped() else: self.state_stopped_extern() else: self.state_inactive() dialog = AdapterSettingsDialog.AdapterSettingsDialog(self, self.bv) dialog.show() dialog.finished.connect(settings_finished) def perform_pause(self): self.debug_state.pause() # Don't update state here-- one of the other buttons is running in a thread and updating for us def perform_resume(self): def perform_resume_thread(): (reason, data) = self.debug_state.go() execute_on_main_thread_and_wait( lambda: perform_resume_after(reason, data)) def perform_resume_after(reason, data): self.handle_stop_return(reason, data) self.debug_state.ui.on_step() self.state_running() threading.Thread(target=perform_resume_thread).start() def perform_step_into_asm(self): def perform_step_into_asm_thread(): (reason, data) = self.debug_state.step_into() execute_on_main_thread_and_wait( lambda: perform_step_into_asm_after(reason, data)) def perform_step_into_asm_after(reason, data): self.handle_stop_return(reason, data) self.debug_state.ui.on_step() self.state_busy("STEPPING") threading.Thread(target=perform_step_into_asm_thread).start() def perform_step_into_il(self): disasm = self.debug_state.ui.debug_view.binary_editor.getDisassembly() graph_type = disasm.getGraphType() def perform_step_into_il_thread(): (reason, data) = self.debug_state.step_into(graph_type) execute_on_main_thread_and_wait( lambda: perform_step_into_il_after(reason, data)) def perform_step_into_il_after(reason, data): self.handle_stop_return(reason, data) self.debug_state.ui.on_step() self.state_busy("STEPPING") threading.Thread(target=perform_step_into_il_thread).start() def perform_step_over_asm(self): def perform_step_over_asm_thread(): (reason, data) = self.debug_state.step_over() execute_on_main_thread_and_wait( lambda: perform_step_over_asm_after(reason, data)) def perform_step_over_asm_after(reason, data): self.handle_stop_return(reason, data) self.debug_state.ui.on_step() self.state_busy("STEPPING") threading.Thread(target=perform_step_over_asm_thread).start() def perform_step_over_il(self): disasm = self.debug_state.ui.debug_view.binary_editor.getDisassembly() graph_type = disasm.getGraphType() def perform_step_over_il_thread(): (reason, data) = self.debug_state.step_over(graph_type) execute_on_main_thread_and_wait( lambda: perform_step_over_il_after(reason, data)) def perform_step_over_il_after(reason, data): self.handle_stop_return(reason, data) self.debug_state.ui.on_step() self.state_busy("STEPPING") threading.Thread(target=perform_step_over_il_thread).start() def perform_step_return(self): def perform_step_return_thread(): (reason, data) = self.debug_state.step_return() execute_on_main_thread_and_wait( lambda: perform_step_return_after(reason, data)) def perform_step_return_after(reason, data): self.handle_stop_return(reason, data) self.debug_state.ui.on_step() self.state_busy("STEPPING") threading.Thread(target=perform_step_return_thread).start() def set_actions_enabled(self, **kwargs): def enable_step_into(e): self.actionStepIntoAsm.setEnabled(e) self.actionStepIntoIL.setEnabled(e) def enable_step_over(e): self.actionStepOverAsm.setEnabled(e) self.actionStepOverIL.setEnabled(e) def enable_starting(e): self.actionRun.setEnabled(e and self.can_exec()) self.actionAttach.setEnabled(e and self.can_connect()) def enable_stopping(e): self.actionRestart.setEnabled(e) self.actionQuit.setEnabled(e) self.actionDetach.setEnabled(e) def enable_stepping(e): self.actionStepIntoAsm.setEnabled(e) self.actionStepIntoIL.setEnabled(e) self.actionStepOverAsm.setEnabled(e) self.actionStepOverIL.setEnabled(e) self.actionStepReturn.setEnabled(e) actions = { "Run": lambda e: self.actionRun.setEnabled(e), "Restart": lambda e: self.actionRestart.setEnabled(e), "Quit": lambda e: self.actionQuit.setEnabled(e), "Attach": lambda e: self.actionAttach.setEnabled(e), "Detach": lambda e: self.actionDetach.setEnabled(e), "Pause": lambda e: self.actionPause.setEnabled(e), "Resume": lambda e: self.actionResume.setEnabled(e), "StepInto": enable_step_into, "StepOver": enable_step_over, "StepReturn": lambda e: self.actionStepReturn.setEnabled(e), "Threads": lambda e: self.btnThreads.setEnabled(e), "Starting": enable_starting, "Stopping": enable_stopping, "Stepping": enable_stepping, } for (action, enabled) in kwargs.items(): actions[action](enabled) def set_default_process_action(self, action): actions = { "Run": self.actionRun, "Restart": self.actionRestart, "Quit": self.actionQuit, "Attach": self.actionAttach, "Detach": self.actionDetach, } self.btnControl.setDefaultAction(actions[action]) def set_resume_pause_action(self, action): self.actionResume.setVisible(action == "Resume") self.actionPause.setVisible(action == "Pause") def set_thread_list(self, threads): def select_thread_fn(tid): def select_thread(tid): stateObj = binjaplug.get_state(self.bv) if stateObj.state == 'STOPPED': adapter = stateObj.adapter adapter.thread_select(tid) self.debug_state.ui.context_display() else: print('cannot set thread in state %s' % stateObj.state) return lambda: select_thread(tid) self.threadMenu.clear() if len(threads) > 0: for thread in threads: item_name = "Thread {} at {}".format(thread['tid'], hex(thread['ip'])) action = self.threadMenu.addAction( item_name, select_thread_fn(thread['tid'])) if thread['selected']: self.btnThreads.setDefaultAction(action) else: defaultThreadAction = self.threadMenu.addAction("Thread List") defaultThreadAction.setEnabled(False) self.btnThreads.setDefaultAction(defaultThreadAction) def state_starting(self, msg=None): self.editStatus.setText(msg or 'INACTIVE') self.set_actions_enabled(Starting=False, Stopping=False, Stepping=False, Pause=False, Resume=False, Threads=False) self.set_default_process_action( "Attach" if self.can_connect() else "Run") self.set_thread_list([]) self.set_resume_pause_action("Pause") def state_inactive(self, msg=None): self.editStatus.setText(msg or 'INACTIVE') self.set_actions_enabled(Starting=True, Stopping=False, Stepping=False, Pause=False, Resume=False, Threads=False) self.set_default_process_action( "Attach" if self.can_connect() else "Run") self.set_thread_list([]) self.set_resume_pause_action("Pause") def state_stopped(self, msg=None): self.editStatus.setText(msg or 'STOPPED') self.set_actions_enabled(Starting=False, Stopping=True, Stepping=True, Pause=True, Resume=True, Threads=True) self.set_default_process_action("Quit") self.set_resume_pause_action("Resume") def state_stopped_extern(self, msg=None): self.editStatus.setText(msg or 'STOPPED') self.set_actions_enabled(Starting=False, Stopping=True, Stepping=True, StepReturn=False, Pause=True, Resume=True, Threads=True) self.set_default_process_action("Quit") self.set_resume_pause_action("Resume") def state_running(self, msg=None): self.editStatus.setText(msg or 'RUNNING') self.set_actions_enabled(Starting=False, Stopping=True, Stepping=False, Pause=True, Resume=False, Threads=False) self.set_default_process_action("Quit") self.set_resume_pause_action("Pause") def state_busy(self, msg=None): self.editStatus.setText(msg or 'RUNNING') self.set_actions_enabled(Starting=False, Stopping=True, Stepping=False, Pause=True, Resume=False, Threads=False) self.set_default_process_action("Quit") self.set_resume_pause_action("Pause") def state_error(self, msg=None): self.editStatus.setText(msg or 'ERROR') self.set_actions_enabled(Starting=True, Stopping=False, Pause=False, Resume=False, Stepping=False, Threads=False) self.set_default_process_action( "Attach" if self.can_connect() else "Run") self.set_thread_list([]) self.set_resume_pause_action("Resume") def handle_stop_return(self, reason, data): if reason == DebugAdapter.STOP_REASON.STDOUT_MESSAGE: self.state_stopped('stdout: ' + data) elif reason == DebugAdapter.STOP_REASON.PROCESS_EXITED: self.debug_state.quit() self.state_inactive('process exited, return code=%d' % data) elif reason == DebugAdapter.STOP_REASON.BACKEND_DISCONNECTED: self.debug_state.quit() self.state_inactive('backend disconnected (process exited?)')
class PlotImage(QLabel): def __init__(self, model, FM, parent=None): super(PlotImage, self).__init__(parent) self.model = model self.FM = FM self.mw = parent self.setAlignment(QtCore.Qt.AlignCenter) self.setMouseTracking(True) self.rubberBand = QRubberBand(QRubberBand.Rectangle, self) self.bandOrigin = QtCore.QPoint() self.xPlotOrigin = None self.yPlotOrigin = None self.menu = QMenu(self) def enterEvent(self, event): self.setCursor(QtCore.Qt.CrossCursor) self.mw.coordLabel.show() def leaveEvent(self, event): self.mw.coordLabel.hide() self.mw.statusBar().showMessage('') def mousePressEvent(self, event): # Set rubber band absolute and relative position self.bandOrigin = event.pos() self.xPlotOrigin, self.yPlotOrigin = self.getPlotCoords(event.pos()) # Create rubber band self.rubberBand.setGeometry( QtCore.QRect(self.bandOrigin, QtCore.QSize())) QLabel.mousePressEvent(self, event) def mouseDoubleClickEvent(self, event): xCenter, yCenter = self.getPlotCoords(event.pos()) self.mw.editPlotOrigin(xCenter, yCenter, apply=True) QLabel.mouseDoubleClickEvent(self, event) def mouseMoveEvent(self, event): # Show Cursor position relative to plot in status bar xPlotPos, yPlotPos = self.getPlotCoords(event.pos()) # Show Cell/Material ID, Name in status bar id, domain, domain_kind = self.getIDinfo(event) if id != '-1' and domain[id].name: domainInfo = f"{domain_kind} {id}: {domain[id].name}" elif id != '-1': domainInfo = f"{domain_kind} {id}" else: domainInfo = "" self.mw.statusBar().showMessage(f" {domainInfo}") # Update rubber band and values if mouse button held down if event.buttons() == QtCore.Qt.LeftButton: self.rubberBand.setGeometry( QtCore.QRect(self.bandOrigin, event.pos()).normalized()) # Show rubber band if both dimensions > 10 pixels if self.rubberBand.width() > 10 and self.rubberBand.height() > 10: self.rubberBand.show() else: self.rubberBand.hide() # Update plot X Origin xCenter = (self.xPlotOrigin + xPlotPos) / 2 yCenter = (self.yPlotOrigin + yPlotPos) / 2 self.mw.editPlotOrigin(xCenter, yCenter) modifiers = event.modifiers() # Zoom out if Shift held if modifiers == QtCore.Qt.ShiftModifier: cv = self.model.currentView bandwidth = abs(self.bandOrigin.x() - event.pos().x()) width = cv.width * (cv.hRes / max(bandwidth, .001)) bandheight = abs(self.bandOrigin.y() - event.pos().y()) height = cv.height * (cv.vRes / max(bandheight, .001)) else: # Zoom in width = max(abs(self.xPlotOrigin - xPlotPos), 0.1) height = max(abs(self.yPlotOrigin - yPlotPos), 0.1) self.mw.editWidth(width) self.mw.editHeight(height) def mouseReleaseEvent(self, event): if self.rubberBand.isVisible(): self.rubberBand.hide() self.mw.applyChanges() else: self.mw.revertDockControls() def wheelEvent(self, event): if event.delta() and event.modifiers() == QtCore.Qt.ShiftModifier: numDegrees = event.delta() / 8 if 24 < self.mw.zoom + numDegrees < 5001: self.mw.editZoom(self.mw.zoom + numDegrees) def contextMenuEvent(self, event): self.menu.clear() self.mw.undoAction.setText(f'&Undo ({len(self.model.previousViews)})') self.mw.redoAction.setText( f'&Redo ({len(self.model.subsequentViews)})') id, domain, domain_kind = self.getIDinfo(event) if id != '-1': # Domain ID domainID = self.menu.addAction(f"{domain_kind} {id}") domainID.setDisabled(True) # Domain Name (if any) if domain[id].name: domainName = self.menu.addAction(domain[id].name) domainName.setDisabled(True) self.menu.addSeparator() self.menu.addAction(self.mw.undoAction) self.menu.addAction(self.mw.redoAction) self.menu.addSeparator() colorAction = self.menu.addAction(f'Edit {domain_kind} Color...') colorAction.setDisabled(self.model.currentView.highlighting) colorAction.setToolTip(f'Edit {domain_kind} color') colorAction.setStatusTip(f'Edit {domain_kind} color') colorAction.triggered.connect( lambda: self.mw.editDomainColor(domain_kind, id)) maskAction = self.menu.addAction(f'Mask {domain_kind}') maskAction.setCheckable(True) maskAction.setChecked(domain[id].masked) maskAction.setDisabled(not self.model.currentView.masking) maskAction.setToolTip(f'Toggle {domain_kind} mask') maskAction.setStatusTip(f'Toggle {domain_kind} mask') maskAction.triggered[bool].connect( lambda bool=bool: self.mw.toggleDomainMask( bool, domain_kind, id)) highlightAction = self.menu.addAction(f'Highlight {domain_kind}') highlightAction.setCheckable(True) highlightAction.setChecked(domain[id].highlighted) highlightAction.setDisabled( not self.model.currentView.highlighting) highlightAction.setToolTip(f'Toggle {domain_kind} highlight') highlightAction.setStatusTip(f'Toggle {domain_kind} highlight') highlightAction.triggered[bool].connect( lambda bool=bool: self.mw.toggleDomainHighlight( bool, domain_kind, id)) else: self.menu.addAction(self.mw.undoAction) self.menu.addAction(self.mw.redoAction) self.menu.addSeparator() bgColorAction = self.menu.addAction('Edit Background Color...') bgColorAction.setToolTip('Edit background color') bgColorAction.setStatusTip('Edit plot background color') bgColorAction.triggered.connect( lambda: self.mw.editBackgroundColor(apply=True)) self.menu.addSeparator() self.menu.addAction(self.mw.saveImageAction) self.menu.addAction(self.mw.saveViewAction) self.menu.addAction(self.mw.openAction) self.menu.addSeparator() self.menu.addMenu(self.mw.basisMenu) self.menu.addMenu(self.mw.colorbyMenu) self.menu.addSeparator() self.menu.addAction(self.mw.maskingAction) self.menu.addAction(self.mw.highlightingAct) self.menu.addSeparator() self.menu.addAction(self.mw.dockAction) self.mw.maskingAction.setChecked(self.model.currentView.masking) self.mw.highlightingAct.setChecked(self.model.currentView.highlighting) if self.mw.dock.isVisible(): self.mw.dockAction.setText('Hide &Dock') else: self.mw.dockAction.setText('Show &Dock') self.menu.exec_(event.globalPos()) def getPlotCoords(self, pos): cv = self.model.currentView factor = (self.width() / cv.hRes, self.height() / cv.vRes) # Cursor position in pixels relative to center of plot image xPos = (pos.x() + 0.5) / factor[0] - cv.hRes / 2 yPos = (-pos.y() - 0.5) / factor[1] + cv.vRes / 2 # Curson position in plot coordinates xPlotCoord = (xPos / self.mw.scale[0]) + cv.origin[self.mw.xBasis] yPlotCoord = (yPos / self.mw.scale[1]) + cv.origin[self.mw.yBasis] self.mw.showCoords(xPlotCoord, yPlotCoord) return (xPlotCoord, yPlotCoord) def getIDinfo(self, event): cv = self.model.currentView factor = (self.width() / cv.hRes, self.height() / cv.vRes) xPos = int((event.pos().x() + .05) / factor[0]) yPos = int((event.pos().y() + .05) / factor[1]) if yPos < self.model.currentView.vRes \ and xPos < self.model.currentView.hRes: id = f"{self.model.ids[yPos][xPos]}" else: id = '-1' if self.model.currentView.colorby == 'cell': domain = self.model.activeView.cells domain_kind = 'Cell' else: domain = self.model.activeView.materials domain_kind = 'Material' return id, domain, domain_kind
class FileMenu(QMenu): """ A custom menu designed to fit the menubar """ def __init__(self, mainWindow, parent=None): super(FileMenu, self).__init__(parent) self.mainWindow_ = mainWindow self.setTitle("File") self.addAction("New", self.newFunc) self.openMenu = QMenu() self.openMenu.setTitle("Open...") self.refreshOpenMenu() self.addMenu(self.openMenu) self.addAction("Import...", self.importFunc) self.addAction("Save", self.saveFunc) self.addAction("Save all", self.saveAllFunc) self.addAction("Export...", self.exportFunc) self.addSeparator() self.addAction("Quit", self.quitFunc) def newFunc(self): """ Creates a new table, it opens a popup for the user to chose the type and name """ items = ["Members", "Finances"] tableType = QInputDialog.getItem(self, "New table", "table type", items, 0, False) if tableType[1]: tableName = QInputDialog.getText(self, "New table", "table name") if tableName[1]: if tableType == "Members": table = Group(tableName[1]) else: table = Table(tableName[0]) dataTable = DataTable(name=tableName[0], table=table, tableType=tableType[0]) self.mainWindow_.contentTab.addTable(dataTable) def openFunc(self, fileName): """ Open the file at the specified path The file has to be JSON (for ot it's CSV though) """ print(fileName) #TODO temporary, to be change by json call try: table, name = csvParser(fileName) except Exception as inst: message = ErrorMessage(str(inst)) message.exec_() return if isinstance(table, Group): dataTable = DataTable(name, table, "Members") elif isinstance(table, Table): dataTable = DataTable(name, table, "Tresury") else: return self.mainWindow_.contentTab.addTable(dataTable) def refreshOpenMenu(self): """ Refresh the "Open" menu so that new file can be seen To be changed when csv is dropped for internal saves """ path = dirname(dirname(dirname((abspath(__file__))))) path += "/csv_files" #TODO implement json files = csvList(path) print(files) self.openMenu.clear() for elem in files: self.openMenu.addAction(elem, lambda: self.openFunc(path + "/" + elem)) self.openMenu.addSeparator() self.openMenu.addAction("Refresh", self.refreshOpenMenu) def importFunc(self): """ Allow the user to choose in the filesystem a csv file and open it in MLE """ fileName = QFileDialog.getOpenFileName(self, "Import table", "~/../../", "CSV files (*.csv)") print(fileName[0]) if fileName[0] == "": return try: table, name = csvParser(fileName[0]) except Exception as inst: message = ErrorMessage(str(inst)) message.exec_() return if isinstance(table, Group): dataTable = DataTable(name, table, "Members") elif isinstance(table, Table): dataTable = DataTable(name, table, "Tresury") else: return self.mainWindow_.contentTab.addTable(dataTable) def saveFunc(self): """ Saves the specified Table in internal storage """ self.mainWindow_.contentTab.saveCurrent() def saveAllFunc(self): """ Saves all tables in internal storage """ self.maiWindow_.contentTab.saveAll() def exportFunc(self): """ Allow the user to choose in the filesystem a path to save a table as a csv """ fileName = QFileDialog.getSaveFileName(self, "Export table", "~/../../", "CSV files (*.csv)") if fileName[0] == "": return self.mainWindow_.contentTab.saveCurrent(fileName[0], False) def quitFunc(self): """ Quit the application after checking if all tables have been saved """ if self.mainWindow_.contentTab.checkTables(): self.mainWindow_.quit()
class FileMenu(QObject): recent_files_changed = Signal() new_document_count = 0 current_progress_obj = ShowTreeViewProgressMessage(None) load_save_mgr = None supported_file_types = ('.xml', '.rksession', '.xlsx') viewer_app = Path(get_current_modules_dir()) / KNECHT_VIEWER_BIN schnuffi_app = Path(get_current_modules_dir()) / POS_SCHNUFFI_BIN def __init__(self, ui, menu: QMenu = None): """ The File menu :param modules.gui.main_ui.KnechtWindow ui: :param menu: Menu created setup in ui file """ super(FileMenu, self).__init__(parent=ui) self.ui = ui self.view_mgr: UiViewManager = None self.menu = menu or ui.menuDatei self.menu.setEnabled(False) self.recent_menu = QMenu(_('Zuletzt geöffnet'), self.menu) self.import_menu = ImportMenu(self.ui) self.import_menu.new_model_ready.connect(self.model_loaded) self.xml_message_box = XmlFailedMsgBox(self.ui) self.setup_file_menu() QTimer.singleShot(1, self.delayed_setup) @Slot() def delayed_setup(self): """ Setup attributes that require a fully initialized ui""" self.view_mgr: UiViewManager = self.ui.view_mgr self.menu.setEnabled(True) self.load_save_mgr = SaveLoadController(self) self.load_save_mgr.model_loaded.connect(self.model_loaded) self.load_save_mgr.load_aborted.connect(self._load_aborted) @Slot(Path) def guess_open_file(self, local_file_path: Path) -> bool: if local_file_path.suffix.casefold() == '.xml': self.open_xml(local_file_path.as_posix()) return True elif local_file_path.suffix.casefold() == '.rksession': self.import_menu.open_wizard(local_file_path) return True elif local_file_path.suffix.casefold() == '.xlsx': self.import_menu.open_xlsx(local_file_path) return True return False def setup_file_menu(self): insert_before = 0 if self.ui.actionBeenden in self.menu.actions(): insert_before = self.ui.actionBeenden self.ui.actionBeenden.setIcon(IconRsc.get_icon('sad')) self.ui.actionBeenden.setShortcut(QKeySequence('Ctrl+Q')) # ---- New file ---- new_action = QAction(IconRsc.get_icon('document'), _('Neu\tStrg+N'), self.menu) new_action.setShortcut(QKeySequence('Ctrl+N')) new_action.triggered.connect(self.new_document) self.menu.insertAction(insert_before, new_action) # ---- Open ---- open_xml_action = QAction(_('Öffnen\tStrg+O'), self.menu) open_xml_action.setShortcut(QKeySequence('Ctrl+O')) open_xml_action.triggered.connect(self.open_xml) open_xml_action.setIcon(IconRsc.get_icon('folder')) self.menu.insertAction(insert_before, open_xml_action) # ---- Import Menu ---- self.menu.insertMenu(insert_before, self.import_menu) # ---- Save ---- save_xml_action = QAction(_('Speichern\tStrg+S'), self.menu) save_xml_action.setShortcut(QKeySequence('Ctrl+S')) save_xml_action.triggered.connect(self.save_xml) save_xml_action.setIcon(IconRsc.get_icon('disk')) self.menu.insertAction(insert_before, save_xml_action) save_as_action = QAction(_('Speichern unter ...\tStrg+Shift+S'), self.menu) save_as_action.setShortcut(QKeySequence('Ctrl+Shift+S')) save_as_action.triggered.connect(self.save_as_xml) save_as_action.setIcon(IconRsc.get_icon('save_alt')) self.menu.insertAction(insert_before, save_as_action) self.menu.insertSeparator(insert_before) # ---- Apps ---- start_knecht_viewer = QAction(_('KnechtViewer starten'), self.menu) start_knecht_viewer.triggered.connect(self.start_knecht_viewer) start_knecht_viewer.setIcon(IconRsc.get_icon('img')) self.menu.insertAction(insert_before, start_knecht_viewer) if not path_exists(self.viewer_app): LOGGER.info('KnechtViewer executable could not be found: %s', self.viewer_app.as_posix()) start_knecht_viewer.setEnabled(False) start_schnuffi_app = QAction(_('POS Schnuffi starten'), self.menu) start_schnuffi_app.triggered.connect(self.start_schnuffi_app) start_schnuffi_app.setIcon(IconRsc.get_icon('dog')) self.menu.insertAction(insert_before, start_schnuffi_app) if not path_exists(self.schnuffi_app): LOGGER.info('KnechtViewer executable could not be found: %s', self.schnuffi_app.as_posix()) start_schnuffi_app.setEnabled(False) img_conv = QAction(_('Bilddaten konvertieren ...')) img_conv.triggered.connect(self.convert_image_directory) img_conv.setIcon(IconRsc.get_icon('render')) self.menu.insertAction(insert_before, img_conv) material_merger = QAction('AViT Material Merger') material_merger.triggered.connect(self.start_material_merger) material_merger.setIcon(IconRsc.get_icon('options')) self.menu.insertAction(insert_before, material_merger) self.menu.insertSeparator(insert_before) # ---- Recent files menu ---- self.recent_menu.aboutToShow.connect(self.update_recent_files_menu) self.menu.insertMenu(insert_before, self.recent_menu) self.menu.insertSeparator(insert_before) def new_document(self): new_file = Path( _('Neues_Dokument_{:02d}.xml').format(self.new_document_count)) self.view_mgr.create_view(None, new_file) self.new_document_count += 1 def start_knecht_viewer(self): start_app(self.viewer_app) def start_schnuffi_app(self): start_app(self.schnuffi_app) def start_material_merger(self): material_merger = MaterialMerger(self.ui) GenericTabWidget(self.ui, material_merger) def save_xml(self): if not self.view_mgr.current_tab_is_document_tab(): return self.enable_menus(False) file = self.view_mgr.current_file() if not file or not path_exists(file): if self._ask_save_as_file(file): # User agreed to set new save file self.save_as_xml() return # User aborted self.enable_menus(True) return self.save_as_xml(file) def save_as_xml(self, file: Path = None): if not self.view_mgr.current_tab_is_document_tab(): return self.enable_menus(False) if not file: current_dir = Path(KnechtSettings.app['current_path']) file, file_type = FileDialog.save(self.ui, current_dir, file_key='xml') if not file: LOGGER.info('Save Xml File dialog canceled.') self.enable_menus(True) return file = Path(file) view = self.view_mgr.current_view() result, error = self.load_save_mgr.save(file, view) if result: LOGGER.debug('File saved: %s', file.as_posix()) self.view_mgr.tab_view_saved(file) self.ui.msg( _('Datei gespeichert:{0}{1:.3}s').format( f'\n{file.name}\n', self.load_save_mgr.last_progress_time)) else: self._save_aborted(error, file) self.enable_menus(True) def open_xml(self, file: str = None) -> None: self.enable_menus(False) if not file: file = FileDialog.open(self.ui, None, 'xml') if not file: LOGGER.info('Open Xml File dialog canceled.') self.enable_menus(True) return # Check if the file is already opened file = Path(file) if self.view_mgr.file_mgr.already_open(file): LOGGER.info('File already open.') self.enable_menus(True) return # Update treeview progress view = self.view_mgr.current_view() view.progress_msg.msg(_('Daten werden gelesen')) view.progress_msg.show_progress() self.load_save_mgr.open(file) self.enable_menus(True) @Slot(KnechtModel, Path) @Slot(KnechtModel, Path, bool) def model_loaded(self, model: KnechtModel, file: Path, reset_clean: bool = False): # Update progress view = self.view_mgr.current_view() view.progress_msg.hide_progress() # Create a new view inside a new tab or load into current view if view model is empty new_view = self.view_mgr.create_view(model, file) # Refresh model data if reset_clean: new_view.undo_stack.resetClean() self.ui.statusBar().showMessage( _('{0} in {1:.3}s geladen.').format( file.name, self.load_save_mgr.last_progress_time)) @Slot(str, Path) def _load_aborted(self, error_msg: str, file: Path): # Update progress view = self.view_mgr.current_view() view.progress_msg.hide_progress() self.xml_message_box.set_error_msg(error_msg, Path(file)) self.ui.play_warning_sound() self.xml_message_box.exec_() def _save_aborted(self, error_msg: str, file: Path): self.xml_message_box.set_error_msg(error_msg, Path(file)) self.ui.play_warning_sound() self.xml_message_box.exec_() def enable_menus(self, enabled: bool = True): for a in self.menu.actions(): a.setEnabled(enabled) self.recent_menu.setEnabled(enabled) def _open_recent_xml_file(self): recent_action = self.sender() self.open_xml(recent_action.file) def _open_recent_xlsx_file(self): recent_action = self.sender() self.import_menu.open_xlsx(recent_action.file) def _open_recent_rksession(self): recent_action = self.sender() self.import_menu.open_wizard(recent_action.file) def _ask_save_as_file(self, file: Path): """ User hits save but file to save does not exist yet """ msg_box = AskToContinue(self.ui) if not msg_box.ask( _('Zieldatei zum Speichern festlegen?'), _('Die Datei: <i>{}</i><br>' 'Pfad: <i>{}</i><br>' 'wurde entfernt oder existiert nicht mehr.<br><br>' 'Neue Zieldatei zum Speichern festlegen?' '').format(file.name, file.parent.as_posix()), _('Speichern unter..')): # User wants to abort save as return False return True def convert_image_directory(self): img_dir = FileDialog.open_dir(self.ui, None) if not img_dir or not path_exists(img_dir): self.ui.msg( _('Zu konvertierendes Verzeichnis ist nicht erreichbar.'), 8000) return img_dir = Path(img_dir) out_dir = img_dir / 'converted' try: out_dir.mkdir(exist_ok=True) except Exception as e: self.ui.msg( _('Konnte Bild Konvertierung Ausgabeverzeichnis nicht erstellen.' ), 8000) LOGGER.warning(e) img_converter = KnechtImage(self) img_converter.conversion_result.connect(self._conversion_result) if img_converter.convert_directory(img_dir, out_dir): self.ui.msg( _('Bildkonvertierung gestartet.<br /><i>{}</i>').format( img_dir), 5000) else: self.ui.msg( _('Bildkonvertierung konnte nicht gestartet werden. Keine konvertierbaren Dateien gefunden.' ), 10000) def _conversion_result(self, result: str): result = result.replace('\n', '<br />') title = _("Ergebnis der Bildkonvertierung:") self.ui.overlay.display_confirm(f'<b>{title}</b><br />{result}', (('[X]', None), )) def update_recent_files_menu(self): self.recent_menu.clear() if not len(KnechtSettings.app['recent_files']): no_entries_dummy = QAction(_("Keine Einträge vorhanden"), self.recent_menu) no_entries_dummy.setEnabled(False) self.recent_menu.addAction(no_entries_dummy) for idx, entry in enumerate(KnechtSettings.app['recent_files']): if idx >= 20: break file, file_type = entry file_name = Path(file).stem if not path_exists(file): # Skip and remove non existing files KnechtSettings.app['recent_files'].pop(idx) continue recent_action = QAction(f'{file_name} - {file_type}', self.recent_menu) recent_action.file = Path(file) if file_type == 'xml': recent_action.setText(f'{file_name} - Xml Presets') recent_action.setIcon(IconRsc.get_icon('document')) recent_action.triggered.connect(self._open_recent_xml_file) elif file_type == 'xlsx': recent_action.setText(f'{file_name} - Excel Import') recent_action.setIcon(IconRsc.get_icon('excel')) recent_action.triggered.connect(self._open_recent_xlsx_file) elif file_type == 'rksession': recent_action.setText(f'{file_name} - Preset Wizard Session') recent_action.setIcon(IconRsc.get_icon('qub_button')) recent_action.triggered.connect(self._open_recent_rksession) self.recent_menu.addAction(recent_action) self.recent_files_changed.emit()
class MainWindow(QMainWindow): def __init__(self, parent=None): super().__init__(parent) self.setWindowIcon(QIcon(":/icons/apps/16/tabulator.svg")) self._recentDocuments = [] self._actionRecentDocuments = [] self._keyboardShortcutsDialog = None self._preferences = Preferences() self._preferences.loadSettings() self._createActions() self._createMenus() self._createToolBars() self._loadSettings() self._updateActions() self._updateActionFullScreen() self._updateMenuOpenRecent() # Central widget self._documentArea = QMdiArea() self._documentArea.setViewMode(QMdiArea.TabbedView) self._documentArea.setTabsMovable(True) self._documentArea.setTabsClosable(True) self.setCentralWidget(self._documentArea) self._documentArea.subWindowActivated.connect(self._onDocumentWindowActivated) def closeEvent(self, event): if True: # Store application properties and preferences self._saveSettings() self._preferences.saveSettings() event.accept() else: event.ignore() def _loadSettings(self): settings = QSettings() # Recent documents size = settings.beginReadArray("RecentDocuments") for idx in range(size-1, -1, -1): settings.setArrayIndex(idx) canonicalName = QFileInfo(settings.value("Document")).canonicalFilePath() self._updateRecentDocuments(canonicalName) settings.endArray() # Application properties: Geometry geometry = settings.value("Application/Geometry", QByteArray()) if self._preferences.restoreApplicationGeometry() else QByteArray() if not geometry.isEmpty(): self.restoreGeometry(geometry) else: availableGeometry = self.screen().availableGeometry() self.resize(availableGeometry.width() * 2/3, availableGeometry.height() * 2/3) self.move((availableGeometry.width() - self.width()) / 2, (availableGeometry.height() - self.height()) / 2) # Application properties: State state = settings.value("Application/State", QByteArray()) if self._preferences.restoreApplicationState() else QByteArray() if not state.isEmpty(): self.restoreState(state) else: self._toolbarApplication.setVisible(True) self._toolbarDocument.setVisible(True) self._toolbarEdit.setVisible(True) self._toolbarTools.setVisible(True) self._toolbarView.setVisible(False) self._toolbarHelp.setVisible(False) def _saveSettings(self): settings = QSettings() # Recent documents if not self._preferences.restoreRecentDocuments(): self._recentDocuments.clear() settings.remove("RecentDocuments") settings.beginWriteArray("RecentDocuments") for idx in range(len(self._recentDocuments)): settings.setArrayIndex(idx) settings.setValue("Document", self._recentDocuments[idx]) settings.endArray() # Application properties: Geometry geometry = self.saveGeometry() if self._preferences.restoreApplicationGeometry() else QByteArray() settings.setValue("Application/Geometry", geometry) # Application properties: State state = self.saveState() if self._preferences.restoreApplicationState() else QByteArray() settings.setValue("Application/State", state) def _createActions(self): # # Actions: Application self._actionAbout = QAction(self.tr("About {0}").format(QApplication.applicationName()), self) self._actionAbout.setObjectName("actionAbout") self._actionAbout.setIcon(QIcon(":/icons/apps/16/tabulator.svg")) self._actionAbout.setIconText(self.tr("About")) self._actionAbout.setToolTip(self.tr("Brief description of the application")) self._actionAbout.triggered.connect(self._onActionAboutTriggered) self._actionColophon = QAction(self.tr("Colophon"), self) self._actionColophon.setObjectName("actionColophon") self._actionColophon.setToolTip(self.tr("Lengthy description of the application")) self._actionColophon.triggered.connect(self._onActionColophonTriggered) self._actionPreferences = QAction(self.tr("Preferences…"), self) self._actionPreferences.setObjectName("actionPreferences") self._actionPreferences.setIcon(QIcon.fromTheme("configure", QIcon(":/icons/actions/16/application-configure.svg"))) self._actionPreferences.setToolTip(self.tr("Customize the appearance and behavior of the application")) self._actionPreferences.triggered.connect(self._onActionPreferencesTriggered) self._actionQuit = QAction(self.tr("Quit"), self) self._actionQuit.setObjectName("actionQuit") self._actionQuit.setIcon(QIcon.fromTheme("application-exit", QIcon(":/icons/actions/16/application-exit.svg"))) self._actionQuit.setShortcut(QKeySequence.Quit) self._actionQuit.setToolTip(self.tr("Quit the application")) self._actionQuit.triggered.connect(self.close) # # Actions: Document self._actionNew = QAction(self.tr("New"), self) self._actionNew.setObjectName("actionNew") self._actionNew.setIcon(QIcon.fromTheme("document-new", QIcon(":/icons/actions/16/document-new.svg"))) self._actionNew.setShortcut(QKeySequence.New) self._actionNew.setToolTip(self.tr("Create new document")) self._actionNew.triggered.connect(self._onActionNewTriggered) self._actionOpen = QAction(self.tr("Open…"), self) self._actionOpen.setObjectName("actionOpen") self._actionOpen.setIcon(QIcon.fromTheme("document-open", QIcon(":/icons/actions/16/document-open.svg"))) self._actionOpen.setShortcut(QKeySequence.Open) self._actionOpen.setToolTip(self.tr("Open an existing document")) self._actionOpen.triggered.connect(self._onActionOpenTriggered) self._actionOpenRecentClear = QAction(self.tr("Clear List"), self) self._actionOpenRecentClear.setObjectName("actionOpenRecentClear") self._actionOpenRecentClear.setToolTip(self.tr("Clear document list")) self._actionOpenRecentClear.triggered.connect(self._onActionOpenRecentClearTriggered) self._actionSave = QAction(self.tr("Save"), self) self._actionSave.setObjectName("actionSave") self._actionSave.setIcon(QIcon.fromTheme("document-save", QIcon(":/icons/actions/16/document-save.svg"))) self._actionSave.setShortcut(QKeySequence.Save) self._actionSave.setToolTip(self.tr("Save document")) self._actionSave.triggered.connect(self._onActionSaveTriggered) self._actionSaveAs = QAction(self.tr("Save As…"), self) self._actionSaveAs.setObjectName("actionSaveAs") self._actionSaveAs.setIcon(QIcon.fromTheme("document-save-as", QIcon(":/icons/actions/16/document-save-as.svg"))) self._actionSaveAs.setShortcut(QKeySequence.SaveAs) self._actionSaveAs.setToolTip(self.tr("Save document under a new name")) self._actionSaveAs.triggered.connect(self._onActionSaveAsTriggered) self._actionSaveAsDelimiterColon = QAction(self.tr("Colon"), self) self._actionSaveAsDelimiterColon.setObjectName("actionSaveAsDelimiterColon") self._actionSaveAsDelimiterColon.setCheckable(True) self._actionSaveAsDelimiterColon.setToolTip(self.tr("Save document with colon as delimiter under a new name")) self._actionSaveAsDelimiterColon.setData("colon") self._actionSaveAsDelimiterColon.triggered.connect(lambda: self._onActionSaveAsDelimiterTriggered("colon") ) self._actionSaveAsDelimiterComma = QAction(self.tr("Comma"), self) self._actionSaveAsDelimiterComma.setObjectName("actionSaveAsDelimiterComma") self._actionSaveAsDelimiterComma.setCheckable(True) self._actionSaveAsDelimiterComma.setToolTip(self.tr("Save document with comma as delimiter under a new name")) self._actionSaveAsDelimiterComma.setData("comma") self._actionSaveAsDelimiterComma.triggered.connect(lambda: self._onActionSaveAsDelimiterTriggered("comma") ) self._actionSaveAsDelimiterSemicolon = QAction(self.tr("Semicolon"), self) self._actionSaveAsDelimiterSemicolon.setObjectName("actionSaveAsDelimiterSemicolon") self._actionSaveAsDelimiterSemicolon.setCheckable(True) self._actionSaveAsDelimiterSemicolon.setToolTip(self.tr("Save document with semicolon as delimiter under a new name")) self._actionSaveAsDelimiterSemicolon.setData("semicolon") self._actionSaveAsDelimiterSemicolon.triggered.connect(lambda: self._onActionSaveAsDelimiterTriggered("semicolon") ) self._actionSaveAsDelimiterTab = QAction(self.tr("Tab"), self) self._actionSaveAsDelimiterTab.setObjectName("actionSaveAsDelimiterTab") self._actionSaveAsDelimiterTab.setCheckable(True) self._actionSaveAsDelimiterTab.setToolTip(self.tr("Save document with tab as delimiter under a new name")) self._actionSaveAsDelimiterTab.setData("tab") self._actionSaveAsDelimiterTab.triggered.connect(lambda: self._onActionSaveAsDelimiterTriggered("tab") ) self._actionSaveAsDelimiter = QActionGroup(self) self._actionSaveAsDelimiter.setObjectName("actionSaveAsDelimiter") self._actionSaveAsDelimiter.addAction(self._actionSaveAsDelimiterColon) self._actionSaveAsDelimiter.addAction(self._actionSaveAsDelimiterComma) self._actionSaveAsDelimiter.addAction(self._actionSaveAsDelimiterSemicolon) self._actionSaveAsDelimiter.addAction(self._actionSaveAsDelimiterTab) self._actionSaveCopyAs = QAction(self.tr("Save Copy As…"), self) self._actionSaveCopyAs.setObjectName("actionSaveCopyAs") self._actionSaveCopyAs.setIcon(QIcon.fromTheme("document-save-as", QIcon(":/icons/actions/16/document-save-as.svg"))) self._actionSaveCopyAs.setToolTip(self.tr("Save copy of document under a new name")) self._actionSaveCopyAs.triggered.connect(self._onActionSaveCopyAsTriggered) self._actionSaveAll = QAction(self.tr("Save All"), self) self._actionSaveAll.setObjectName("actionSaveAll") self._actionSaveAll.setIcon(QIcon.fromTheme("document-save-all", QIcon(":/icons/actions/16/document-save-all.svg"))) self._actionSaveAll.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_L)) self._actionSaveAll.setToolTip(self.tr("Save all documents")) self._actionSaveAll.triggered.connect(self._onActionSaveAllTriggered) self._actionClose = QAction(self.tr("Close"), self) self._actionClose.setObjectName("actionClose") self._actionClose.setIcon(QIcon.fromTheme("document-close", QIcon(":/icons/actions/16/document-close.svg"))) self._actionClose.setShortcut(QKeySequence.Close) self._actionClose.setToolTip(self.tr("Close document")) self._actionClose.triggered.connect(self._onActionCloseTriggered) self._actionCloseOther = QAction(self.tr("Close Other"), self) self._actionCloseOther.setObjectName("actionCloseOther") self._actionCloseOther.setToolTip(self.tr("Close all other documents")) self._actionCloseOther.triggered.connect(self._onActionCloseOtherTriggered) self._actionCloseAll = QAction(self.tr("Close All"), self) self._actionCloseAll.setObjectName("actionCloseAll") self._actionCloseAll.setShortcut(QKeySequence(Qt.CTRL + Qt.SHIFT + Qt.Key_W)) self._actionCloseAll.setToolTip(self.tr("Close all documents")) self._actionCloseAll.triggered.connect(self._onActionCloseAllTriggered) # # Actions: View self._actionFullScreen = QAction(self) self._actionFullScreen.setObjectName("actionFullScreen") self._actionFullScreen.setIconText(self.tr("Full Screen")) self._actionFullScreen.setCheckable(True) self._actionFullScreen.setShortcuts([QKeySequence(Qt.Key_F11), QKeySequence.FullScreen]) self._actionFullScreen.triggered.connect(self._onActionFullScreenTriggered) self._actionTitlebarFullPath = QAction(self.tr("Show Path in Titlebar"), self) self._actionTitlebarFullPath.setObjectName("actionTitlebarFullPath") self._actionTitlebarFullPath.setCheckable(True) self._actionTitlebarFullPath.setChecked(True) self._actionTitlebarFullPath.setToolTip(self.tr("Display the full path of the document in the titlebar")) self._actionTitlebarFullPath.triggered.connect(self._onActionTitlebarFullPathTriggered) self._actionToolbarApplication = QAction(self.tr("Show Application Toolbar"), self) self._actionToolbarApplication.setObjectName("actionToolbarApplication") self._actionToolbarApplication.setCheckable(True) self._actionToolbarApplication.setToolTip(self.tr("Display the Application toolbar")) self._actionToolbarApplication.toggled.connect(lambda checked: self._toolbarApplication.setVisible(checked)) self._actionToolbarDocument = QAction(self.tr("Show Document Toolbar"), self) self._actionToolbarDocument.setObjectName("actionToolbarDocument") self._actionToolbarDocument.setCheckable(True) self._actionToolbarDocument.setToolTip(self.tr("Display the Document toolbar")) self._actionToolbarDocument.toggled.connect(lambda checked: self._toolbarDocument.setVisible(checked)) self._actionToolbarEdit = QAction(self.tr("Show Edit Toolbar"), self) self._actionToolbarEdit.setObjectName("actionToolbarEdit") self._actionToolbarEdit.setCheckable(True) self._actionToolbarEdit.setToolTip(self.tr("Display the Edit toolbar")) self._actionToolbarEdit.toggled.connect(lambda checked: self._toolbarEdit.setVisible(checked)) self._actionToolbarTools = QAction(self.tr("Show Tools Toolbar"), self) self._actionToolbarTools.setObjectName("actionToolbarTools") self._actionToolbarTools.setCheckable(True) self._actionToolbarTools.setToolTip(self.tr("Display the Tools toolbar")) self._actionToolbarTools.toggled.connect(lambda checked: self._toolbarTools.setVisible(checked)) self._actionToolbarView = QAction(self.tr("Show View Toolbar"), self) self._actionToolbarView.setObjectName("actionToolbarView") self._actionToolbarView.setCheckable(True) self._actionToolbarView.setToolTip(self.tr("Display the View toolbar")) self._actionToolbarView.toggled.connect(lambda checked: self._toolbarView.setVisible(checked)) self._actionToolbarHelp = QAction(self.tr("Show Help Toolbar"), self) self._actionToolbarHelp.setObjectName("actionToolbarHelp") self._actionToolbarHelp.setCheckable(True) self._actionToolbarHelp.setToolTip(self.tr("Display the Help toolbar")) self._actionToolbarHelp.toggled.connect(lambda checked: self._toolbarHelp.setVisible(checked)) # # Actions: Help self._actionKeyboardShortcuts = QAction(self.tr("Keyboard Shortcuts"), self) self._actionKeyboardShortcuts.setObjectName("actionKeyboardShortcuts") self._actionKeyboardShortcuts.setIcon(QIcon.fromTheme("help-keyboard-shortcuts", QIcon(":/icons/actions/16/help-keyboard-shortcuts.svg"))) self._actionKeyboardShortcuts.setIconText(self.tr("Shortcuts")) self._actionKeyboardShortcuts.setToolTip(self.tr("List of all keyboard shortcuts")) self._actionKeyboardShortcuts.triggered.connect(self._onActionKeyboardShortcutsTriggered) def _createMenus(self): # Menu: Application menuApplication = self.menuBar().addMenu(self.tr("Application")) menuApplication.setObjectName("menuApplication") menuApplication.addAction(self._actionAbout) menuApplication.addAction(self._actionColophon) menuApplication.addSeparator() menuApplication.addAction(self._actionPreferences) menuApplication.addSeparator() menuApplication.addAction(self._actionQuit) # # Menu: Document self._menuOpenRecent = QMenu(self.tr("Open Recent"), self) self._menuOpenRecent.setObjectName("menuOpenRecent") self._menuOpenRecent.setIcon(QIcon.fromTheme("document-open-recent", QIcon(":/icons/actions/16/document-open-recent.svg"))) self._menuOpenRecent.setToolTip(self.tr("Open a document which was recently opened")) self._menuSaveAsDelimiter = QMenu(self.tr("Save As with Delimiter…"), self) self._menuSaveAsDelimiter.setObjectName("menuSaveAsDelimiter") self._menuSaveAsDelimiter.setIcon(QIcon.fromTheme("document-save-as", QIcon(":/icons/actions/16/document-save-as.svg"))) self._menuSaveAsDelimiter.setToolTip(self.tr("Save document with specific delimiter under a new name")) self._menuSaveAsDelimiter.addActions(self._actionSaveAsDelimiter.actions()) menuDocument = self.menuBar().addMenu(self.tr("Document")) menuDocument.setObjectName("menuDocument") menuDocument.addAction(self._actionNew) menuDocument.addSeparator() menuDocument.addAction(self._actionOpen) menuDocument.addMenu(self._menuOpenRecent) menuDocument.addSeparator() menuDocument.addAction(self._actionSave) menuDocument.addAction(self._actionSaveAs) menuDocument.addMenu(self._menuSaveAsDelimiter) menuDocument.addAction(self._actionSaveCopyAs) menuDocument.addAction(self._actionSaveAll) menuDocument.addSeparator() menuDocument.addAction(self._actionClose) menuDocument.addAction(self._actionCloseOther) menuDocument.addAction(self._actionCloseAll) # Menu: Edit menuEdit = self.menuBar().addMenu(self.tr("Edit")) menuEdit.setObjectName("menuEdit") # Menu: Tools menuTools = self.menuBar().addMenu(self.tr("Tools")) menuTools.setObjectName("menuTools") # Menu: View menuView = self.menuBar().addMenu(self.tr("View")) menuView.setObjectName("menuView") menuView.addAction(self._actionFullScreen) menuView.addSeparator() menuView.addAction(self._actionTitlebarFullPath) menuView.addSeparator() menuView.addAction(self._actionToolbarApplication) menuView.addAction(self._actionToolbarDocument) menuView.addAction(self._actionToolbarEdit) menuView.addAction(self._actionToolbarTools) menuView.addAction(self._actionToolbarView) menuView.addAction(self._actionToolbarHelp) # Menu: Help menuHelp = self.menuBar().addMenu(self.tr("Help")) menuHelp.setObjectName("menuHelp") menuHelp.addAction(self._actionKeyboardShortcuts) def _createToolBars(self): # Toolbar: Application self._toolbarApplication = self.addToolBar(self.tr("Application Toolbar")) self._toolbarApplication.setObjectName("toolbarApplication") self._toolbarApplication.addAction(self._actionAbout) self._toolbarApplication.addAction(self._actionPreferences) self._toolbarApplication.addSeparator() self._toolbarApplication.addAction(self._actionQuit) self._toolbarApplication.visibilityChanged.connect(lambda visible: self._actionToolbarApplication.setChecked(visible)) # Toolbar: Document self._toolbarDocument = self.addToolBar(self.tr("Document Toolbar")) self._toolbarDocument.setObjectName("toolbarDocument") self._toolbarDocument.addAction(self._actionNew) self._toolbarDocument.addAction(self._actionOpen) self._toolbarDocument.addSeparator() self._toolbarDocument.addAction(self._actionSave) self._toolbarDocument.addAction(self._actionSaveAs) self._toolbarDocument.addSeparator() self._toolbarDocument.addAction(self._actionClose) self._toolbarDocument.visibilityChanged.connect(lambda visible: self._actionToolbarDocument.setChecked(visible)) # Toolbar: Edit self._toolbarEdit = self.addToolBar(self.tr("Edit Toolbar")) self._toolbarEdit.setObjectName("toolbarEdit") self._toolbarEdit.visibilityChanged.connect(lambda visible: self._actionToolbarEdit.setChecked(visible)) # Toolbar: Tools self._toolbarTools = self.addToolBar(self.tr("Tools Toolbar")) self._toolbarTools.setObjectName("toolbarTools") self._toolbarTools.visibilityChanged.connect(lambda visible: self._actionToolbarTools.setChecked(visible)) # Toolbar: View self._toolbarView = self.addToolBar(self.tr("View Toolbar")) self._toolbarView.setObjectName("toolbarView") self._toolbarView.addAction(self._actionFullScreen) self._toolbarView.visibilityChanged.connect(lambda visible: self._actionToolbarView.setChecked(visible)) # Toolbar: Help self._toolbarHelp = self.addToolBar(self.tr("Help Toolbar")) self._toolbarHelp.setObjectName("toolbarHelp") self._toolbarHelp.addAction(self._actionKeyboardShortcuts) self._toolbarHelp.visibilityChanged.connect(lambda visible: self._actionToolbarHelp.setChecked(visible)) def _updateActions(self, subWindowCount=0): hasDocument = subWindowCount >= 1 hasDocuments = subWindowCount >= 2 # Actions: Document self._actionSave.setEnabled(hasDocument) self._actionSaveAs.setEnabled(hasDocument) self._menuSaveAsDelimiter.setEnabled(hasDocument) self._actionSaveCopyAs.setEnabled(hasDocument) self._actionSaveAll.setEnabled(hasDocument) self._actionClose.setEnabled(hasDocument) self._actionCloseOther.setEnabled(hasDocuments) self._actionCloseAll.setEnabled(hasDocument) def _updateActionFullScreen(self): if not self.isFullScreen(): self._actionFullScreen.setText(self.tr("Full Screen Mode")) self._actionFullScreen.setIcon(QIcon.fromTheme("view-fullscreen", QIcon(":/icons/actions/16/view-fullscreen.svg"))) self._actionFullScreen.setChecked(False) self._actionFullScreen.setToolTip(self.tr("Display the window in full screen")) else: self._actionFullScreen.setText(self.tr("Exit Full Screen Mode")) self._actionFullScreen.setIcon(QIcon.fromTheme("view-restore", QIcon(":/icons/actions/16/view-restore.svg"))) self._actionFullScreen.setChecked(True) self._actionFullScreen.setToolTip(self.tr("Exit the full screen mode")) def _updateActionRecentDocuments(self): # Add items to the list, if necessary for idx in range(len(self._actionRecentDocuments)+1, self._preferences.maximumRecentDocuments()+1): actionRecentDocument = QAction(self) actionRecentDocument.setObjectName(f"actionRecentDocument_{idx}") actionRecentDocument.triggered.connect(lambda data=actionRecentDocument.data(): self._onActionOpenRecentDocumentTriggered(data)) self._actionRecentDocuments.append(actionRecentDocument) # Remove items from the list, if necessary while len(self._actionRecentDocuments) > self._preferences.maximumRecentDocuments(): self._actionRecentDocuments.pop() # Update items for idx in range(len(self._actionRecentDocuments)): text = None data = None show = False if idx < len(self._recentDocuments): text = self.tr("{0} [{1}]").format(QFileInfo(self._recentDocuments[idx]).fileName(), self._recentDocuments[idx]) data = self._recentDocuments[idx] show = True self._actionRecentDocuments[idx].setText(text) self._actionRecentDocuments[idx].setData(data) self._actionRecentDocuments[idx].setVisible(show) def _updateMenuOpenRecent(self): self._menuOpenRecent.clear() if self._preferences.maximumRecentDocuments() > 0: # Document list wanted; show the menu self._menuOpenRecent.menuAction().setVisible(True) if len(self._recentDocuments) > 0: # Document list has items; enable the menu self._menuOpenRecent.setEnabled(True) self._menuOpenRecent.addActions(self._actionRecentDocuments) self._menuOpenRecent.addSeparator() self._menuOpenRecent.addAction(self._actionOpenRecentClear) else: # Document list is empty; disable the menu self._menuOpenRecent.setEnabled(False) else: # No document list wanted; hide the menu self._menuOpenRecent.menuAction().setVisible(False) def _updateTitleBar(self): title = None document = self._activeDocument() if document: title = document.canonicalName() if self._actionTitlebarFullPath.isChecked() and document.canonicalName() else document.documentTitle() self.setWindowTitle(title) def _onActionAboutTriggered(self): dialog = AboutDialog(self) dialog.exec_() def _onActionColophonTriggered(self): dialog = ColophonDialog(self) dialog.exec_() def _onActionPreferencesTriggered(self): dialog = PreferencesDialog(self) dialog.setPreferences(self._preferences) dialog.exec_() self._preferences = dialog.preferences() self._updateRecentDocuments(None) self._updateMenuOpenRecent() def _onActionNewTriggered(self): self._loadDocument("") def _onActionOpenTriggered(self): fileNames = QFileDialog.getOpenFileNames(self, self.tr("Open Document"), QStandardPaths.writableLocation(QStandardPaths.HomeLocation), self.tr("CSV Files (*.csv);;All Files (*.*)"))[0] for fileName in fileNames: self._openDocument(fileName) def _onActionOpenRecentDocumentTriggered(self, canonicalName): pass # self.openDocument(canonicalName) def _onActionOpenRecentClearTriggered(self): self._recentDocuments.clear() self._updateRecentDocuments(None) self._updateMenuOpenRecent() def _onActionSaveTriggered(self): pass def _onActionSaveAsTriggered(self): pass def _onActionSaveAsDelimiterTriggered(self, delimiter): pass def _onActionSaveCopyAsTriggered(self): pass def _onActionSaveAllTriggered(self): pass def _onActionCloseTriggered(self): self._documentArea.closeActiveSubWindow() def _onActionCloseOtherTriggered(self): for subWindow in self._documentArea.subWindowList(): if subWindow != self._documentArea.activeSubWindow(): subWindow.close() def _onActionCloseAllTriggered(self): self._documentArea.closeAllSubWindows() def _onActionFullScreenTriggered(self): if not self.isFullScreen(): self.setWindowState(self.windowState() | Qt.WindowFullScreen) else: self.setWindowState(self.windowState() & ~Qt.WindowFullScreen) self._updateActionFullScreen() def _onActionTitlebarFullPathTriggered(self): self._updateTitleBar() def _onActionKeyboardShortcutsTriggered(self): if not self._keyboardShortcutsDialog: self._keyboardShortcutsDialog = KeyboardShortcutsDialog(self) self._keyboardShortcutsDialog.show() self._keyboardShortcutsDialog.raise_() self._keyboardShortcutsDialog.activateWindow() def _onDocumentWindowActivated(self, subWindow): # Update the application window self._updateActions(len(self._documentArea.subWindowList())) self._updateTitleBar() if not subWindow: return def _onDocumentAboutToClose(self, canonicalName): # Workaround to show subwindows always maximized for subWindow in self._documentArea.subWindowList(): if not subWindow.isMaximized(): subWindow.showMaximized() # Update menu items without the emitter self._updateActions(len(self._documentArea.subWindowList()) - 1) def _createDocument(self): document = Document() document.setPreferences(self._preferences) document.aboutToClose.connect(self._onDocumentAboutToClose) subWindow = self._documentArea.addSubWindow(document) subWindow.setWindowIcon(QIcon()) subWindow.showMaximized() return document def _createDocumentIndex(self, canonicalName): fileName = QFileInfo(canonicalName).fileName() canonicalIndex = 0 for subWindow in self._documentArea.subWindowList(): if QFileInfo(subWindow.widget().canonicalName()).fileName() == fileName: if subWindow.widget().canonicalIndex() > canonicalIndex: canonicalIndex = subWindow.widget().canonicalIndex() return canonicalIndex + 1 def _findDocumentWindow(self, canonicalName): for subWindow in self._documentArea.subWindowList(): if subWindow.widget().canonicalName() == canonicalName: return subWindow return None def _activeDocument(self): subWindow = self._documentArea.activeSubWindow() return subWindow.widget() if subWindow else None def _openDocument(self, fileName): canonicalName = QFileInfo(fileName).canonicalFilePath() subWindow = self._findDocumentWindow(canonicalName) if subWindow: # Given document is already loaded; activate the subwindow self._documentArea.setActiveSubWindow(subWindow) # Update list of recent documents self._updateRecentDocuments(canonicalName) self._updateMenuOpenRecent() return True return self._loadDocument(canonicalName); def _loadDocument(self, canonicalName): document = self._createDocument() succeeded = document.load(canonicalName) if succeeded: document.setCanonicalIndex(self._createDocumentIndex(canonicalName)) document.updateDocumentTitle() document.show() # Update list of recent documents self._updateRecentDocuments(canonicalName) self._updateMenuOpenRecent() # Update the application window self._updateActions(len(self._documentArea.subWindowList())) self._updateTitleBar() else: document.close() return succeeded def _updateRecentDocuments(self, canonicalName): if canonicalName: while canonicalName in self._recentDocuments: self._recentDocuments.remove(canonicalName) self._recentDocuments.insert(0, canonicalName) # Remove items from the list, if necessary while len(self._recentDocuments) > self._preferences.maximumRecentDocuments(): self._recentDocuments.pop() self._updateActionRecentDocuments()
class Mixin: def make_file_actions(self): self.file_new_action = make_action( self, get_icon(DOCUMENT_NEW_SVG), '&New...', self.file_new, QKeySequence.New, f'Create a new SQLite or {APPNAME} database') self.file_open_action = make_action( self, get_icon(DOCUMENT_OPEN_SVG), '&Open...', self.file_open, QKeySequence.Open, f'Open an existing SQLite or {APPNAME} database') self.file_open_recent_action = make_action(self, get_icon(DOCUMENT_OPEN_SVG), 'Open &Recent') self.file_open_recent_menu = QMenu(self) self.file_open_recent_action.setMenu(self.file_open_recent_menu) self.file_save_action = make_action( self, get_icon(FILESAVE_SVG), '&Save', self.file_save, QKeySequence.Save, 'Save any unsaved changes to the database') self.file_saveas_action = make_action( self, get_icon(FILESAVEAS_SVG), 'Save &As...', self.file_saveas, QKeySequence.SaveAs, 'Save the database under a new name and ' 'open the database with the new name') self.file_backup_action = make_action( self, get_icon(FILESAVEAS_SVG), '&Backup...', self.file_backup, tip='Save a copy of the database') self.file_import_action = make_action( self, get_icon(IMPORT_SVG), '&Import...', self.file_import, tip='Import an external data file (e.g., CSV) as a new ' 'table in the current database') self.file_export_action = make_action( self, get_icon(EXPORT_SVG), '&Export...', self.file_export, tip="Export a table, view or SELECT query's data as an " 'external data file (e.g., CSV)') self.file_quit_action = make_action( self, get_icon(SHUTDOWN_SVG), '&Quit', self.close, QKeySequence(Qt.CTRL + Qt.Key_Q), 'Save any unsaved changes and quit') @property def file_actions_for_menu(self): return (self.file_new_action, self.file_open_action, self.file_open_recent_action, self.file_save_action, self.file_saveas_action, self.file_backup_action, None, self.file_import_action, self.file_export_action, None, self.file_quit_action) @property def file_actions_for_toolbar(self): return (self.file_new_action, self.file_open_action, self.file_save_action, None, self.file_import_action, self.file_export_action) def file_update_ui(self): enable = bool(self.db) for action in (self.file_save_action, self.file_saveas_action, self.file_backup_action, self.file_export_action): action.setEnabled(enable) QTimer.singleShot(0, self.file_populate_open_recent_menu) def file_populate_open_recent_menu(self): self.file_open_recent_menu.clear() filenames = [str(filename) for filename in list(self.recent_files)] self.file_open_recent_menu.setEnabled(bool(filenames)) icon = get_icon(DOCUMENT_OPEN_SVG) for i, filename in enumerate(filenames, 1): action = make_action( self, icon, '&{} {}'.format(i, filename), lambda *_, filename=filename: self.file_load(filename), tip=f'Open {filename}') self.file_open_recent_menu.addAction(action) if filenames: self.file_open_recent_menu.addSeparator() self.file_open_recent_menu.addAction( make_action(self, get_icon(EDIT_CLEAR_SVG), '&Clear', self.file_clear_recent_files, tip='Clear the list of recently opened databases')) def file_clear_recent_files(self): self.recent_files.clear() self.file_update_ui() def file_new(self): if filename := self._file_new_or_open('New', QFileDialog.getSaveFileName): if filename.exists(): QMessageBox.warning( self, f'Database exists — {APPNAME}', f'Will not overwrite an existing database ' '({filename}) with a new one') else: self.file_load(filename, new=True)