class SearchStatus(QWidget): def __init__(self, parent=None): super().__init__(parent) self.text = QLabel() self._progress = QProgressBar() self._progress.hide() layout = QHBoxLayout() layout.addWidget(self.text) layout.addWidget(self._progress) layout.setContentsMargins(0, 0, 0, 0) self.setLayout(layout) def sizeHint(self): width = super().sizeHint().width() height = self._progress.sizeHint().height() return QSize(width, height) def SetProgress(self, bytes_received, bytes_total): self._progress.setMaximum(bytes_total) self._progress.setValue(bytes_received) def ShowProgress(self): self._progress.show() def HideProgress(self): self._progress.hide() self._progress.reset()
class ProgressOverlay(QWidget): """ Displays a progress bar on top of the provided parent QWidget """ progress_bar_width_factor = 0.75 def __init__(self, parent): super(ProgressOverlay, self).__init__(parent=parent) self.parent = parent self.setStyleSheet('background: rgba(255, 0, 0, 100);') # Make widget transparent self.setAttribute(Qt.WA_TransparentForMouseEvents) # Setup widget Layout self.box_layout = QHBoxLayout(self) self.box_layout.setContentsMargins(0, 0, 0, 0) self.box_layout.setSpacing(0) self.box_layout.setAlignment(Qt.AlignHCenter) self.progress = QProgressBar(self) self.progress.setFormat('%v/%m') self.progress.setAlignment(Qt.AlignCenter) self.progress.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.box_layout.addWidget(self.progress, 0, Qt.AlignCenter) self.parent.installEventFilter(self) self.progress.hide() def eventFilter(self, obj, event): if obj == self.progress: """ Hide or Show ProgressOverlay if progress bar is hidden or shown """ if event.type() == QEvent.Hide: self.hide() return True elif event.type() == QEvent.Show: self.show() return True if obj == self.parent: """ Resize ProgressOverlay on parent widget resize event """ if event.type() == QEvent.Resize: self.resize(self.parent.size()) width = self.parent.size().width() * self.progress_bar_width_factor self.progress.setMinimumSize(QSize(width, 20)) return True return False
class MainWindow(QMainWindow): """ The main window of angr management. """ def __init__(self, file_to_open=None, parent=None): super(MainWindow, self).__init__(parent) icon_location = os.path.join(IMG_LOCATION, 'angr.png') self.setWindowIcon(QIcon(icon_location)) GlobalInfo.main_window = self # initialization self.caption = "angr Management" self.setMinimumSize(QSize(400, 400)) self.setDockNestingEnabled(True) self.workspace = None self.central_widget = None self._plugin_mgr = None # type: PluginManager self._file_toolbar = None # type: FileToolbar self._states_toolbar = None # type: StatesToolbar self._analysis_toolbar = None # type: AnalysisToolbar self._progressbar = None # type: QProgressBar self._load_binary_dialog = None self._status = "" self._progress = None self._init_menus() self._init_toolbars() self._init_statusbar() self._init_workspace() self._init_plugins() self.showMaximized() # I'm ready to show off! self.show() self.status = "Ready." if file_to_open is not None: self.load_file(file_to_open) def sizeHint(self, *args, **kwargs): return QSize(1200, 800) # # Properties # @property def caption(self): return self.getWindowTitle() @caption.setter def caption(self, v): self.setWindowTitle(v) @property def status(self): return self._status @status.setter def status(self, v): self._status = v self.statusBar().showMessage(v) @property def progress(self): return self._progress @progress.setter def progress(self, v): self._progress = v self._progressbar.show() self._progressbar.setValue(v) # # Dialogs # def _open_mainfile_dialog(self): file_path, _ = QFileDialog.getOpenFileName( self, "Open a binary", ".", "All executables (*);;Windows PE files (*.exe);;Core Dumps (*.core);;angr database (*.adb)", ) return file_path def _pick_image_dialog(self): try: prompt = LoadDockerPrompt() except LoadDockerPromptError: return if prompt.exec_() == 0: return # User canceled return prompt.textValue() def _load_options_dialog(self, partial_ld): try: self._load_binary_dialog = LoadBinary(partial_ld) self._load_binary_dialog.setModal(True) self._load_binary_dialog.exec_() if self._load_binary_dialog.cfg_args is not None: # load the binary return (self._load_binary_dialog.load_options, self._load_binary_dialog.cfg_args) except LoadBinaryError: pass return None, None def open_newstate_dialog(self): new_state_dialog = NewState(self.workspace.instance, parent=self) new_state_dialog.exec_() def open_doc_link(self): QDesktopServices.openUrl( QUrl("https://docs.angr.io/", QUrl.TolerantMode)) def open_about_dialog(self): QMessageBox.about(self, "About angr", "Version 8") # # Widgets # def _init_statusbar(self): self._progressbar = QProgressBar() self._progressbar.setMinimum(0) self._progressbar.setMaximum(100) self._progressbar.hide() self.statusBar().addPermanentWidget(self._progressbar) def _init_toolbars(self): self._file_toolbar = FileToolbar(self) self._states_toolbar = StatesToolbar(self) self._analysis_toolbar = AnalysisToolbar(self) self.addToolBar(Qt.TopToolBarArea, self._file_toolbar.qtoolbar()) self.addToolBar(Qt.TopToolBarArea, self._states_toolbar.qtoolbar()) self.addToolBar(Qt.TopToolBarArea, self._analysis_toolbar.qtoolbar()) # # Menus # def _init_menus(self): fileMenu = FileMenu(self) analyzeMenu = AnalyzeMenu(self) helpMenu = HelpMenu(self) self.menuBar().addMenu(fileMenu.qmenu()) self.menuBar().addMenu(analyzeMenu.qmenu()) self.menuBar().addMenu(helpMenu.qmenu()) # # Workspace # def _init_workspace(self): self.central_widget = QMainWindow() self.setCentralWidget(self.central_widget) wk = Workspace(self, Instance()) self.workspace = wk self.central_widget.setTabPosition(Qt.RightDockWidgetArea, QTabWidget.North) # # PluginManager # def _init_plugins(self): self._plugin_mgr = PluginManager(self.workspace) self._plugin_mgr.initialize_all() # # Event # def resizeEvent(self, event): """ :param QResizeEvent event: :return: """ self._recalculate_view_sizes(event.oldSize()) def closeEvent(self, event): self._plugin_mgr.stop_all() event.accept() def event(self, event): if event.type() == QEvent.User: # our event callback try: event.result = event.execute() except Exception as e: event.exception = e event.event.set() return True return super(MainWindow, self).event(event) # # Actions # def reload(self): self.workspace.reload() def open_file_button(self): file_path = self._open_mainfile_dialog() self.load_file(file_path) def open_docker_button(self): required = { 'archr: git clone https://github.com/angr/archr && cd archr && pip install -e .': archr, 'keystone: pip install --no-binary keystone-engine keystone-engine': keystone } is_missing = [key for key, value in required.items() if value is None] if len(is_missing) > 0: req_msg = 'You need to install the following:\n\n\t' + '\n\t'.join( is_missing) req_msg += '\n\nInstall them to enable this functionality.' req_msg += '\nRelaunch angr-management after install.' QMessageBox(self).critical(None, 'Dependency error', req_msg) return img_name = self._pick_image_dialog() if img_name is None: return self.workspace.instance.set_image(img_name) self.load_image(img_name) def load_file(self, file_path): if os.path.isfile(file_path): if file_path.endswith(".adb"): self.load_database(file_path) else: partial_ld = cle.Loader(file_path, perform_relocations=False) load_options, cfg_args = self._load_options_dialog(partial_ld) partial_ld.close() if cfg_args is None: return proj = angr.Project(file_path, load_options=load_options) self._set_proj(proj, cfg_args) def load_image(self, img_name): with archr.targets.DockerImageTarget( img_name, target_path=None).build().start() as t: # this is perhaps the point where we should split out loading of generic targets? dsb = archr.arsenal.DataScoutBow(t) apb = archr.arsenal.angrProjectBow(t, dsb) partial_ld = apb.fire(return_loader=True, perform_relocations=False) load_options, cfg_args = self._load_options_dialog(partial_ld) partial_ld.close() if cfg_args is None: return # Create the project, load it, then record the image name on success proj = apb.fire(use_sim_procedures=True, load_options=load_options) self._set_proj(proj, cfg_args) def save_database(self): if self.workspace.instance.database_path is None: self.save_database_as() else: self._save_database(self.workspace.instance.database_path) def save_database_as(self): # Open File window file_path, _ = QFileDialog.getSaveFileName( self, "Save angr database", ".", "angr databases (*.adb)", ) if not file_path.endswith(".adb"): file_path = file_path + ".adb" self._save_database(file_path) def quit(self): self.close() def run_variable_recovery(self): self.workspace.view_manager.first_view_in_category( 'disassembly').variable_recovery_flavor = 'accurate' def run_induction_variable_analysis(self): self.workspace.view_manager.first_view_in_category( 'disassembly').run_induction_variable_analysis() def decompile_current_function(self): if self.workspace is not None: self.workspace.decompile_current_function() def interact(self): self.workspace.interact_program(self.workspace.instance.img_name) # # Other public methods # def progress_done(self): self._progress = None self._progressbar.hide() # # Private methods # def _set_proj(self, proj, cfg_args=None): if cfg_args is None: cfg_args = {} self.workspace.instance.set_project(proj) self.workspace.instance.initialize(cfg_args=cfg_args) def _load_database(self, file_path): with open(file_path, "rb") as o: p, cfg, cfb = pickle.load(o) self.workspace.instance.project = p self.workspace.instance.cfg = cfg self.workspace.instance.cfb = cfb self.workspace.reload() self.workspace.on_cfg_generated() self.workspace.instance.database_path = file_path print("DATABASE %s LOADED" % file_path) def _save_database(self, file_path): with open(file_path, "wb") as o: pickle.dump( (self.workspace.instance.project, self.workspace.instance.cfg, self.workspace.instance.cfb), o) self.workspace.instance.database_path = file_path print("DATABASE %s SAVED" % file_path) def _recalculate_view_sizes(self, old_size): adjustable_dockable_views = [ dock for dock in self.workspace.view_manager.docks if dock.widget().default_docking_position in ( 'left', 'bottom', ) ] if not adjustable_dockable_views: return for dock in adjustable_dockable_views: widget = dock.widget() if old_size.width() < 0: dock.old_size = widget.sizeHint() continue if old_size != self.size(): # calculate the width ratio if widget.default_docking_position == 'left': # we want to adjust the width ratio = widget.old_width * 1.0 / old_size.width() new_width = int(self.width() * ratio) widget.width_hint = new_width widget.updateGeometry() elif widget.default_docking_position == 'bottom': # we want to adjust the height ratio = widget.old_height * 1.0 / old_size.height() new_height = int(self.height() * ratio) widget.height_hint = new_height widget.updateGeometry() dock.old_size = widget.size() def _resize_dock_widget(self, dock_widget, new_width, new_height): original_size = dock_widget.size() original_min = dock_widget.minimumSize() original_max = dock_widget.maximumSize() dock_widget.resize(new_width, new_height) if new_width != original_size.width(): if original_size.width() > new_width: dock_widget.setMaximumWidth(new_width) else: dock_widget.setMinimumWidth(new_width) if new_height != original_size.height(): if original_size.height() > new_height: dock_widget.setMaximumHeight(new_height) else: dock_widget.setMinimumHeight(new_height) dock_widget.original_min = original_min dock_widget.original_max = original_max QTimer.singleShot(1, dock_widget.restore_original_size)
class MainWindow(QMainWindow): """ The main window of angr management. """ def __init__(self, parent=None): super(MainWindow, self).__init__(parent) icon_location = os.path.join(IMG_LOCATION, 'angr.png') self.setWindowIcon(QIcon(icon_location)) GlobalInfo.main_window = self # initialization self.setMinimumSize(QSize(400, 400)) self.setDockNestingEnabled(True) self.workspace = None self.central_widget = None self.central_widget2 = None self._file_toolbar = None # type: FileToolbar self._states_toolbar = None # type: StatesToolbar self._analysis_toolbar = None # type: AnalysisToolbar self._progressbar = None # type: QProgressBar self._load_binary_dialog = None self._status = "" self._progress = None self.defaultWindowFlags = None # menus self._file_menu = None self._analyze_menu = None self._view_menu = None self._help_menu = None self._plugin_menu = None self._sync_menu = None self._init_toolbars() self._init_statusbar() self._init_workspace() self._init_shortcuts() self._init_menus() self._init_plugins() self.showMaximized() # event handlers self.windowHandle().screenChanged.connect(self.on_screen_changed) # I'm ready to show off! self.show() self.status = "Ready." def sizeHint(self, *args, **kwargs): return QSize(1200, 800) # # Properties # @property def caption(self): return self.getWindowTitle() @caption.setter def caption(self, v): self.setWindowTitle(v) @property def status(self): return self._status @status.setter def status(self, v): self._status = v self.statusBar().showMessage(v) @property def progress(self): return self._progress @progress.setter def progress(self, v): self._progress = v self._progressbar.show() self._progressbar.setValue(v) # # Dialogs # def _open_mainfile_dialog(self): file_path, _ = QFileDialog.getOpenFileName( self, "Open a binary", "", "All executables (*);;Windows PE files (*.exe);;Core Dumps (*.core);;angr database (*.adb)", ) return file_path def _pick_image_dialog(self): try: prompt = LoadDockerPrompt() except LoadDockerPromptError: return if prompt.exec_() == 0: return # User canceled return prompt.textValue() def open_load_plugins_dialog(self): dlg = LoadPlugins(self.workspace.plugins) dlg.setModal(True) dlg.exec_() def open_newstate_dialog(self): new_state_dialog = NewState(self.workspace.instance, parent=self) new_state_dialog.exec_() def open_doc_link(self): QDesktopServices.openUrl( QUrl("https://docs.angr.io/", QUrl.TolerantMode)) def open_sync_config_dialog(self): if self.workspace.instance.project is None: # project does not exist yet return sync_config = SyncConfig(self.workspace.instance, parent=self) sync_config.exec_() def open_about_dialog(self): dlg = LoadAboutDialog() dlg.exec_() # # Widgets # def _init_statusbar(self): self._progressbar = QProgressBar() self._progressbar.setMinimum(0) self._progressbar.setMaximum(100) self._progressbar.hide() self.statusBar().addPermanentWidget(self._progressbar) def _init_toolbars(self): self._file_toolbar = FileToolbar(self) self._states_toolbar = StatesToolbar(self) self._analysis_toolbar = AnalysisToolbar(self) self.addToolBar(Qt.TopToolBarArea, self._file_toolbar.qtoolbar()) self.addToolBar(Qt.TopToolBarArea, self._states_toolbar.qtoolbar()) self.addToolBar(Qt.TopToolBarArea, self._analysis_toolbar.qtoolbar()) # # Menus # def _init_menus(self): self._file_menu = FileMenu(self) self._analyze_menu = AnalyzeMenu(self) self._view_menu = ViewMenu(self) self._help_menu = HelpMenu(self) self._plugin_menu = PluginMenu(self) self.menuBar().addMenu(self._file_menu.qmenu()) self.menuBar().addMenu(self._view_menu.qmenu()) self.menuBar().addMenu(self._analyze_menu.qmenu()) if has_binsync(): self._sync_menu = SyncMenu(self) self.menuBar().addMenu(self._sync_menu.qmenu()) def on_load(**kwargs): self._sync_menu.action_by_key("config").enable() self.workspace.instance._project_container.am_subscribe(on_load) self.menuBar().addMenu(self._plugin_menu.qmenu()) self.menuBar().addMenu(self._help_menu.qmenu()) # # Workspace # def _init_workspace(self): """ Initialize workspace :return: None """ self.central_widget_main = QSplitter(Qt.Horizontal) self.setCentralWidget(self.central_widget_main) self.central_widget = QMainWindow() self.central_widget2 = QMainWindow() self.central_widget_main.addWidget(self.central_widget) self.central_widget_main.addWidget(self.central_widget2) wk = Workspace(self, Instance()) self.workspace = wk self.workspace.view_manager.tabify_center_views() self.central_widget.setTabPosition(Qt.RightDockWidgetArea, QTabWidget.North) self.central_widget2.setTabPosition(Qt.LeftDockWidgetArea, QTabWidget.North) def set_caption(**kwargs): if self.workspace.instance.project == None: self.caption = '' else: self.caption = os.path.basename( self.workspace.instance.project.filename) self.workspace.instance.project_container.am_subscribe(set_caption) # # Shortcuts # def _init_shortcuts(self): """ Initialize shortcuts :return: None """ center_dockable_views = self.workspace.view_manager.get_center_views() for i in range(1, len(center_dockable_views) + 1): QShortcut(QKeySequence('Ctrl+' + str(i)), self, center_dockable_views[i - 1].raise_) # Raise the DisassemblyView after everything has initialized center_dockable_views[0].raise_() # # Plugins # def _init_plugins(self): os.environ['AM_BUILTIN_PLUGINS'] = os.path.dirname(plugins.__file__) blacklist = Conf.plugin_blacklist.split(',') for search_dir in Conf.plugin_search_path.split(':'): search_dir = os.path.expanduser(search_dir) search_dir = os.path.expandvars(search_dir) for plugin_or_exception in plugins.load_plugins_from_dir( search_dir): if isinstance(plugin_or_exception, Exception): self.workspace.log(plugin_or_exception) elif not any(dont in repr(plugin_or_exception) for dont in blacklist): self.workspace.plugins.activate_plugin(plugin_or_exception) else: self.workspace.plugins.load_plugin(plugin_or_exception) self.workspace.log("Blacklisted plugin %s" % plugin_or_exception.get_display_name()) # # Event # def resizeEvent(self, event): """ :param QResizeEvent event: :return: """ self._recalculate_view_sizes(event.oldSize()) def closeEvent(self, event): # Ask if the user wants to save things if self.workspace.instance is not None and self.workspace.instance.project is not None: msgbox = QMessageBox() msgbox.setWindowTitle("Save database") msgbox.setText( "angr management is about to exit. Do you want to save the database?" ) msgbox.setIcon(QMessageBox.Question) msgbox.setWindowIcon(self.windowIcon()) msgbox.setStandardButtons(QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel) msgbox.setDefaultButton(QMessageBox.Yes) r = msgbox.exec_() if r == QMessageBox.Cancel: event.ignore() return elif r == QMessageBox.Yes: save_r = self.save_database() if not save_r: # failed to save the database event.ignore() return for plugin in list(self.workspace.plugins.active_plugins): self.workspace.plugins.deactivate_plugin(plugin) event.accept() def event(self, event): if event.type() == QEvent.User: # our event callback try: event.result = event.execute() except Exception as e: event.exception = e event.event.set() return True return super(MainWindow, self).event(event) def on_screen_changed(self, screen): """ When changing from one screen to another, ask disassembly views to refresh in case the DPI is changed. """ self.workspace.current_screen.am_obj = screen self.workspace.current_screen.am_event() # # Actions # def reload(self): self.workspace.reload() def open_file_button(self): file_path = self._open_mainfile_dialog() if not file_path: return self.load_file(file_path) def open_docker_button(self): required = { 'archr: git clone https://github.com/angr/archr && cd archr && pip install -e .': archr, 'keystone: pip install --no-binary keystone-engine keystone-engine': keystone } is_missing = [key for key, value in required.items() if value is None] if len(is_missing) > 0: req_msg = 'You need to install the following:\n\n\t' + '\n\t'.join( is_missing) req_msg += '\n\nInstall them to enable this functionality.' req_msg += '\nRelaunch angr-management after install.' QMessageBox(self).critical(None, 'Dependency error', req_msg) return img_name = self._pick_image_dialog() if img_name is None: return target = archr.targets.DockerImageTarget(img_name, target_path=None) self.workspace.instance.add_job(LoadTargetJob(target)) self.workspace.instance.set_image(img_name) def load_file(self, file_path): if not isurl(file_path): # file if os.path.isfile(file_path): if file_path.endswith(".adb"): self._load_database(file_path) else: self.workspace.instance.add_job(LoadBinaryJob(file_path)) else: QMessageBox.critical( self, "File not found", "angr management cannot open file %s. " "Please make sure that the file exists." % file_path) else: # url r = QMessageBox.question( self, "Downloading a file", "Do you want to download a file from %s and open it in angr management?" % file_path, defaultButton=QMessageBox.Yes) if r == QMessageBox.Yes: try: target_path = download_url(file_path, parent=self, to_file=True, file_path=None) except InvalidURLError: QMessageBox.critical( self, "Downloading failed", "angr management failed to download the file. The URL is invalid." ) return except UnexpectedStatusCodeError as ex: QMessageBox.critical( self, "Downloading failed", "angr management failed to retrieve the header of the file. " "The HTTP request returned an unexpected status code %d." % ex.status_code) return if target_path: # open the file - now it's a local file self.load_file(target_path) def load_database(self): # Open File window file_path, _ = QFileDialog.getOpenFileName( self, "Load angr database", ".", "angr databases (*.adb)", ) if not file_path: return self._load_database(file_path) def save_database(self): if self.workspace.instance is None or self.workspace.instance.project is None: return True if self.workspace.instance.database_path is None: return self.save_database_as() else: return self._save_database(self.workspace.instance.database_path) def save_database_as(self): if self.workspace.instance is None or self.workspace.instance.project is None: return False default_database_path = self.workspace.instance.database_path if default_database_path is None: default_database_path = os.path.normpath( self.workspace.instance.project.filename) + ".adb" # Open File window file_path, _ = QFileDialog.getSaveFileName( self, "Save angr database", default_database_path, "angr databases (*.adb)", ) if not file_path: return False if not file_path.endswith(".adb"): file_path = file_path + ".adb" return self._save_database(file_path) def preferences(self): # Open Preferences dialog pref = Preferences(self.workspace, parent=self) pref.exec_() def quit(self): self.close() def run_variable_recovery(self): self.workspace.view_manager.first_view_in_category( 'disassembly').variable_recovery_flavor = 'accurate' def run_induction_variable_analysis(self): self.workspace.view_manager.first_view_in_category( 'disassembly').run_induction_variable_analysis() def decompile_current_function(self): if self.workspace is not None: self.workspace.decompile_current_function() def interact(self): self.workspace.interact_program(self.workspace.instance.img_name) def setup_sync(self): self.open_sync_config_dialog() # # Other public methods # def progress_done(self): self._progress = None self._progressbar.hide() def bring_to_front(self): self.setWindowState((self.windowState() & ~Qt.WindowMinimized) | Qt.WindowActive) self.activateWindow() self.raise_() # # Private methods # def _load_database(self, file_path): if AngrDB is None: QMessageBox.critical( None, 'Error', 'AngrDB is not enabled. Maybe you do not have SQLAlchemy installed?' ) return angrdb = AngrDB() try: proj = angrdb.load(file_path) except angr.errors.AngrIncompatibleDBError as ex: QMessageBox.critical( None, 'Error', "Failed to load the angr database because of compatibility issues.\n" "Details: %s" % str(ex)) return except angr.errors.AngrDBError as ex: QMessageBox.critical( None, 'Error', 'Failed to load the angr database.\n' 'Details: %s' % str(ex)) _l.critical("Failed to load the angr database.", exc_info=True) return cfg = proj.kb.cfgs['CFGFast'] cfb = proj.analyses.CFB() # it will load functions from kb self.workspace.instance.database_path = file_path self.workspace.instance.initialized = True # skip automated CFG recovery self.workspace.instance.project = proj self.workspace.instance.cfg = cfg self.workspace.instance.cfb = cfb # trigger callbacks self.workspace.reload() self.workspace.on_cfg_generated() def _save_database(self, file_path): if self.workspace.instance is None or self.workspace.instance.project is None: return False if AngrDB is None: QMessageBox.critical( None, 'Error', 'AngrDB is not enabled. Maybe you do not have SQLAlchemy installed?' ) return False angrdb = AngrDB(project=self.workspace.instance.project) angrdb.dump(file_path) self.workspace.instance.database_path = file_path return True def _recalculate_view_sizes(self, old_size): adjustable_dockable_views = [ dock for dock in self.workspace.view_manager.docks if dock.widget().default_docking_position in ( 'left', 'bottom', ) ] if not adjustable_dockable_views: return for dock in adjustable_dockable_views: widget = dock.widget() if old_size.width() < 0: dock.old_size = widget.sizeHint() continue if old_size != self.size(): # calculate the width ratio if widget.default_docking_position == 'left': # we want to adjust the width ratio = widget.old_width * 1.0 / old_size.width() new_width = int(self.width() * ratio) widget.width_hint = new_width widget.updateGeometry() elif widget.default_docking_position == 'bottom': # we want to adjust the height ratio = widget.old_height * 1.0 / old_size.height() new_height = int(self.height() * ratio) widget.height_hint = new_height widget.updateGeometry() dock.old_size = widget.size() def _resize_dock_widget(self, dock_widget, new_width, new_height): original_size = dock_widget.size() original_min = dock_widget.minimumSize() original_max = dock_widget.maximumSize() dock_widget.resize(new_width, new_height) if new_width != original_size.width(): if original_size.width() > new_width: dock_widget.setMaximumWidth(new_width) else: dock_widget.setMinimumWidth(new_width) if new_height != original_size.height(): if original_size.height() > new_height: dock_widget.setMaximumHeight(new_height) else: dock_widget.setMinimumHeight(new_height) dock_widget.original_min = original_min dock_widget.original_max = original_max QTimer.singleShot(1, dock_widget.restore_original_size)
class SpineDatapackageWidget(QMainWindow): """A widget to allow user to edit a datapackage and convert it to a Spine database in SQLite. Attributes: toolbox (ToolboxUI): QMainWindow instance data_connection (DataConnection): Data Connection associated to this widget datapackage (CustomPackage): Datapackage to load and use """ msg = Signal(str, name="msg") msg_proc = Signal(str, name="msg_proc") msg_error = Signal(str, name="msg_error") def __init__(self, data_connection): """Initialize class.""" super().__init__(flags=Qt.Window) # TODO: Set parent as toolbox here if it makes sense # TODO: Maybe set the parent as ToolboxUI so that its stylesheet is inherited. This may need # TODO: reimplementing the window minimizing and maximizing actions as well as setting the window modality self._data_connection = data_connection self.datapackage = None self.descriptor_tree_context_menu = None self.selected_resource_name = None self.resource_data = dict() self.resources_model = DatapackageResourcesModel(self) self.fields_model = DatapackageFieldsModel(self) self.foreign_keys_model = DatapackageForeignKeysModel(self) self.foreign_keys_model.set_default_row(length=1) self.resource_data_model = MinimalTableModel(self) self.default_row_height = QFontMetrics(QFont("", 0)).lineSpacing() max_screen_height = max([s.availableSize().height() for s in QGuiApplication.screens()]) self.visible_rows = int(max_screen_height / self.default_row_height) self.err_msg = QErrorMessage(self) self.remove_row_icon = QIcon(":/icons/minus.png") self.progress_bar = QProgressBar() self.progress_bar.hide() self.focus_widget = None # Last widget which had focus before showing a menu from the menubar # Set up the user interface from Designer. self.ui = Ui_MainWindow() self.ui.setupUi(self) self.setWindowIcon(QIcon(":/symbols/app.ico")) self.qsettings = QSettings("SpineProject", "Spine Toolbox") self.restore_ui() self.add_toggle_view_actions() # Add status bar to form self.ui.statusbar.setFixedHeight(20) self.ui.statusbar.setSizeGripEnabled(False) self.ui.statusbar.setStyleSheet(STATUSBAR_SS) self.ui.statusbar.addPermanentWidget(self.progress_bar) self.ui.tableView_resources.setModel(self.resources_model) self.ui.tableView_fields.setModel(self.fields_model) self.ui.tableView_foreign_keys.setModel(self.foreign_keys_model) self.ui.tableView_resource_data.setModel(self.resource_data_model) self.ui.tableView_resources.verticalHeader().setDefaultSectionSize(self.default_row_height) self.ui.tableView_resource_data.verticalHeader().setDefaultSectionSize(self.default_row_height) self.ui.tableView_resource_data.horizontalHeader().setResizeContentsPrecision(self.visible_rows) self.ui.tableView_fields.verticalHeader().setDefaultSectionSize(self.default_row_height) self.ui.tableView_fields.horizontalHeader().setResizeContentsPrecision(self.visible_rows) self.ui.tableView_foreign_keys.verticalHeader().setDefaultSectionSize(self.default_row_height) self.ui.tableView_foreign_keys.horizontalHeader().setResizeContentsPrecision(self.visible_rows) self.connect_signals() # Ensure this window gets garbage-collected when closed self.setAttribute(Qt.WA_DeleteOnClose) def add_toggle_view_actions(self): """Add toggle view actions to View menu.""" self.ui.menuDock_Widgets.addAction(self.ui.dockWidget_foreign_keys.toggleViewAction()) self.ui.menuDock_Widgets.addAction(self.ui.dockWidget_fields.toggleViewAction()) def show(self): """Called when the form shows. Init datapackage (either from existing datapackage.json or by inferring a new one from sources) and update ui.""" super().show() if not self.load_datapackage(): return self.update_ui() @Slot(bool, name="infer_datapackage") def infer_datapackage(self, checked=False): """Called when the user triggers the infer action. Infer datapackage from sources and update ui.""" if not self.infer_datapackage_(): return self.update_ui() def load_datapackage(self): """Load datapackage from 'datapackage.json' file in data directory, or infer one from CSV files in that directory.""" file_path = os.path.join(self._data_connection.data_dir, "datapackage.json") if os.path.exists(file_path): self.datapackage = CustomPackage(file_path) msg = "Datapackage succesfully loaded from {}".format(file_path) self.msg.emit(msg) return True return self.infer_datapackage() def infer_datapackage_(self): """Infer datapackage from CSV files in data directory.""" data_files = self._data_connection.data_files() if ".csv" in [os.path.splitext(f)[1] for f in data_files]: self.datapackage = CustomPackage(base_path=self._data_connection.data_dir) self.datapackage.infer(os.path.join(self._data_connection.data_dir, '*.csv')) msg = "Datapackage succesfully inferred from {}".format(self._data_connection.data_dir) self.msg.emit(msg) return True self.msg_error.emit("Unable to infer a datapackage from <b>{0}</b>. " "Please add some CSV files to that folder, " "and then select the <b>Infer datapackage</b> option " "from the <b>File</b> menu.".format(self._data_connection.data_dir)) return False def update_ui(self): """Update ui from datapackage attribute.""" if not self.datapackage: return self.load_resource_data() self.resources_model.reset_model(self.datapackage.resources) first_index = self.resources_model.index(0, 0) if not first_index.isValid(): return self.ui.tableView_resources.selectionModel().select(first_index, QItemSelectionModel.Select) self.reset_resource_models(first_index, QModelIndex()) def connect_signals(self): """Connect signals to slots.""" # Message actions self.msg.connect(self.add_message) self.msg_proc.connect(self.add_process_message) self.msg_error.connect(self.add_error_message) # DC destroyed self._data_connection.destroyed.connect(self.close) # Copy and paste self.ui.actionCopy.triggered.connect(self.copy) self.ui.actionPaste.triggered.connect(self.paste) # Delegates # Resource name line_edit_delegate = LineEditDelegate(self) line_edit_delegate.data_committed.connect(self._handle_resource_name_data_committed) self.ui.tableView_resources.setItemDelegateForColumn(0, line_edit_delegate) # Field name line_edit_delegate = LineEditDelegate(self) line_edit_delegate.data_committed.connect(self._handle_field_name_data_committed) self.ui.tableView_fields.setItemDelegateForColumn(0, line_edit_delegate) # Primary key checkbox_delegate = CheckBoxDelegate(self) checkbox_delegate.data_committed.connect(self._handle_primary_key_data_committed) self.ui.tableView_fields.setItemDelegateForColumn(2, checkbox_delegate) self.ui.tableView_resource_data.setItemDelegate(line_edit_delegate) # Foreign keys foreign_keys_delegate = ForeignKeysDelegate(self) foreign_keys_delegate.data_committed.connect(self._handle_foreign_keys_data_committed) self.ui.tableView_foreign_keys.setItemDelegate(foreign_keys_delegate) self.foreign_keys_model.rowsInserted.connect(self._handle_foreign_keys_model_rows_inserted) # Selected resource changed self.ui.tableView_resources.selectionModel().currentChanged.connect(self.reset_resource_models) # Foreign keys data changed self.foreign_keys_model.dataChanged.connect(self._handle_foreign_keys_data_changed) # Actions self.ui.actionClose.triggered.connect(self.close) self.ui.actionSave_datapackage.triggered.connect(self.save_datapackage) self.ui.actionInfer_datapackage.triggered.connect(self.infer_datapackage) self.ui.actionExport_to_spine.triggered.connect(self.show_export_to_spine_dialog) # Menu about to show self.ui.menuFile.aboutToShow.connect(self._handle_menu_about_to_show) self.ui.menuEdit.aboutToShow.connect(self._handle_menu_about_to_show) self.ui.menuView.aboutToShow.connect(self._handle_menu_about_to_show) def restore_ui(self): """Restore UI state from previous session.""" window_size = self.qsettings.value("dataPackageWidget/windowSize") window_pos = self.qsettings.value("dataPackageWidget/windowPosition") splitter_state = self.qsettings.value("dataPackageWidget/splitterState") window_maximized = self.qsettings.value("dataPackageWidget/windowMaximized", defaultValue='false') window_state = self.qsettings.value("dataPackageWidget/windowState") n_screens = self.qsettings.value("mainWindow/n_screens", defaultValue=1) if window_size: self.resize(window_size) if window_pos: self.move(window_pos) if window_maximized == 'true': self.setWindowState(Qt.WindowMaximized) if window_state: self.restoreState(window_state, version=1) # Toolbar and dockWidget positions if splitter_state: self.ui.splitter.restoreState(splitter_state) # noinspection PyArgumentList if len(QGuiApplication.screens()) < int(n_screens): # There are less screens available now than on previous application startup self.move(0, 0) # Move this widget to primary screen position (0,0) @Slot(name="_handle_menu_about_to_show") def _handle_menu_about_to_show(self): """Called when a menu from the menubar is about to show. Adjust infer action depending on whether or not we have a datapackage. Adjust copy paste actions depending on which widget has the focus. TODO Enable/disable action to save datapackage depending on status. """ if self.datapackage: self.ui.actionInfer_datapackage.setText("Re-infer datapackage") else: self.ui.actionInfer_datapackage.setText("Infer datapackage") self.ui.actionCopy.setText("Copy") self.ui.actionPaste.setText("Paste") self.ui.actionCopy.setEnabled(False) self.ui.actionPaste.setEnabled(False) if self.focusWidget() != self.ui.menubar: self.focus_widget = self.focusWidget() if self.focus_widget == self.ui.tableView_resources: focus_widget_name = "resources" elif self.focus_widget == self.ui.tableView_resource_data: focus_widget_name = "data" elif self.focus_widget == self.ui.tableView_fields: focus_widget_name = "fields" elif self.focus_widget == self.ui.tableView_foreign_keys: focus_widget_name = "foreign keys" else: return if not self.focus_widget.selectionModel().selection().isEmpty(): self.ui.actionCopy.setText("Copy from {}".format(focus_widget_name)) self.ui.actionCopy.setEnabled(True) if self.focus_widget.canPaste(): self.ui.actionPaste.setText("Paste to {}".format(focus_widget_name)) self.ui.actionPaste.setEnabled(True) @Slot(str, name="add_message") def add_message(self, msg): """Prepend regular message to status bar. Args: msg (str): String to show in QStatusBar """ msg += "\t" + self.ui.statusbar.currentMessage() self.ui.statusbar.showMessage(msg, 5000) @Slot(str, name="add_process_message") def add_process_message(self, msg): """Show process message in status bar. This messages stays until replaced. Args: msg (str): String to show in QStatusBar """ self.ui.statusbar.showMessage(msg, 0) @Slot(str, name="add_error_message") def add_error_message(self, msg): """Show error message. Args: msg (str): String to show """ self.err_msg.showMessage(msg) @Slot(bool, name="save_datapackage") def save_datapackage(self, checked=False): """Write datapackage to file 'datapackage.json' in data directory.""" if os.path.isfile(os.path.join(self._data_connection.data_dir, "datapackage.json")): msg = ('<b>Replacing file "datapackage.json" in "{}"</b>. ' 'Are you sure?').format(os.path.basename(self._data_connection.data_dir)) # noinspection PyCallByClass, PyTypeChecker answer = QMessageBox.question( self, 'Replace "datapackage.json"', msg, QMessageBox.Yes, QMessageBox.No) if not answer == QMessageBox.Yes: return False if self.datapackage.save(os.path.join(self._data_connection.data_dir, 'datapackage.json')): msg = '"datapackage.json" saved in {}'.format(self._data_connection.data_dir) self.msg.emit(msg) return True msg = 'Failed to save "datapackage.json" in {}'.format(self._data_connection.data_dir) self.msg_error.emit(msg) return False @Slot(bool, name="show_export_to_spine_dialog") def show_export_to_spine_dialog(self, checked=False): """Show dialog to allow user to select a file to export.""" answer = QFileDialog.getSaveFileName(self, "Export to file", self._data_connection._project.project_dir, "SQlite database (*.sqlite *.db)") file_path = answer[0] if not file_path: # Cancel button clicked return self.export_to_spine(file_path) @busy_effect def export_to_spine(self, file_path): """Export datapackage into Spine SQlite file.""" # Remove file if exists (at this point, the user has confirmed that overwritting is ok) try: os.remove(file_path) except OSError: pass db_url = 'sqlite:///{0}'.format(file_path) # datapackage_path = os.path.join(self.datapackage.base_path, "datapackage.json") self.progress_bar.show() # converter = DatapackageToSpineConverter(db_url, datapackage_path) converter = DatapackageToSpineConverter(db_url, self.datapackage.descriptor, self.datapackage.base_path) converter.signaler.finished.connect(self._handle_converter_finished) converter.signaler.failed.connect(self._handle_converter_failed) converter.signaler.progressed.connect(self._handle_converter_progressed) self.msg_proc.emit("Estimating work load...") self.progress_bar.setRange(0, converter.number_of_steps()) self.progress_bar.reset() QThreadPool.globalInstance().start(converter) @Slot("int", "QString", name="_handle_converter_progressed") def _handle_converter_progressed(self, step, msg): self.progress_bar.setValue(step) if msg: self.msg_proc.emit(msg) @Slot("QString", name="_handle_converter_failed") def _handle_converter_failed(self, msg): self.progress_bar.hide() self.msg_error.emit("Unable to export datapackage: {}.".format(msg)) @Slot(name="_handle_converter_finished") def _handle_converter_finished(self): self.progress_bar.hide() self.msg_proc.emit("Datapackage successfully exported.") @Slot("bool", name="copy") def copy(self, checked=False): """Copy data to clipboard.""" focus_widget = self.focusWidget() try: focus_widget.copy() except AttributeError: pass @Slot("bool", name="paste") def paste(self, checked=False): """Paste data from clipboard.""" focus_widget = self.focusWidget() try: focus_widget.paste() except AttributeError: pass def load_resource_data(self): """Load resource data into a local list of tables.""" for resource in self.datapackage.resources: self.resource_data[resource.name] = resource.read(cast=False) @Slot("QModelIndex", "QModelIndex", name="reset_resource_models") def reset_resource_models(self, current, previous): """Reset resource data and schema models whenever a new resource is selected.""" if current.column() != 0: return new_selected_resource_name = current.data(Qt.DisplayRole) self.selected_resource_name = new_selected_resource_name self.reset_resource_data_model() schema = self.datapackage.get_resource(self.selected_resource_name).schema self.fields_model.reset_model(schema) self.foreign_keys_model.reset_model(schema.foreign_keys) self.ui.tableView_fields.resizeColumnsToContents() self.ui.tableView_foreign_keys.resizeColumnsToContents() # Add buttons self._handle_foreign_keys_model_rows_inserted( QModelIndex(), 0, self.foreign_keys_model.rowCount() - 1) # Resize last section that has the button to remove row self.ui.tableView_foreign_keys.horizontalHeader().resizeSection( self.foreign_keys_model.columnCount() - 1, self.default_row_height) def reset_resource_data_model(self): """Reset resource data model with data from newly selected resource.""" data = self.resource_data[self.selected_resource_name] field_names = self.datapackage.get_resource(self.selected_resource_name).schema.field_names self.resource_data_model.set_horizontal_header_labels(field_names) self.resource_data_model.reset_model(data) self.ui.tableView_resource_data.resizeColumnsToContents() # Replace delegate line_edit_delegate = LineEditDelegate(self) self.ui.tableView_resource_data.setItemDelegate(line_edit_delegate) line_edit_delegate.data_committed.connect(self.update_resource_data) @Slot("QModelIndex", "QVariant", name="update_resource_data") def update_resource_data(self, index, new_value): """Update resource data with newly edited data.""" if not self.resource_data_model.setData(index, new_value, Qt.EditRole): return self.ui.tableView_resource_data.resizeColumnsToContents() @Slot("QModelIndex", "QVariant", name="_handle_resource_name_data_committed") def _handle_resource_name_data_committed(self, index, new_name): """Called when line edit delegate wants to edit resource name data. Update resources model and descriptor with new resource name.""" if not new_name: return old_name = index.data(Qt.DisplayRole) if not self.resources_model.setData(index, new_name, Qt.EditRole): return resource_data = self.resource_data.pop(self.selected_resource_name) if resource_data is None: msg = "Couldn't find key in resource data dict. Something is wrong." logging.debug(msg) return self.resource_data[new_name] = resource_data self.selected_resource_name = new_name self.datapackage.rename_resource(old_name, new_name) @Slot("QModelIndex", "QVariant", name="_handle_field_name_data_committed") def _handle_field_name_data_committed(self, index, new_name): """Called when line edit delegate wants to edit field name data. Update name in fields_model, resource_data_model's header and datapackage descriptor. """ if not new_name: return old_name = index.data(Qt.DisplayRole) if not self.fields_model.setData(index, new_name, Qt.EditRole): return self.datapackage.rename_field(self.selected_resource_name, old_name, new_name) field_names = self.datapackage.get_resource(self.selected_resource_name).schema.field_names self.resource_data_model.set_horizontal_header_labels(field_names) self.ui.tableView_resource_data.resizeColumnsToContents() schema = self.datapackage.get_resource(self.selected_resource_name).schema self.foreign_keys_model.reset_model(schema.foreign_keys) @Slot("QModelIndex", name="_handle_primary_key_data_committed") def _handle_primary_key_data_committed(self, index): """Called when checkbox delegate wants to edit primary key data. Add or remove primary key field accordingly. """ status = index.data(Qt.EditRole) field_name = index.sibling(index.row(), 0).data(Qt.DisplayRole) if status is False: # Add to primary key self.fields_model.setData(index, True, Qt.EditRole) self.datapackage.append_to_primary_key(self.selected_resource_name, field_name) else: # Remove from primary key self.fields_model.setData(index, False, Qt.EditRole) self.datapackage.remove_from_primary_key(self.selected_resource_name, field_name) @Slot("QModelIndex", "QVariant", name="_handle_foreign_keys_data_committed") def _handle_foreign_keys_data_committed(self, index, value): self.foreign_keys_model.setData(index, value, Qt.EditRole) @Slot("QModelIndex", "QModelIndex", "QVector<int>", name="_handle_foreign_keys_data_changed") def _handle_foreign_keys_data_changed(self, top_left, bottom_right, roles=list()): """Called when foreign keys data is updated in model. Update descriptor accordingly.""" if roles and Qt.EditRole not in roles: return resource = self.selected_resource_name foreign_keys = self.datapackage.get_resource(resource).schema.foreign_keys anything_updated = False rows = range(top_left.row(), bottom_right.row() + 1) error_log = "" for row in rows: # Remove previous foreign key self.datapackage.remove_foreign_keys_row(row, resource) # Add new foreign key if possible row_data = self.foreign_keys_model._main_data[row][0:3] if all(row_data): fields_str, reference_resource, reference_fields_str = row_data fields = fields_str.split(",") reference_fields = reference_fields_str.split(",") try: self.datapackage.insert_foreign_key(row, resource, fields, reference_resource, reference_fields) anything_updated = True except DataPackageException as e: v_section = self.foreign_keys_model.headerData(row, Qt.Vertical) error_log += "<p>Unable to add foreign key at row {0}: '{1}'</p>".format(v_section, e) if anything_updated: self.msg.emit("Successfully updated foreign keys.") if error_log: self.msg_error.emit(error_log) @Slot("QModelIndex", "int", "int", name="_handle_foreign_keys_model_rows_inserted") def _handle_foreign_keys_model_rows_inserted(self, parent, first, last): column = self.foreign_keys_model.columnCount() - 1 for row in range(first, last + 1): index = self.foreign_keys_model.index(row, column, parent) self.create_remove_foreign_keys_row_button(index) def create_remove_foreign_keys_row_button(self, index): """Create button to remove foreign keys row.""" action = QAction() action.setIcon(self.remove_row_icon) button = QToolButton() button.setDefaultAction(action) button.setIconSize(QSize(20, 20)) self.ui.tableView_foreign_keys.setIndexWidget(index, button) action.triggered.connect(lambda: self.remove_foreign_key_row(button)) def remove_foreign_key_row(self, button): column = self.foreign_keys_model.columnCount() - 1 for row in range(self.foreign_keys_model.rowCount()): index = self.foreign_keys_model.index(row, column) if button != self.ui.tableView_foreign_keys.indexWidget(index): continue # Remove fk from datapackage descriptor self.foreign_keys_model.removeRows(row, 1) resource = self.selected_resource_name self.datapackage.remove_foreign_keys_row(row, resource) self.msg.emit("Successfully removed foreign key.") break def closeEvent(self, event=None): """Handle close event. Args: event (QEvent): Closing event if 'X' is clicked. """ # save qsettings self.qsettings.setValue("dataPackageWidget/splitterState", self.ui.splitter.saveState()) self.qsettings.setValue("dataPackageWidget/windowSize", self.size()) self.qsettings.setValue("dataPackageWidget/windowPosition", self.pos()) self.qsettings.setValue("dataPackageWidget/windowState", self.saveState(version=1)) if self.windowState() == Qt.WindowMaximized: self.qsettings.setValue("dataPackageWidget/windowMaximized", True) else: self.qsettings.setValue("dataPackageWidget/windowMaximized", False) if event: event.accept()
class MainWindow(QMainWindow): """ The main window of angr management. """ def __init__(self, app: Optional['QApplication'] = None, parent=None, show=True, use_daemon=False): super().__init__(parent) icon_location = os.path.join(IMG_LOCATION, 'angr.png') self.setWindowIcon(QIcon(icon_location)) GlobalInfo.main_window = self # initialization self.setMinimumSize(QSize(400, 400)) self.setDockNestingEnabled(True) self.app: Optional['QApplication'] = app self.workspace: Workspace = None self.central_widget: QMainWindow = None self.toolbar_manager: ToolbarManager = ToolbarManager(self) self._progressbar = None # type: QProgressBar self._progress_dialog = None # type: QProgressDialog self._load_binary_dialog = None self.defaultWindowFlags = None # menus self._file_menu = None # FileMenu self._analyze_menu = None self._view_menu = None self._help_menu = None self._plugin_menu = None self._init_statusbar() self._init_workspace() self._init_toolbars() self._init_menus() self._init_plugins() self._init_library_docs() self._init_url_scheme_handler() self.workspace.plugins.on_workspace_initialized(self) self._init_shortcuts() self._init_flirt_signatures() self._run_daemon(use_daemon=use_daemon) # I'm ready to show off! if show: self.showMaximized() self.windowHandle().screenChanged.connect(self.on_screen_changed) self.show() self.status = "Ready." def sizeHint(self, *args, **kwargs): # pylint: disable=unused-argument,no-self-use return QSize(1200, 800) # # Properties # @property def caption(self): return self.getWindowTitle() @caption.setter def caption(self, v): self.setWindowTitle(v) # # Dialogs # def open_mainfile_dialog(self): # pylint: disable=assigning-non-slot # https://github.com/PyCQA/pylint/issues/3793 file_path, _ = QFileDialog.getOpenFileName( self, "Open a binary", Conf.last_used_directory, "All executables (*);;" "Windows PE files (*.exe);;" "Core Dumps (*.core);;" "angr database (*.adb)", ) Conf.last_used_directory = os.path.dirname(file_path) return file_path def _pick_image_dialog(self): try: prompt = LoadDockerPrompt(parent=self) except LoadDockerPromptError: return None if prompt.exec_() == 0: return None # User canceled return prompt.textValue() def open_load_plugins_dialog(self): dlg = LoadPlugins(self.workspace.plugins) dlg.setModal(True) dlg.exec_() def open_newstate_dialog(self): if self.workspace.instance.project.am_none: QMessageBox.critical(self, "Cannot create new states", "Please open a binary to analyze first.") return new_state_dialog = NewState(self.workspace.instance, parent=self, create_simgr=True) new_state_dialog.exec_() def open_doc_link(self): QDesktopServices.openUrl( QUrl("https://docs.angr.io/", QUrl.TolerantMode)) def open_about_dialog(self): dlg = LoadAboutDialog() dlg.exec_() # # Widgets # def _init_statusbar(self): self._progressbar = QProgressBar() self._progressbar.setMinimum(0) self._progressbar.setMaximum(100) self._progressbar.hide() self.statusBar().addPermanentWidget(self._progressbar) self._progress_dialog = QProgressDialog("Waiting...", "Cancel", 0, 100, self) self._progress_dialog.setAutoClose(False) self._progress_dialog.setWindowFlags( self._progress_dialog.windowFlags() & ~Qt.WindowContextHelpButtonHint) self._progress_dialog.setModal(True) self._progress_dialog.setMinimumDuration(2**31 - 1) def on_cancel(): if self.workspace is None: return for job in self.workspace.instance.jobs: if job.blocking: job.keyboard_interrupt() break self._progress_dialog.canceled.connect(on_cancel) self._progress_dialog.close() def _init_toolbars(self): for cls in (FileToolbar, DebugToolbar): self.toolbar_manager.show_toolbar_by_class(cls) # # Menus # def _init_menus(self): self._file_menu = FileMenu(self) self._analyze_menu = AnalyzeMenu(self) self._view_menu = ViewMenu(self) self._help_menu = HelpMenu(self) self._plugin_menu = PluginMenu(self) for path in Conf.recent_files: self._file_menu.add_recent(path) # TODO: Eventually fix menu bars to have native support on MacOS # if on a Mac, don't use the native menu bar (bug mitigation from QT) if sys.platform == 'darwin': self.menuBar().setNativeMenuBar(False) self.menuBar().addMenu(self._file_menu.qmenu()) self.menuBar().addMenu(self._view_menu.qmenu()) self.menuBar().addMenu(self._analyze_menu.qmenu()) self.menuBar().addMenu(self._plugin_menu.qmenu()) self.menuBar().addMenu(self._help_menu.qmenu()) # # Workspace # def _init_workspace(self): """ Initialize workspace :return: None """ self.central_widget = QMainWindow() self.setCentralWidget(self.central_widget) wk = Workspace(self, Instance()) self.workspace = wk self.workspace.view_manager.tabify_center_views() self.central_widget.setTabPosition(Qt.RightDockWidgetArea, QTabWidget.North) self.central_widget.setDockNestingEnabled(True) def set_caption(**kwargs): # pylint: disable=unused-argument if self.workspace.instance.project.am_none: self.caption = '' elif self.workspace.instance.project.filename is None: self.caption = "Loaded from stream" else: self.caption = os.path.basename( self.workspace.instance.project.filename) self.workspace.instance.project.am_subscribe(set_caption) self.tab = self.central_widget.findChild(QTabBar) self.tab.tabBarClicked.connect(self.on_center_tab_clicked) # # Shortcuts # def interrupt_current_job(self): self.workspace.instance.interrupt_current_job() def _init_shortcuts(self): """ Initialize shortcuts :return: None """ center_dockable_views = self.workspace.view_manager.get_center_views() for i in range(1, len(center_dockable_views) + 1): QShortcut(QKeySequence('Ctrl+' + str(i)), self, center_dockable_views[i - 1].raise_) QShortcut(QKeySequence("Ctrl+I"), self, self.interrupt_current_job) # Raise the DisassemblyView after everything has initialized center_dockable_views[0].raise_() # Toggle exec breakpoint QShortcut(QKeySequence(Qt.Key_F2), self, self.workspace.toggle_exec_breakpoint) # Single step QShortcut(QKeySequence(Qt.Key_F7), self, self.workspace.step_forward) # Run QShortcut(QKeySequence(Qt.Key_F9), self, self.workspace.continue_forward) # # Plugins # def _init_plugins(self): self.workspace.plugins.discover_and_initialize_plugins() # # FLIRT Signatures # def _init_flirt_signatures(self): if Conf.flirt_signatures_root: # if it's a relative path, it's relative to the angr-management package if os.path.isabs(Conf.flirt_signatures_root): flirt_signatures_root = Conf.flirt_signatures_root else: if is_pyinstaller(): flirt_signatures_root = os.path.join( app_root(), Conf.flirt_signatures_root) else: # when running as a Python package, we should use the git submodule, which is on the same level # with (instead of inside) the angrmanagement module directory. flirt_signatures_root = os.path.join( app_root(), "..", Conf.flirt_signatures_root) flirt_signatures_root = os.path.normpath(flirt_signatures_root) _l.info("Loading FLIRT signatures from %s.", flirt_signatures_root) angr.flirt.load_signatures(flirt_signatures_root) # # Library docs # def _init_library_docs(self): GlobalInfo.library_docs = LibraryDocs() if Conf.library_docs_root: GlobalInfo.library_docs.load_func_docs(Conf.library_docs_root) # # Daemon # def _run_daemon(self, use_daemon=None): if use_daemon is None: # Load it from the configuration file use_daemon = Conf.use_daemon if not use_daemon: return # connect to daemon (if there is one) if not daemon_exists(): print("[+] Starting a new daemon.") run_daemon_process() time.sleep(0.2) else: print("[+] Connecting to an existing angr management daemon.") while True: try: GlobalInfo.daemon_conn = daemon_conn(service=ClientService) except ConnectionRefusedError: print("[-] Connection failed... try again.") time.sleep(0.4) continue print("[+] Connected to daemon.") break from rpyc import BgServingThread # pylint:disable=import-outside-toplevel _ = BgServingThread(GlobalInfo.daemon_conn) # # URL scheme handler setup # def _init_url_scheme_handler(self): # URL scheme from ..logic.url_scheme import AngrUrlScheme # pylint:disable=import-outside-toplevel scheme = AngrUrlScheme() registered, _ = scheme.is_url_scheme_registered() supported = scheme.is_url_scheme_supported() checrs_plugin = self.workspace.plugins.get_plugin_instance_by_name( "ChessConnector") if checrs_plugin is None: return if not registered and supported: btn = QMessageBox.question( None, "Setting up angr URL scheme", "angr URL scheme allows \"deep linking\" from browsers and other applications " "by registering the angr:// protocol to the current user. Do you want to " "register it? You may unregister at any " "time in Preferences.", defaultButton=QMessageBox.Yes) if btn == QMessageBox.Yes: try: AngrUrlScheme().register_url_scheme() except (ValueError, FileNotFoundError) as ex: QMessageBox.warning( None, "Error in registering angr URL scheme", "Failed to register the angr URL scheme.\n" "The following exception occurred:\n" + str(ex)) # # Event # def resizeEvent(self, event: QResizeEvent): """ :param event: :return: """ self._recalculate_view_sizes(event.oldSize()) def closeEvent(self, event): # Ask if the user wants to save things if self.workspace.instance is not None and not self.workspace.instance.project.am_none: msgbox = QMessageBox() msgbox.setWindowTitle("Save database") msgbox.setText( "angr management is about to exit. Do you want to save the database?" ) msgbox.setIcon(QMessageBox.Question) msgbox.setWindowIcon(self.windowIcon()) msgbox.setStandardButtons(QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel) msgbox.setDefaultButton(QMessageBox.Yes) r = msgbox.exec_() if r == QMessageBox.Cancel: event.ignore() return elif r == QMessageBox.Yes: save_r = self.save_database() if not save_r: # failed to save the database event.ignore() return for plugin in list(self.workspace.plugins.active_plugins): self.workspace.plugins.deactivate_plugin(plugin) event.accept() def event(self, event): if event.type() == QEvent.User: # our event callback try: event.result = event.execute() except Exception as ex: # pylint:disable=broad-except event.exception = ex event.event.set() return True return super().event(event) def on_screen_changed(self, screen): """ When changing from one screen to another, ask disassembly views to refresh in case the DPI is changed. """ self.workspace.current_screen.am_obj = screen self.workspace.current_screen.am_event() def on_center_tab_clicked(self, index): self.workspace.view_manager.handle_center_tab_click(index) # # Actions # def reload(self): self.workspace.reload() def open_file_button(self): file_path = self.open_mainfile_dialog() if not file_path: return self.load_file(file_path) def open_trace_file_button(self): file_path, _ = QFileDialog.getOpenFileName( self, "Open a trace file", Conf.last_used_directory, "All files (*);;" "Trace files (*.trace);;", ) Conf.last_used_directory = os.path.dirname(file_path) if not file_path: return self.load_trace_file(file_path) def open_docker_button(self): required = { 'archr: git clone https://github.com/angr/archr && cd archr && pip install -e .': archr, 'keystone: pip install --no-binary keystone-engine keystone-engine': keystone } is_missing = [key for key, value in required.items() if value is None] if len(is_missing) > 0: req_msg = 'You need to install the following:\n\n\t' + '\n\t'.join( is_missing) req_msg += '\n\nInstall them to enable this functionality.' req_msg += '\nRelaunch angr-management after install.' QMessageBox(self).critical(None, 'Dependency error', req_msg) return img_name = self._pick_image_dialog() if img_name is None: return target = archr.targets.DockerImageTarget(img_name, target_path=None) self.workspace.instance.add_job(LoadTargetJob(target)) self.workspace.instance.img_name = img_name def load_trace_file(self, file_path): if isurl(file_path): QMessageBox.critical( self, "Unsupported Action", "Downloading trace files is not yet supported." "Please specify a path to a file on disk.") else: # File if os.path.isfile(file_path): try: with open(file_path, 'rb') as f: loaded_sim_state = pickle.load(f) analysis_params = { 'end_state': loaded_sim_state, 'start_addr': None, 'end_addr': None, 'block_addrs': None, } self.workspace.view_data_dependency_graph( analysis_params) self._recent_file(file_path) except pickle.PickleError: QMessageBox.critical( self, "Unable to load trace file", "Trace file must contain a serialized SimState.") else: QMessageBox.critical( self, "File not found", f"angr management cannot open file {file_path}. " "Please make sure that the file exists.") def load_file(self, file_path): if not isurl(file_path): # file if os.path.isfile(file_path): if file_path.endswith(".trace"): self.workspace.load_trace_from_path(file_path) return self.workspace.instance.binary_path = file_path self.workspace.instance.original_binary_path = file_path if file_path.endswith(".adb"): self._load_database(file_path) else: self._recent_file(file_path) self.workspace.instance.add_job(LoadBinaryJob(file_path)) else: QMessageBox.critical( self, "File not found", f"angr management cannot open file {file_path}. " "Please make sure that the file exists.") else: # url r = QMessageBox.question( self, "Downloading a file", f"Do you want to download a file from {file_path} and open it in angr management?", defaultButton=QMessageBox.Yes) if r == QMessageBox.Yes: try: target_path = download_url(file_path, parent=self, to_file=True, file_path=None) except InvalidURLError: QMessageBox.critical( self, "Downloading failed", "angr management failed to download the file. The URL is invalid." ) return except UnexpectedStatusCodeError as ex: QMessageBox.critical( self, "Downloading failed", "angr management failed to retrieve the header of the file. " f"The HTTP request returned an unexpected status code {ex.status_code}." ) return if target_path: # open the file - now it's a local file self.workspace.instance.binary_path = target_path self.workspace.instance.original_binary_path = file_path self.load_file(target_path) def load_database(self): # Open File window file_path, _ = QFileDialog.getOpenFileName( self, "Load angr database", ".", "angr databases (*.adb)", ) if not file_path: return self._load_database(file_path) def save_database(self): if self.workspace.instance is None or self.workspace.instance.project.am_none: return True if self.workspace.instance.database_path is None: return self.save_database_as() else: return self._save_database(self.workspace.instance.database_path) def save_database_as(self): if self.workspace.instance is None or self.workspace.instance.project.am_none: return False default_database_path = self.workspace.instance.database_path if default_database_path is None: default_database_path = os.path.normpath( self.workspace.instance.project.filename) + ".adb" # Open File window file_path, _ = QFileDialog.getSaveFileName( self, "Save angr database", default_database_path, "angr databases (*.adb)", ) if not file_path: return False if not file_path.endswith(".adb"): file_path = file_path + ".adb" return self._save_database(file_path) def load_trace(self): file_path, _ = QFileDialog.getOpenFileName(self, "Load trace", ".", "bintrace (*.trace)") if not file_path: return self.workspace.load_trace_from_path(file_path) def preferences(self): # Open Preferences dialog pref = Preferences(self.workspace, parent=self) pref.exec_() def quit(self): self.close() def run_variable_recovery(self): self.workspace._get_or_create_disassembly_view( ).variable_recovery_flavor = 'accurate' def run_induction_variable_analysis(self): self.workspace._get_or_create_disassembly_view( ).run_induction_variable_analysis() def run_dependency_analysis(self, func_addr: Optional[int] = None, func_arg_idx: Optional[int] = None): if self.workspace is None or self.workspace.instance is None: return dep_analysis_job = DependencyAnalysisJob(func_addr=func_addr, func_arg_idx=func_arg_idx) self.workspace.instance.add_job(dep_analysis_job) def decompile_current_function(self): if self.workspace is not None: self.workspace.decompile_current_function() def view_proximity_for_current_function(self): if self.workspace is not None: self.workspace.view_proximity_for_current_function() def interact(self): self.workspace.interact_program(self.workspace.instance.img_name) # # Other public methods # def progress(self, status, progress): self.statusBar().showMessage(f'Working... {status}') self._progress_dialog.setLabelText(status) self._progressbar.show() self._progressbar.setValue(progress) self._progress_dialog.setValue(progress) def progress_done(self): self._progressbar.hide() self.statusBar().showMessage("Ready.") self._progress_dialog.hide() def bring_to_front(self): self.setWindowState((self.windowState() & ~Qt.WindowMinimized) | Qt.WindowActive) self.activateWindow() self.raise_() # # Private methods # def _recent_file(self, file_path): file_path = os.path.abspath(file_path) self._file_menu.add_recent(file_path) Conf.recent_file(file_path) save_config() def _load_database(self, file_path): if AngrDB is None: QMessageBox.critical( None, 'Error', 'AngrDB is not enabled. Maybe you do not have SQLAlchemy installed?' ) return angrdb = AngrDB() other_kbs = {} extra_info = {} try: proj = angrdb.load(file_path, kb_names=["global", "pseudocode_variable_kb"], other_kbs=other_kbs, extra_info=extra_info) except angr.errors.AngrIncompatibleDBError as ex: QMessageBox.critical( None, 'Error', "Failed to load the angr database because of compatibility issues.\n" f"Details: {ex}") return except angr.errors.AngrDBError as ex: QMessageBox.critical( None, 'Error', 'Failed to load the angr database.\n' f'Details: {ex}') _l.critical("Failed to load the angr database.", exc_info=True) return self._recent_file(file_path) cfg = proj.kb.cfgs['CFGFast'] cfb = proj.analyses.CFB() # it will load functions from kb self.workspace.instance.database_path = file_path self.workspace.instance._reset_containers() self.workspace.instance.project = proj self.workspace.instance.cfg = cfg self.workspace.instance.cfb = cfb if "pseudocode_variable_kb" in other_kbs: self.workspace.instance.pseudocode_variable_kb = other_kbs[ "pseudocode_variable_kb"] else: self.workspace.instance.initialize_pseudocode_variable_kb() self.workspace.instance.project.am_event(initialized=True) # trigger callbacks self.workspace.reload() self.workspace.on_cfg_generated() self.workspace.plugins.angrdb_load_entries(extra_info) def _save_database(self, file_path): if self.workspace.instance is None or self.workspace.instance.project.am_none: return False if AngrDB is None: QMessageBox.critical( None, 'Error', 'AngrDB is not enabled. Maybe you do not have SQLAlchemy installed?' ) return False self.workspace.plugins.handle_project_save(file_path) angrdb = AngrDB(project=self.workspace.instance.project) extra_info = self.workspace.plugins.angrdb_store_entries() angrdb.dump( file_path, kbs=[ self.workspace.instance.kb, self.workspace.instance.pseudocode_variable_kb, ], extra_info=extra_info, ) self.workspace.instance.database_path = file_path return True def _recalculate_view_sizes(self, old_size): adjustable_dockable_views = [ dock for dock in self.workspace.view_manager.docks if dock.widget().default_docking_position in ( 'left', 'bottom', ) ] if not adjustable_dockable_views: return for dock in adjustable_dockable_views: widget = dock.widget() if old_size.width() < 0: dock.old_size = widget.sizeHint() continue if old_size != self.size(): # calculate the width ratio if widget.default_docking_position == 'left': # we want to adjust the width ratio = widget.old_width * 1.0 / old_size.width() new_width = int(self.width() * ratio) widget.width_hint = new_width widget.updateGeometry() elif widget.default_docking_position == 'bottom': # we want to adjust the height ratio = widget.old_height * 1.0 / old_size.height() new_height = int(self.height() * ratio) widget.height_hint = new_height widget.updateGeometry() dock.old_size = widget.size() def _resize_dock_widget(self, dock_widget, new_width, new_height): original_size = dock_widget.size() original_min = dock_widget.minimumSize() original_max = dock_widget.maximumSize() dock_widget.resize(new_width, new_height) if new_width != original_size.width(): if original_size.width() > new_width: dock_widget.setMaximumWidth(new_width) else: dock_widget.setMinimumWidth(new_width) if new_height != original_size.height(): if original_size.height() > new_height: dock_widget.setMaximumHeight(new_height) else: dock_widget.setMinimumHeight(new_height) dock_widget.original_min = original_min dock_widget.original_max = original_max QTimer.singleShot(1, dock_widget.restore_original_size)
class UIMainWindow(object): """Container class to hold all UI-related creation methods. Must be sublcassed. """ def create_ui(self): """Setup main UI elements, dock widgets, UI-related elements, etc. """ log.debug('Loading UI') # Undo Stack self.undo_stack = QUndoStack(self) self.undo_stack.setUndoLimit(100) # Object navigation history self.obj_history = deque([], config.MAX_OBJ_HISTORY) app = QApplication.instance() base_font = QFont() base_font.fromString(self.prefs['base_font']) app.setFont(base_font) # Object class table widget # classTable = QTableView(self) classTable = classtable.TableView(self) classTable.setObjectName("classTable") classTable.setAlternatingRowColors(True) classTable.setFrameShape(QFrame.StyledPanel) classTable_font = QFont() classTable_font.fromString(self.prefs['class_table_font']) classTable.setFont(classTable_font) fm = classTable.fontMetrics() classTable.setWordWrap(True) classTable.setEditTriggers(QAbstractItemView.EditKeyPressed | QAbstractItemView.DoubleClicked | QAbstractItemView.AnyKeyPressed | QAbstractItemView.SelectedClicked) # classTable.horizontalHeader().setMovable(True) # classTable.verticalHeader().setMovable(False) classTable.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive) classTable.verticalHeader().setSectionResizeMode(QHeaderView.Interactive) classTable.horizontalHeader().setDefaultSectionSize(self.prefs['default_column_width']) classTable.verticalHeader().setDefaultSectionSize(fm.height() + 0) classTable.setSelectionMode(QAbstractItemView.ExtendedSelection) classTable.setContextMenuPolicy(Qt.CustomContextMenu) classTable.customContextMenuRequested.connect(self.custom_table_context_menu) # Create table model and proxy layers for transposing and filtering self.classTableModel = classtable.IDFObjectTableModel(classTable) self.transposeableModel = classtable.TransposeProxyModel(self.classTableModel) self.transposeableModel.setSourceModel(self.classTableModel) self.sortableModel = classtable.SortFilterProxyModel(self.transposeableModel) self.sortableModel.setSourceModel(self.transposeableModel) # Assign model to table (enable sorting FIRST) # table.setSortingEnabled(True) # Disable for now, CRUD actions won't work! classTable.setModel(self.sortableModel) # Connect some signals selection_model = classTable.selectionModel() selection_model.selectionChanged.connect(self.table_selection_changed) scroll_bar = classTable.verticalScrollBar() scroll_bar.valueChanged.connect(self.scroll_changed) # These are currently broken # classTable.horizontalHeader().sectionMoved.connect(self.moveObject) # classTable.verticalHeader().sectionMoved.connect(self.moveObject) # Object class tree widget classTreeDockWidget = QDockWidget("Object Classes and Counts", self) classTreeDockWidget.setObjectName("classTreeDockWidget") classTreeDockWidget.setAllowedAreas(Qt.AllDockWidgetAreas) classTree = QTreeView(classTreeDockWidget) classTree.setUniformRowHeights(True) classTree.setAllColumnsShowFocus(True) classTree.setRootIsDecorated(False) classTree.setExpandsOnDoubleClick(True) classTree.setIndentation(15) classTree.setAnimated(True) classTree_font = QFont() classTree_font.fromString(self.prefs['class_tree_font']) classTree.setFont(classTree_font) classTree.setAlternatingRowColors(True) classTree.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel) palette = classTree.palette() palette.setColor(QPalette.Highlight, Qt.darkCyan) classTree.setPalette(palette) class_tree_window = QWidget(classTreeDockWidget) class_tree_dock_layout_v = QVBoxLayout() class_tree_dock_layout_h = QHBoxLayout() class_tree_dock_layout_v.setContentsMargins(0, 8, 0, 0) class_tree_dock_layout_h.setContentsMargins(0, 0, 0, 0) class_tree_filter_edit = QLineEdit(classTreeDockWidget) class_tree_filter_edit.setPlaceholderText("Filter Classes") class_tree_filter_edit.textChanged.connect(self.treeFilterRegExpChanged) class_tree_filter_cancel = QPushButton("Clear", classTreeDockWidget) class_tree_filter_cancel.setMaximumWidth(45) class_tree_filter_cancel.clicked.connect(self.clearTreeFilterClicked) class_tree_dock_layout_h.addWidget(class_tree_filter_edit) class_tree_dock_layout_h.addWidget(class_tree_filter_cancel) class_tree_dock_layout_v.addLayout(class_tree_dock_layout_h) class_tree_dock_layout_v.addWidget(classTree) class_tree_window.setLayout(class_tree_dock_layout_v) classTreeDockWidget.setWidget(class_tree_window) classTreeDockWidget.setContentsMargins(0,0,0,0) # Comments widget commentDockWidget = QDockWidget("Comments", self) commentDockWidget.setObjectName("commentDockWidget") commentDockWidget.setAllowedAreas(Qt.AllDockWidgetAreas) commentView = UndoRedoTextEdit(commentDockWidget, self) commentView.setLineWrapMode(QTextEdit.FixedColumnWidth) commentView.setLineWrapColumnOrWidth(499) commentView.setFrameShape(QFrame.StyledPanel) commentView_font = QFont() commentView_font.fromString(self.prefs['comments_font']) commentView.setFont(commentView_font) commentDockWidget.setWidget(commentView) # Info and help widget infoDockWidget = QDockWidget("Info", self) infoDockWidget.setObjectName("infoDockWidget") infoDockWidget.setAllowedAreas(Qt.AllDockWidgetAreas) infoView = QTextEdit(infoDockWidget) infoView.setFrameShape(QFrame.StyledPanel) infoView.setReadOnly(True) infoDockWidget.setWidget(infoView) # Node list and jump menu widget refDockWidget = QDockWidget("Field References", self) refDockWidget.setObjectName("refDockWidget") refDockWidget.setAllowedAreas(Qt.AllDockWidgetAreas) ref_model = reftree.ReferenceTreeModel(None, refDockWidget) refView = QTreeView(refDockWidget) refView.setModel(ref_model) refView.setUniformRowHeights(True) refView.setRootIsDecorated(False) refView.setIndentation(15) refView.setColumnWidth(0, 160) refView.setFrameShape(QFrame.StyledPanel) refDockWidget.setWidget(refView) refView.doubleClicked.connect(self.ref_tree_double_clicked) # Logging and debugging widget logDockWidget = QDockWidget("Log Viewer", self) logDockWidget.setObjectName("logDockWidget") logDockWidget.setAllowedAreas(Qt.AllDockWidgetAreas) logView = QPlainTextEdit(logDockWidget) logView.setLineWrapMode(QPlainTextEdit.NoWrap) logView.setReadOnly(True) logView_font = QFont() logView_font.fromString(self.prefs['base_font']) logView.setFont(logView_font) logView.ensureCursorVisible() logDockWidget.setWidget(logView) # Undo view widget undoDockWidget = QDockWidget("Undo History", self) undoDockWidget.setObjectName("undoDockWidget") undoDockWidget.setAllowedAreas(Qt.AllDockWidgetAreas) undoView = QUndoView(self.undo_stack) undoDockWidget.setWidget(undoView) # Define corner docking behaviour self.setDockNestingEnabled(True) self.setCorner(Qt.TopLeftCorner, Qt.LeftDockWidgetArea) self.setCorner(Qt.TopRightCorner, Qt.RightDockWidgetArea) self.setCorner(Qt.BottomLeftCorner, Qt.LeftDockWidgetArea) self.setCorner(Qt.BottomRightCorner, Qt.RightDockWidgetArea) # Assign main widget and dock widgets to QMainWindow self.setCentralWidget(classTable) self.addDockWidget(Qt.LeftDockWidgetArea, classTreeDockWidget) self.addDockWidget(Qt.RightDockWidgetArea, commentDockWidget) self.addDockWidget(Qt.RightDockWidgetArea, infoDockWidget) self.addDockWidget(Qt.RightDockWidgetArea, refDockWidget) self.addDockWidget(Qt.RightDockWidgetArea, logDockWidget) self.addDockWidget(Qt.RightDockWidgetArea, undoDockWidget) # Store widgets for access by other objects self.classTable = classTable self.commentView = commentView self.infoView = infoView self.classTree = classTree self.logView = logView self.undoView = undoView self.refView = refView self.filterTreeBox = class_tree_filter_edit # Store docks for access by other objects self.commentDockWidget = commentDockWidget self.infoDockWidget = infoDockWidget self.classTreeDockWidget = classTreeDockWidget self.logDockWidget = logDockWidget self.undoDockWidget = undoDockWidget self.refDockWidget = refDockWidget # Perform other UI-related initialization tasks self.center() self.setUnifiedTitleAndToolBarOnMac(True) self.setWindowIcon(QIcon(':/images/logo.png')) # Status bar setup self.statusBar().showMessage('Status: Ready') self.unitsLabel = QLabel() self.unitsLabel.setAlignment(Qt.AlignCenter) self.unitsLabel.setMinimumSize(self.unitsLabel.sizeHint()) self.unitsLabel.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken) self.statusBar().addPermanentWidget(self.unitsLabel) self.pathLabel = QLabel() self.pathLabel.setAlignment(Qt.AlignCenter) self.pathLabel.setMinimumSize(self.pathLabel.sizeHint()) self.pathLabel.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken) self.statusBar().addPermanentWidget(self.pathLabel) self.versionLabel = QLabel() self.versionLabel.setAlignment(Qt.AlignCenter) self.versionLabel.setMinimumSize(self.versionLabel.sizeHint()) self.versionLabel.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken) self.statusBar().addPermanentWidget(self.versionLabel) self.progressBarIDF = QProgressBar() self.progressBarIDF.setAlignment(Qt.AlignCenter) self.progressBarIDF.setMaximumWidth(200) self.statusBar().addPermanentWidget(self.progressBarIDF) self.clipboard = QApplication.instance().clipboard() self.obj_clipboard = [] self.setStyleSheet(""" QToolTip { background-color: gray; color: white; border: black solid 1px } # QMenu { # background-color: rgbf(0.949020, 0.945098, 0.941176); # color: rgb(255,255,255); # } # QMenu::item::selected { # background-color: rgbf(0.949020, 0.945098, 0.941176); # } """) def create_tray_menu(self): """Creates an icon and menu for the system tray """ # Menu for the system tray self.trayIconMenu = QMenu(self) self.trayIconMenu.addAction(self.minimizeAction) self.trayIconMenu.addAction(self.maximizeAction) self.trayIconMenu.addAction(self.restoreAction) self.trayIconMenu.addSeparator() self.trayIconMenu.addAction(self.exitAct) # System tray itself self.trayIcon = QSystemTrayIcon(self) self.trayIcon.setContextMenu(self.trayIconMenu) self.trayIcon.setIcon(QIcon(':/images/logo.png')) self.trayIcon.setToolTip('IDF+') self.trayIcon.show() def create_actions(self): """Creates appropriate actions for use in menus and toolbars. """ self.newAct = QAction(QIcon(':/images/new1.png'), "&New", self, shortcut=QKeySequence.New, statusTip="Create a new file", iconVisibleInMenu=True, triggered=self.new_file) self.openAct = QAction(QIcon(':/images/open.png'), "&Open...", self, shortcut=QKeySequence.Open, statusTip="Open an existing file", iconVisibleInMenu=True, triggered=self.open_file) self.saveAct = QAction(QIcon(':/images/save.png'), "&Save", self, shortcut=QKeySequence.Save, statusTip="Save the document to disk", iconVisibleInMenu=True, triggered=self.save) self.saveFormatAct = QAction(QIcon(':/images/save.png'), "&Format && Save", self, shortcut=QKeySequence('Ctrl+Shift+F'), statusTip="Format File and Save to disk", iconVisibleInMenu=True, triggered=self.format_save) self.saveAsAct = QAction(QIcon(':/images/saveas.png'), "Save &As...", self, shortcut=QKeySequence.SaveAs, statusTip="Save the document under a new name", iconVisibleInMenu=True, triggered=self.save_as) self.exitAct = QAction(QIcon(':/images/quit.png'), "E&xit", self, shortcut=QKeySequence('Ctrl+Q'), iconVisibleInMenu=True, statusTip="Exit the application", triggered=self.closeAllWindows) self.cutObjAct = QAction(QIcon(':/images/cut.png'), "Cu&t Object", self, shortcut=QKeySequence.Cut, statusTip="Cut current selection's contents to clipboard", iconVisibleInMenu=True, triggered=self.cutObject, iconText='Cut Obj') self.copyAct = QAction(QIcon(':/images/copy.png'), "&Copy Selected Values", self, statusTip="Copy current selection's contents to clipboard", iconVisibleInMenu=True, triggered=self.copySelected) self.pasteAct = QAction(QIcon(':/images/paste.png'), "&Paste Selected Values", self, statusTip="Paste clipboard into current selection", iconVisibleInMenu=True, triggered=self.pasteSelected) self.pasteExtAct = QAction(QIcon(':/images/paste.png'), "&Paste from External", self, shortcut=QKeySequence('Ctrl+Shift+v'), statusTip="Paste from external program", iconVisibleInMenu=True, triggered=self.paste_from_external) self.transposeAct = QAction("Transpose", self, shortcut=QKeySequence('Ctrl+t'), statusTip="Transpose rows and columns in object display", triggered=self.transpose_table) self.newObjAct = QAction(QIcon(':/images/new2.png'), "New Object", self, shortcut=QKeySequence('Ctrl+Shift+n'), statusTip="Create new object in current class", iconVisibleInMenu=True, triggered=self.newObject, iconText='New Obj') self.copyObjAct = QAction(QIcon(':/images/copy.png'), "Copy Object", self, shortcut=QKeySequence.Copy, statusTip="Copy the current Object(s)", iconVisibleInMenu=True, triggered=self.copyObject, iconText='Copy Obj') self.pasteObjAct = QAction(QIcon(':/images/paste.png'), "Paste Object", self, shortcut=QKeySequence.Paste, statusTip="Paste the currently copies Object(s)", iconVisibleInMenu=True, triggered=self.pasteObject, iconText='Paste Obj') self.dupObjAct = QAction(QIcon(':/images/copy.png'), "Duplicate Object", self, shortcut=QKeySequence('Shift+Ctrl+d'), statusTip="Duplicate the current Object(s)", iconVisibleInMenu=True, triggered=self.duplicateObject, iconText='Dup Obj') self.delObjAct = QAction(QIcon(':/images/delete.png'), "Delete Object", self, shortcut=QKeySequence.Delete, statusTip="Delete the current Object(s)", iconVisibleInMenu=True, triggered=self.deleteObject, iconText='Del Obj') self.undoAct = QAction(QIcon(':/images/undo.png'), "&Undo", self, shortcut=QKeySequence.Undo, statusTip="Undo previous action", iconVisibleInMenu=True, triggered=self.undo_stack.undo) self.redoAct = QAction(QIcon(':/images/redo.png'), "&Redo", self, shortcut=QKeySequence.Redo, statusTip="Redo previous action", iconVisibleInMenu=True, triggered=self.undo_stack.redo) self.groupAct = QAction("Hide Groups in Class Tree", self, shortcut=QKeySequence('Ctrl+g'), triggered=self.toggle_groups, checkable=True) # self.navForwardAct = QAction("Forward", self, # shortcut=QKeySequence('Ctrl+Plus'), # statusTip="Go forward to the next object", # triggered=self.navForward) # # self.navBackAct = QAction("Back", self, # shortcut=QKeySequence('Ctrl+Minus'), # statusTip="Go back to the previous object", # triggered=self.navBack) self.showInFolderAct = QAction(QIcon(':/images/new.png'), "&Show in folder", self, shortcut=QKeySequence('Ctrl+Shift+t'), statusTip="Open location of current file", iconVisibleInMenu=True) self.showInFolderAct.triggered.connect(lambda: self.show_in_folder()) self.epDocGettingStartedAction = QAction("EnergyPlus Getting Started", self, triggered=self.energy_plus_docs) self.epDocIORefAction = QAction("EnergyPlus I/O Reference", self, triggered=self.energy_plus_docs) self.epDocOutputDetailsAction = QAction("EnergyPlus Output Details and Examples", self, triggered=self.energy_plus_docs) self.epDocEngineeringRefAction = QAction("EnergyPlus Engineering Reference", self, triggered=self.energy_plus_docs) self.epDocAuxiliaryProgsAction = QAction("EnergyPlus Auxiliary Programs", self, triggered=self.energy_plus_docs) self.epDocEMSGuideAction = QAction("EnergyPlus EMS Application Guide", self, triggered=self.energy_plus_docs) self.epDocComplianceAction = QAction("Using EnergyPlus for Compliance", self, triggered=self.energy_plus_docs) self.epDocInterfaceAction = QAction("External Interface Application Guide", self, triggered=self.energy_plus_docs) self.epDocTipsTricksAction = QAction("Tips and Tricks Using EnergyPlus", self, triggered=self.energy_plus_docs) self.epDocPlantGuideAction = QAction("EnergyPlus Plant Application Guide", self, triggered=self.energy_plus_docs) self.epDocAcknowledgmentsAction = QAction("EnergyPlus Acknowledgments", self, triggered=self.energy_plus_docs) self.openInEditorAct = QAction(QIcon(':/images/new.png'), "&Open in text editor", self, shortcut=QKeySequence('Ctrl+e'), statusTip="Open current file in default editor", iconVisibleInMenu=True, triggered=self.open_in_text_editor) self.helpAct = QAction("&EnergyPlus Help (Online)", self, statusTip="Show the EnergyPlus' help", triggered=self.energyplus_help) self.aboutAct = QAction("&About IDF+", self, statusTip="Show the application's About box", triggered=self.about) self.clearRecentAct = QAction("Clear Recent", self, statusTip="Clear recent files", triggered=self.clear_recent) self.minimizeAction = QAction("Mi&nimize", self, triggered=self.hide) self.maximizeAction = QAction("Ma&ximize", self, triggered=self.showMaximized) self.restoreAction = QAction("&Restore", self, triggered=self.showNormal) self.showPrefsAction = QAction("&Preferences", self, triggered=self.show_prefs_dialog) self.showSearchAction = QAction("&Search && Replace", self, shortcut=QKeySequence('Ctrl+f'), triggered=self.show_search_dialog) self.findThisAct = QAction("Find This", self, triggered=self.find_this) self.jumpFilterGeometry = QAction("Include Geometry", self, triggered=self.jump_to_filter_geometry, checkable=True) self.setIPUnitsAction = QAction("&IP Units", self, triggered=self.toggle_units, checkable=True) self.setSIUnitsAction = QAction("&SI Units", self, triggered=self.toggle_units, checkable=True) self.classWithObjsAction = QAction("Show Only Classes With Objects", self, shortcut=QKeySequence('Ctrl+l'), statusTip="Show Only Classes With Objects", triggered=self.toggle_full_tree, checkable=True) self.fillRightAction = QAction("Fill right", self, shortcut=QKeySequence('Ctrl+d'), statusTip="Fill right", triggered=self.fill_right) self.logDockWidgetAct = self.logDockWidget.toggleViewAction() self.transposeAct.setEnabled(False) self.setSIUnitsAction.setChecked(True) self.undoAct.setEnabled(False) self.redoAct.setEnabled(False) self.saveAct.setEnabled(False) self.undo_stack.canUndoChanged.connect(self.toggle_can_undo) self.undo_stack.canRedoChanged.connect(self.toggle_can_redo) self.logDockWidgetAct.toggled.connect(self.start_log_watcher) def toggle_can_undo(self): if self.undo_stack.canUndo(): new_state = True else: new_state = False self.undoAct.setEnabled(new_state) self.set_dirty(new_state) def toggle_can_redo(self): if self.undo_stack.canRedo(): new_state = True else: new_state = False self.redoAct.setEnabled(new_state) def create_menus(self): """Create all required items for menus. """ # File Menu self.fileMenu = self.menuBar().addMenu("&File") self.fileMenuActions = (self.newAct, self.openAct, self.saveAct, self.saveAsAct, self.saveFormatAct, None, self.exitAct) self.update_file_menu() self.fileMenu.aboutToShow.connect(self.update_file_menu) # Edit Menu self.editMenu = self.menuBar().addMenu("&Edit") self.editMenu.addAction(self.undoAct) self.editMenu.addAction(self.redoAct) self.editMenu.addSeparator().setText('Objects') self.editMenu.addAction(self.newObjAct) self.editMenu.addAction(self.dupObjAct) self.editMenu.addSeparator() self.editMenu.addAction(self.cutObjAct) self.editMenu.addAction(self.copyObjAct) self.editMenu.addAction(self.pasteObjAct) self.editMenu.addAction(self.pasteExtAct) self.editMenu.addSeparator() self.editMenu.addAction(self.delObjAct) self.editMenu.addSeparator().setText('Values') self.editMenu.addAction(self.copyAct) self.editMenu.addAction(self.pasteAct) self.editMenu.addSeparator() self.editMenu.addAction(self.fillRightAction) self.editMenu.addSeparator() self.editMenu.addAction(self.showSearchAction) # Tools Menu self.toolsMenu = self.menuBar().addMenu("&Tools") self.toolsMenu.addAction(self.showInFolderAct) self.toolsMenu.addAction(self.openInEditorAct) self.toolsMenu.addSeparator() self.toolsMenu.addAction(self.showPrefsAction) # View Menu self.viewMenu = self.menuBar().addMenu("&View") action_group = QActionGroup(self) self.viewMenu.addAction(action_group.addAction(self.setSIUnitsAction)) self.viewMenu.addAction(action_group.addAction(self.setIPUnitsAction)) self.viewMenu.addSeparator().setText('Dockable Widgets') self.viewMenu.addAction(self.classTreeDockWidget.toggleViewAction()) self.viewMenu.addAction(self.infoView.parent().toggleViewAction()) self.viewMenu.addAction(self.commentView.parent().toggleViewAction()) self.viewMenu.addAction(self.logDockWidgetAct) self.viewMenu.addAction(self.undoView.parent().toggleViewAction()) self.viewMenu.addSeparator().setText('Toolbars') self.viewMenu.addAction(self.fileToolBar.toggleViewAction()) self.viewMenu.addAction(self.editToolBar.toggleViewAction()) # self.viewMenu.addAction(self.navToolBar.toggleViewAction()) self.viewMenu.addAction(self.filterToolBar.toggleViewAction()) self.viewMenu.addSeparator() self.viewMenu.addAction(self.classWithObjsAction) self.viewMenu.addAction(self.groupAct) self.viewMenu.addSeparator() self.viewMenu.addAction(self.transposeAct) # Jump Menu self.jumpToMenu = self.menuBar().addMenu("&Jump") self.update_jump_menu() self.jumpToMenu.aboutToShow.connect(self.update_jump_menu) self.jumpFilterGeometry.setEnabled(False) # Help Menu self.helpMenu = self.menuBar().addMenu("&Help") self.helpMenu.addAction(self.helpAct) self.helpMenu.addAction(self.aboutAct) self.helpMenu.addSeparator() self.helpMenu.addAction(self.epDocGettingStartedAction) self.helpMenu.addAction(self.epDocIORefAction) self.helpMenu.addAction(self.epDocOutputDetailsAction) self.helpMenu.addAction(self.epDocEngineeringRefAction) self.helpMenu.addAction(self.epDocAuxiliaryProgsAction) self.helpMenu.addAction(self.epDocEMSGuideAction) self.helpMenu.addAction(self.epDocComplianceAction) self.helpMenu.addAction(self.epDocInterfaceAction) self.helpMenu.addAction(self.epDocTipsTricksAction) self.helpMenu.addAction(self.epDocPlantGuideAction) self.helpMenu.addAction(self.epDocAcknowledgmentsAction) def create_tool_bars(self): """Creates the necessary toolbars. """ # File Toolbar self.fileToolBar = self.addToolBar("File Toolbar") self.fileToolBar.setObjectName('fileToolbar') self.fileToolBar.addAction(self.newAct) self.fileToolBar.addAction(self.openAct) self.fileToolBar.addAction(self.saveAct) self.fileToolBar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) # Edit Toolbar self.editToolBar = self.addToolBar("Edit Toolbar") self.editToolBar.setObjectName('editToolbar') self.editToolBar.addAction(self.undoAct) self.editToolBar.addAction(self.redoAct) self.editToolBar.addAction(self.newObjAct) self.editToolBar.addAction(self.dupObjAct) self.editToolBar.addAction(self.delObjAct) self.editToolBar.addAction(self.cutObjAct) self.editToolBar.addAction(self.copyObjAct) self.editToolBar.addAction(self.pasteObjAct) self.editToolBar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) # Object history navigation toolbar # self.navToolBar = self.addToolBar("Navigation Toolbar") # self.navToolBar.setObjectName('viewToolBar') # self.navToolBar.addAction(self.navForwardAct) # self.navToolBar.addAction(self.navBackAct) # self.navToolBar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) # Object filter toolbar self.filterToolBar = self.addToolBar("Filter Toolbar") self.filterToolBar.setObjectName('filterToolBar') self.filterBox = QLineEdit() self.filterBox.setPlaceholderText("Filter Objects") self.filterBox.setMaximumWidth(160) self.filterBox.setFixedWidth(160) # filterLabel = QLabel("Filter Obj:", self) # filterLabel.setBuddy(self.filterBox) # self.filterToolBar.addWidget(filterLabel) self.filterBox.textChanged.connect(self.tableFilterRegExpChanged) self.filterBox.textChanged.connect(self.treeFilterRegExpChanged) clearFilterButton = QPushButton('Clear') clearFilterButton.setMaximumWidth(45) clearFilterButton.clicked.connect(self.clearFilterClicked) self.filterToolBar.addWidget(self.filterBox) self.filterToolBar.addWidget(clearFilterButton) self.caseSensitivity = QCheckBox('Case Sensitive') self.caseSensitivity.stateChanged.connect(self.caseSensitivityChanged) self.filterToolBar.addWidget(self.caseSensitivity) self.filterToolBar.addSeparator() self.filterToolBar.addAction(self.transposeAct) def create_shortcuts(self): """Creates keyboard shortcuts. """ # QShortcut(QKeySequence('Ctrl+l'), self).activated.connect(self.toggle_full_tree) # QShortcut(QKeySequence('Ctrl+d'), self).activated.connect(self.fill_right) # QShortcut(QKeySequence('Ctrl+d'), self).activated.connect(self.fill_right) # def createAction(self, text, slot=None, shortcut=None, icon=None, # tip=None, checkable=False, signal="triggered()"): # action = QAction(text, self) # if icon is not None: # action.setIcon(QIcon(":/%s.png" % icon)) # if shortcut is not None: # action.setShortcut(shortcut) # if tip is not None: # action.setToolTip(tip) # action.setStatusTip(tip) # if slot is not None: # self.connect(action, QtCore.SIGNAL(signal), slot) # if checkable: # action.setCheckable(True) # return action # def custom_table_context_menu(self, position): # Create a menu and populate it with actions menu = QMenu(self) menu.addAction(self.undoAct) menu.addAction(self.redoAct) menu.addSeparator() menu.addAction(self.copyObjAct) menu.addAction(self.dupObjAct) menu.addAction(self.delObjAct) menu.addAction(self.newObjAct) menu.addAction(self.cutObjAct) menu.addSeparator() menu.addMenu(self.jumpToMenu) menu.addSeparator() menu.addAction(self.findThisAct) menu.popup(self.classTable.viewport().mapToGlobal(position)) self.mouse_position = position def reset_progress_bar(self): self.progressBarIDF.hide() def center(self): """Called to center the window on the screen on startup. """ screen = QDesktopWidget().screenGeometry() size = self.geometry() self.move((screen.width() - size.width()) / 2, (screen.height() - size.height()) / 2) def show_prefs_dialog(self): """Handles showing the settings dialog and setting its values. """ dlg = PrefsDialog(self, self.prefs) if dlg.exec_(): # Refresh the table view to take into account any new prefs self.load_table_view(self.current_obj_class) # Clear the idd cache if requested if self.prefs.get('clear_idd_cache', False) == True: self.clear_idd_cache() # Update preferences if various flags apply if not self.idf: return options_dict = dict() if self.prefs.get('apply_default_save_behaviour', False) == True: options_dict.update({'sort_order': self.prefs.get('sort_order'), 'special_formatting': self.prefs.get('special_formatting')}) # if self.prefs.get('apply_default_units_behaviour', False) == True: # options_dict.update({'save_units': self.prefs.get('save_units')}) # if self.prefs.get('apply_default_hidden_class_behaviour', False) == True: # options_dict.update({'save_hidden_classes': self.prefs.get('save_hidden_classes')}) if options_dict: self.idf.set_options(options_dict) self.set_dirty(True) def show_search_dialog(self): """Opens the search dialog. """ SearchReplaceDialog(self).show() def find_this(self): """Searches for fields with similar content. """ index = self.classTable.indexAt(self.mouse_position) text = self.classTable.model().data(index, Qt.EditRole) if text: SearchReplaceDialog(self, initial_query=text).show()
class MainWindow(QMainWindow): """ The main window of angr management. """ def __init__(self, file_to_open=None, parent=None): super(MainWindow, self).__init__(parent) icon_location = os.path.join(IMG_LOCATION, 'angr.png') self.setWindowIcon(QIcon(icon_location)) GlobalInfo.main_window = self # initialization self.caption = "angr Management" self.setMinimumSize(QSize(400, 400)) self.setDockNestingEnabled(True) self.workspace = None self.central_widget = None self._plugin_mgr = None # type: PluginManager self._file_toolbar = None # type: FileToolbar self._states_toolbar = None # type: StatesToolbar self._analysis_toolbar = None # type: AnalysisToolbar self._progressbar = None # type: QProgressBar self._load_binary_dialog = None self._status = "" self._progress = None self.defaultWindowFlags = None # menus self._file_menu = None self._analyze_menu = None self._view_menu = None self._help_menu = None self._plugin_menu = None self._sync_menu = None self._init_toolbars() self._init_statusbar() self._init_workspace() self._init_shortcuts() self._init_menus() self._init_plugins() self.showMaximized() # I'm ready to show off! self.show() self.status = "Ready." if file_to_open is not None: self.load_file(file_to_open) def sizeHint(self, *args, **kwargs): return QSize(1200, 800) # # Properties # @property def caption(self): return self.getWindowTitle() @caption.setter def caption(self, v): self.setWindowTitle(v) @property def status(self): return self._status @status.setter def status(self, v): self._status = v self.statusBar().showMessage(v) @property def progress(self): return self._progress @progress.setter def progress(self, v): self._progress = v self._progressbar.show() self._progressbar.setValue(v) # # Dialogs # def _open_mainfile_dialog(self): file_path, _ = QFileDialog.getOpenFileName(self, "Open a binary", ".", "All executables (*);;Windows PE files (*.exe);;Core Dumps (*.core);;angr database (*.adb)", ) return file_path def _pick_image_dialog(self): try: prompt = LoadDockerPrompt() except LoadDockerPromptError: return if prompt.exec_() == 0: return # User canceled return prompt.textValue() def open_load_plugins_dialog(self): try: dlg = LoadPlugins(self._plugin_mgr) dlg.setModal(True) dlg.exec_() except LoadPluginsError: pass def open_newstate_dialog(self): new_state_dialog = NewState(self.workspace.instance, parent=self) new_state_dialog.exec_() def open_doc_link(self): QDesktopServices.openUrl(QUrl("https://docs.angr.io/", QUrl.TolerantMode)) def open_about_dialog(self): QMessageBox.about(self, "About angr", "Version 8") def open_sync_config_dialog(self): if self.workspace.instance.project is None: # project does not exist yet return sync_config = SyncConfig(self.workspace.instance, parent=self) sync_config.exec_() # # Widgets # def _init_statusbar(self): self._progressbar = QProgressBar() self._progressbar.setMinimum(0) self._progressbar.setMaximum(100) self._progressbar.hide() self.statusBar().addPermanentWidget(self._progressbar) def _init_toolbars(self): self._file_toolbar = FileToolbar(self) self._states_toolbar = StatesToolbar(self) self._analysis_toolbar = AnalysisToolbar(self) self.addToolBar(Qt.TopToolBarArea, self._file_toolbar.qtoolbar()) self.addToolBar(Qt.TopToolBarArea, self._states_toolbar.qtoolbar()) self.addToolBar(Qt.TopToolBarArea, self._analysis_toolbar.qtoolbar()) # # Menus # def _init_menus(self): self._file_menu = FileMenu(self) self._analyze_menu = AnalyzeMenu(self) self._view_menu = ViewMenu(self) self._help_menu = HelpMenu(self) self._plugin_menu = PluginMenu(self) self.menuBar().addMenu(self._file_menu.qmenu()) self.menuBar().addMenu(self._view_menu.qmenu()) self.menuBar().addMenu(self._analyze_menu.qmenu()) if has_binsync(): self._sync_menu = SyncMenu(self) self.menuBar().addMenu(self._sync_menu.qmenu()) def on_load(**kwargs): self._sync_menu.action_by_key("config").enable() self.workspace.instance._project_container.am_subscribe(on_load) self.menuBar().addMenu(self._plugin_menu.qmenu()) self.menuBar().addMenu(self._help_menu.qmenu()) # # Workspace # def _init_workspace(self): """ Initialize workspace :return: None """ self.central_widget_main = QSplitter(Qt.Horizontal) self.setCentralWidget(self.central_widget_main) self.central_widget = QMainWindow() self.central_widget2 = QMainWindow() self.central_widget_main.addWidget(self.central_widget) self.central_widget_main.addWidget(self.central_widget2) wk = Workspace(self, Instance()) self.workspace = wk self.workspace.view_manager.tabify_center_views() self.central_widget.setTabPosition(Qt.RightDockWidgetArea, QTabWidget.North) # # Shortcuts # def _init_shortcuts(self): """ Initialize shortcuts :return: None """ center_dockable_views = self.workspace.view_manager.get_center_views() for i in range(1, len(center_dockable_views)+1): QShortcut(QKeySequence('Ctrl+'+str(i)), self, center_dockable_views[i-1].raise_) # Raise the DisassemblyView after everything has initialized center_dockable_views[0].raise_() # # PluginManager # def _init_plugins(self): self._plugin_mgr = PluginManager(self.workspace, autoload=True) # # Event # def resizeEvent(self, event): """ :param QResizeEvent event: :return: """ self._recalculate_view_sizes(event.oldSize()) def closeEvent(self, event): self._plugin_mgr.stop_all_plugin_threads() event.accept() def event(self, event): if event.type() == QEvent.User: # our event callback try: event.result = event.execute() except Exception as e: event.exception = e event.event.set() return True return super(MainWindow, self).event(event) # # Actions # def reload(self): self.workspace.reload() def open_file_button(self): file_path = self._open_mainfile_dialog() self.load_file(file_path) def open_docker_button(self): required = { 'archr: git clone https://github.com/angr/archr && cd archr && pip install -e .':archr, 'keystone: pip install --no-binary keystone-engine keystone-engine':keystone } is_missing = [ key for key, value in required.items() if value is None ] if len(is_missing) > 0: req_msg = 'You need to install the following:\n\n\t' + '\n\t'.join(is_missing) req_msg += '\n\nInstall them to enable this functionality.' req_msg += '\nRelaunch angr-management after install.' QMessageBox(self).critical(None, 'Dependency error', req_msg) return img_name = self._pick_image_dialog() if img_name is None: return target = archr.targets.DockerImageTarget(img_name, target_path=None) self.workspace.instance.add_job(LoadTargetJob(target)) self.workspace.instance.set_image(img_name) def load_file(self, file_path): if os.path.isfile(file_path): if file_path.endswith(".adb"): self.load_database(file_path) else: self.workspace.instance.add_job(LoadBinaryJob(file_path)) def save_database(self): if self.workspace.instance.database_path is None: self.save_database_as() else: self._save_database(self.workspace.instance.database_path) def save_database_as(self): # Open File window file_path, _ = QFileDialog.getSaveFileName( self, "Save angr database", ".", "angr databases (*.adb)", ) if not file_path.endswith(".adb"): file_path = file_path + ".adb" self._save_database(file_path) def quit(self): self.close() def run_variable_recovery(self): self.workspace.view_manager.first_view_in_category('disassembly').variable_recovery_flavor = 'accurate' def run_induction_variable_analysis(self): self.workspace.view_manager.first_view_in_category('disassembly').run_induction_variable_analysis() def decompile_current_function(self): if self.workspace is not None: self.workspace.decompile_current_function() def interact(self): self.workspace.interact_program(self.workspace.instance.img_name) def setup_sync(self): self.open_sync_config_dialog() # # Other public methods # def progress_done(self): self._progress = None self._progressbar.hide() # # Private methods # def _load_database(self, file_path): with open(file_path, "rb") as o: p,cfg,cfb = pickle.load(o) self.workspace.instance.project = p self.workspace.instance.cfg = cfg self.workspace.instance.cfb = cfb self.workspace.reload() self.workspace.on_cfg_generated() self.workspace.instance.database_path = file_path print("DATABASE %s LOADED" % file_path) def _save_database(self, file_path): with open(file_path, "wb") as o: pickle.dump((self.workspace.instance.project, self.workspace.instance.cfg, self.workspace.instance.cfb), o) self.workspace.instance.database_path = file_path print("DATABASE %s SAVED" % file_path) def _recalculate_view_sizes(self, old_size): adjustable_dockable_views = [dock for dock in self.workspace.view_manager.docks if dock.widget().default_docking_position in ('left', 'bottom', )] if not adjustable_dockable_views: return for dock in adjustable_dockable_views: widget = dock.widget() if old_size.width() < 0: dock.old_size = widget.sizeHint() continue if old_size != self.size(): # calculate the width ratio if widget.default_docking_position == 'left': # we want to adjust the width ratio = widget.old_width * 1.0 / old_size.width() new_width = int(self.width() * ratio) widget.width_hint = new_width widget.updateGeometry() elif widget.default_docking_position == 'bottom': # we want to adjust the height ratio = widget.old_height * 1.0 / old_size.height() new_height = int(self.height() * ratio) widget.height_hint = new_height widget.updateGeometry() dock.old_size = widget.size() def _resize_dock_widget(self, dock_widget, new_width, new_height): original_size = dock_widget.size() original_min = dock_widget.minimumSize() original_max = dock_widget.maximumSize() dock_widget.resize(new_width, new_height) if new_width != original_size.width(): if original_size.width() > new_width: dock_widget.setMaximumWidth(new_width) else: dock_widget.setMinimumWidth(new_width) if new_height != original_size.height(): if original_size.height() > new_height: dock_widget.setMaximumHeight(new_height) else: dock_widget.setMinimumHeight(new_height) dock_widget.original_min = original_min dock_widget.original_max = original_max QTimer.singleShot(1, dock_widget.restore_original_size)
class MainWindow(QMainWindow): """ The main window of angr management. """ def __init__(self, file_to_open=None, parent=None): super(MainWindow, self).__init__(parent) icon_location = os.path.join(IMG_LOCATION, 'angr.png') self.setWindowIcon(QIcon(icon_location)) GlobalInfo.main_window = self # initialization self.caption = "angr Management" self.setMinimumSize(QSize(400, 400)) self.setDockNestingEnabled(True) self.workspace = None self.central_widget = None self._file_toolbar = None # type: FileToolbar self._states_toolbar = None # type: StatesToolbar self._analysis_toolbar = None # type: AnalysisToolbar self._progressbar = None # type: QProgressBar self._load_binary_dialog = None self._status = "" self._progress = None self._init_menus() self._init_toolbars() self._init_statusbar() self._init_workspace() self.showMaximized() # I'm ready to show off! self.show() self.status = "Ready." if file_to_open is not None: # load a binary self._open_loadbinary_dialog(file_to_open) # # Properties # @property def caption(self): return self.getWindowTitle() @caption.setter def caption(self, v): self.setWindowTitle(v) @property def status(self): return self._status @status.setter def status(self, v): self._status = v self.statusBar().showMessage(v) @property def progress(self): return self._progress @progress.setter def progress(self, v): self._progress = v self._progressbar.show() self._progressbar.setValue(v) # # Dialogs # def _open_loadbinary_dialog(self, file_to_open): try: self._load_binary_dialog = LoadBinary(file_to_open) self._load_binary_dialog.setModal(True) self._load_binary_dialog.exec_() if self._load_binary_dialog.cfg_args is not None: # load the binary self._load_binary( file_to_open, load_options=self._load_binary_dialog.load_options, cfg_args=self._load_binary_dialog.cfg_args) except LoadBinaryError: pass def open_newstate_dialog(self): new_state_dialog = NewState(self.workspace, parent=self) new_state_dialog.exec_() # # Widgets # def _init_statusbar(self): self._progressbar = QProgressBar() self._progressbar.setMinimum(0) self._progressbar.setMaximum(100) self._progressbar.hide() self.statusBar().addPermanentWidget(self._progressbar) def _init_toolbars(self): self._file_toolbar = FileToolbar(self) self._states_toolbar = StatesToolbar(self) self._analysis_toolbar = AnalysisToolbar(self) self.addToolBar(Qt.TopToolBarArea, self._file_toolbar.qtoolbar()) self.addToolBar(Qt.TopToolBarArea, self._states_toolbar.qtoolbar()) self.addToolBar(Qt.TopToolBarArea, self._analysis_toolbar.qtoolbar()) # # Menus # def _init_menus(self): fileMenu = FileMenu(self) self.menuBar().addMenu(fileMenu.qmenu()) # # Workspace # def _init_workspace(self): self.central_widget = QMainWindow() self.setCentralWidget(self.central_widget) wk = Workspace(self, Instance()) self.workspace = wk right_dockable_views = [ dock for dock in self.workspace.dockable_views if dock.widget().default_docking_position == 'right' ] for d0, d1 in zip(right_dockable_views, right_dockable_views[1:]): self.central_widget.tabifyDockWidget(d0, d1) right_dockable_views[0].raise_() self.central_widget.setTabPosition(Qt.RightDockWidgetArea, QTabWidget.North) # # Event # def resizeEvent(self, event): """ :param QResizeEvent event: :return: """ pass # self._recalculate_view_sizes(event.oldSize()) def event(self, event): if event.type() == QEvent.User: # our event callback try: event.result = event.execute() except Exception as e: event.exception = e event.event.set() return True return super(MainWindow, self).event(event) # # Actions # def reload(self): self.workspace.reload() def load_binary(self): # Open File window file_path, _ = QFileDialog.getOpenFileName( self, "Open a binary", ".", "All executables (*);;Windows PE files (*.exe);;Core Dumps (*.core)", ) if os.path.isfile(file_path): self._open_loadbinary_dialog(file_path) def quit(self): self.close() def run_variable_recovery(self): self.workspace.views_by_category['disassembly'][ 0].variable_recovery_flavor = 'accurate' def run_induction_variable_analysis(self): self.workspace.views_by_category['disassembly'][ 0].run_induction_variable_analysis() # # Other public methods # def progress_done(self): self._progress = None self._progressbar.hide() # # Private methods # def _load_binary(self, file_path, load_options=None, cfg_args=None): if load_options is None: load_options = {} if cfg_args is None: cfg_args = {} proj = angr.Project(file_path, load_options=load_options) self.workspace.instance.set_project(proj) self.workspace.instance.initialize(cfg_args=cfg_args) def _recalculate_view_sizes(self, old_size): adjustable_dockable_views = [ dock for dock in self.workspace.dockable_views if dock.widget().default_docking_position in ('left', 'bottom', 'right') ] if not adjustable_dockable_views: return for dock in adjustable_dockable_views: widget = dock.widget() if old_size.width() < 0: dock.old_size = widget.sizeHint() continue if old_size != self.size(): # calculate the width ratio if widget.default_docking_position == 'left': # we want to adjust the width ratio = dock.old_size.width() * 1.0 / old_size.width() new_width = int(self.width() * ratio) self._resize_dock_widget(dock, new_width, widget.height()) elif widget.default_docking_position == 'bottom': # we want to adjust the height ratio = dock.old_size.height() * 1.0 / old_size.height() new_height = int(self.height() * ratio) self._resize_dock_widget(dock, widget.width(), new_height) dock.old_size = widget.size() def _resize_dock_widget(self, dock_widget, new_width, new_height): original_size = dock_widget.size() original_min = dock_widget.minimumSize() original_max = dock_widget.maximumSize() dock_widget.resize(new_width, new_height) if new_width != original_size.width(): if original_size.width() > new_width: dock_widget.setMaximumWidth(new_width) else: dock_widget.setMinimumWidth(new_width) if new_height != original_size.height(): if original_size.height() > new_height: dock_widget.setMaximumHeight(new_height) else: dock_widget.setMinimumHeight(new_height) dock_widget.original_min = original_min dock_widget.original_max = original_max QTimer.singleShot(1, dock_widget.restore_original_size)
class SetupWizLoadPage(QWizardPage): """ :param parent: :type parent: :param version: :type version: """ def __init__(self, parent, version): super(SetupWizLoadPage, self).__init__(parent) self.version = version self.complete = False self.setTitle('Browse for the IDD file') self.setSubTitle('Browse for the specified IDD version below.') self.setup_page() def setup_page(self): """Create intro text """ text = "The file being loaded requires an IDD file of <b>Version {}</b>. " \ "Please choose the 'Energy+.idd' file from the installation directory " \ "for this version of EnergyPlus.".format(self.version) intro_text = QLabel(text) intro_text.setWordWrap(True) # Create the button to browse for the idd file browse_button = QPushButton("Browse for Energy+.idd v{} in the EnergyPlus " "install directory".format(self.version)) browse_button.clicked.connect(self.load_idd) # Create and configure the progress bar self.progress_bar = QProgressBar(self) self.progress_bar.setRange(0, 100) self.progress_bar.hide() # Create and assign layout layout = QVBoxLayout() layout.addWidget(intro_text) layout.addWidget(browse_button) layout.addWidget(self.progress_bar) self.setLayout(layout) def load_idd(self): """Create and open file dialog """ directory = os.path.expanduser('~') formats = "EnergyPlus IDD Files (*.idd)" dialog_name = 'Select EnergyPlus IDD File (Version: {})'.format(self.version) file_dialog = QFileDialog() dir_name, filt = file_dialog.getOpenFileName(self, dialog_name, directory, formats) if not dir_name: self.complete = False return # Show progress bar and parse IDD file self.progress_bar.show() log.debug("Processing IDD file") idd_parser = parser.IDDParser() for progress in idd_parser.parse_idd(dir_name): self.progress_bar.setValue(progress) # Upon completion set complete status to true and inform object self.complete = True self.completeChanged.emit() def isComplete(self): """ :return: :rtype: """ return True if self.complete else False
class AppDiv: """ +------1--------------2------------3-------------------+ | +--------+ +----------------+ | | | label | --label---name-- | install|version | ⚙ | | | image | +-----------------+ | | +--------+ --label----describe---- | | 4 | | -------progressbar------------ ----progressmsg-- | +------------------------------------------------------+ """ not_widget = ['not_widget', 'job', 'widget'] def __init__(self, widget): self.widget = widget self.job = AppDivJob() self.job.progressbar_signal.connect(self.progressbar_signal_slot) self.job.msg_signal.connect(self.msg_signal_slot) self.job.action_signal.connect(self.action_signal_slot) self.job.switch_signal.connect(self.progress_switch) self.layout = QVBoxLayout() self.layout.setMargin(10) self.app_layout = QHBoxLayout() ### 1 self.icon = QLabel(self.widget) #### 2 self.name = QLabel(self.widget) self.name.setAlignment(Qt.AlignCenter) self.name.setStyleSheet(""" border:1px solid #1b89ca; border-radius:5px; """) self.desc = MyLabel(self.widget) self.desc.setAlignment(Qt.AlignCenter) self.desc_layout = QVBoxLayout() self.desc_layout.addWidget(self.name) self.desc_layout.addWidget(self.desc) #### 3 self.menu = QMenu(self.widget) self.action = QToolButton(self.widget) self.action.setPopupMode(QToolButton.MenuButtonPopup) self.action.setMenu(self.menu) ### self.app_layout.addWidget(self.icon) self.app_layout.addLayout(self.desc_layout) self.app_layout.addWidget(self.action) self.app_layout.setStretch(0, 3) self.app_layout.setStretch(1, 4) self.app_layout.setStretch(2, 3) ## 4 self.progress_layout = QHBoxLayout() self.progressbar = QProgressBar() self.progressbar.setFixedHeight(1) self.progressbar.setTextVisible(False) self.progressbar.setRange(0, 0) self.progressbar.setVisible(False) self.progress_msg = QLabel(self.widget) self.progress_msg.setVisible(False) self.progress_layout.addWidget(self.progressbar) self.progress_layout.addWidget(self.progress_msg) self.progress_layout.setStretch(0, 3) self.progress_layout.setStretch(1, 7) self.layout.addLayout(self.app_layout) self.layout.addLayout(self.progress_layout) def progress_switch(self): if self.progressbar.isHidden(): self.progressbar.show() self.progress_msg.show() else: self.progress_msg.hide() self.progressbar.hide() def progressbar_signal_slot(self, data: dict): if data.get('range'): self.progressbar.setRange(*data['range']) if data.get('value'): self.progressbar.setValue(data['value']) def msg_signal_slot(self, msg: str): fontWidth = QFontMetrics(self.progress_msg.font()) elideNote = fontWidth.elidedText( msg, Qt.ElideRight, self.name.width() + self.action.width() - 30) self.progress_msg.setText(elideNote) def action_signal_slot(self, act: str): self.action.setText(act) def add_installed_layout(self, data): self.widget.job.install_signal.emit(data)