Exemplo n.º 1
0
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()
Exemplo n.º 2
0
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
Exemplo n.º 3
0
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)
Exemplo n.º 4
0
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()
Exemplo n.º 6
0
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)
Exemplo n.º 7
0
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)
Exemplo n.º 9
0
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)
Exemplo n.º 10
0
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
Exemplo n.º 11
0
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)