예제 #1
0
class ObjectBrowser(FMainWindow):
    """ Object browser main application window.
    """
    _n_instances = 0

    def __init__(self,
                 obj,
                 name='',
                 attribute_columns=DEFAULT_ATTR_COLS,
                 attribute_details=DEFAULT_ATTR_DETAILS,
                 show_routine_attributes=None,
                 show_special_attributes=None,
                 reset=False):
        """ Constructor
        
            :param obj: any Python object or variable
            :param name: name of the object as it will appear in the root node
            :param attribute_columns: list of AttributeColumn objects that define which columns
                are present in the table and their defaults
            :param attribute_details: list of AttributeDetails objects that define which attributes
                can be selected in the details pane.
            :param show_routine_attributes: if True rows where the 'is attribute' and 'is routine'
                columns are both True, are displayed. Otherwise they are hidden. 
            :param show_special_attributes: if True rows where the 'is attribute' is True and
                the object name starts and ends with two underscores, are displayed. Otherwise 
                they are hidden.
            :param reset: If true the persistent settings, such as column widths, are reset. 
        """
        super(ObjectBrowser, self).__init__()

        ObjectBrowser._n_instances += 1
        self._instance_nr = self._n_instances

        # Model
        self._attr_cols = attribute_columns
        self._attr_details = attribute_details

        (show_routine_attributes,
         show_special_attributes) = self._readModelSettings(
             reset=reset,
             show_routine_attributes=show_routine_attributes,
             show_special_attributes=show_special_attributes)
        self.show_routine_attributes = show_routine_attributes
        self.show_special_attributes = show_special_attributes

        # Views
        self._setup_views()

        self.setModel(obj)

        self._setup_actions()
        self._setup_menu()

        self.setWindowTitle("{} - {}".format(PROGRAM_NAME, name))
        app = QtGui.QApplication.instance()
        app.lastWindowClosed.connect(app.quit)

        self.initSize()

        self.titleBar().skinButton.hide()
        self.titleBar().closeButton.clicked.connect(self.close)

        self.setWindowIcon(":/icons/dark/appbar.tree.png")

        self.titleBar().modeButton.click()

        self.toggle_callable_action.toggled.emit(False)
        self.toggle_special_attribute_action.toggled.emit(False)

        self.setskin()

    def setskin(self, skinID="BB"):
        path = os.path.dirname(__file__)
        # setSkinForApp( os.path.join(path, '%s.qss' % skinID))  # 设置主窗口样式
        fd = open(os.path.join(path, '%s.qss' % skinID), "r")
        style = fd.read()
        fd.close()
        self.setStyleSheet(style)

    def initSize(self):
        desktopWidth = QtGui.QDesktopWidget().availableGeometry().width()
        desktopHeight = QtGui.QDesktopWidget().availableGeometry().height()
        self.resize(desktopWidth * 0.6, desktopHeight * 0.8)
        self.moveCenter()

    def setModel(self, obj):
        self._tree_model = TreeModel(
            obj,
            root_obj_name='',
            attr_cols=self._attr_cols,
            show_routine_attributes=self.show_routine_attributes,
            show_special_attributes=self.show_special_attributes)

        self.obj_tree.setModel(self._tree_model)

        first_row = self._tree_model.first_item_index()
        self.obj_tree.setCurrentIndex(first_row)

        # Connect signals
        selection_model = self.obj_tree.selectionModel()
        selection_model.currentChanged.connect(self._update_details)

    def _make_show_column_function(self, column_idx):
        """ Creates a function that shows or hides a column."""
        show_column = lambda checked: self.obj_tree.setColumnHidden(
            column_idx, not checked)
        return show_column

    def _setup_actions(self):
        """ Creates the main window actions.
        """
        # Show/hide callable objects
        self.toggle_callable_action = \
            QtGui.QAction("Show routine attributes", self, checkable=True,
                          statusTip = "Shows/hides attributes that are routings (functions, methods, etc)")
        self.toggle_callable_action.toggled.connect(self.toggle_callables)

        # Show/hide special attributes
        self.toggle_special_attribute_action = \
            QtGui.QAction("Show __special__ attributes", self, checkable=True,
                          statusTip = "Shows or hides __special__ attributes")
        self.toggle_special_attribute_action.toggled.connect(
            self.toggle_special_attributes)

    def _setup_menu(self):
        """ Sets up the main menu.
        """
        # file_menu = self.menuBar().addMenu("&File")
        self.menu = QtGui.QMenu(self)
        self.menu.addAction("C&lose", self.close_window, "Ctrl+W")
        self.menu.addAction("E&xit", self.quit_application, "Ctrl+Q")
        if DEBUGGING is True:
            self.menu.addSeparator()
            self.menu.addAction("&Test", self.my_test, "Ctrl+T")

        self.show_cols_submenu = self.menu.addMenu("Table columns")
        self.menu.addAction(self.toggle_callable_action)
        self.menu.addAction(self.toggle_special_attribute_action)

        self.titleBar().settingDownButton.setMenu(self.menu)
        self.titleBar().settingMenuShowed.connect(
            self.titleBar().settingDownButton.showMenu)

    def _setup_views(self):
        """ Creates the UI widgets. 
        """
        self.central_splitter = QtGui.QSplitter(self,
                                                orientation=QtCore.Qt.Vertical)
        self.setCentralWidget(self.central_splitter)
        # central_layout = QtGui.QVBoxLayout()
        # self.central_splitter.setLayout(central_layout)

        # Tree widget
        self.obj_tree = ToggleColumnTreeView()
        self.obj_tree.setAlternatingRowColors(True)
        self.obj_tree.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
        self.obj_tree.setUniformRowHeights(True)
        self.obj_tree.setAnimated(True)
        self.obj_tree.add_header_context_menu()

        # Stretch last column?
        # It doesn't play nice when columns are hidden and then shown again.
        obj_tree_header = self.obj_tree.header()
        # obj_tree_header.setMovable(True)
        obj_tree_header.setStretchLastSection(False)
        for action in self.obj_tree.toggle_column_actions_group.actions():
            self.show_cols_submenu.addAction(action)

        self.central_splitter.addWidget(self.obj_tree)

        # Bottom pane
        bottom_pane_widget = QtGui.QWidget()
        bottom_layout = QtGui.QHBoxLayout()
        bottom_layout.setSpacing(0)
        bottom_layout.setContentsMargins(5, 5, 5, 5)  # left top right bottom
        bottom_pane_widget.setLayout(bottom_layout)
        self.central_splitter.addWidget(bottom_pane_widget)

        group_box = QtGui.QGroupBox("Details")
        bottom_layout.addWidget(group_box)

        group_layout = QtGui.QHBoxLayout()
        group_layout.setContentsMargins(2, 2, 2, 2)  # left top right bottom
        group_box.setLayout(group_layout)

        # Radio buttons
        radio_widget = QtGui.QWidget()
        radio_layout = QtGui.QVBoxLayout()
        radio_layout.setContentsMargins(0, 0, 0, 0)  # left top right bottom
        radio_widget.setLayout(radio_layout)

        self.button_group = QtGui.QButtonGroup(self)
        for button_id, attr_detail in enumerate(self._attr_details):
            radio_button = QtGui.QRadioButton(attr_detail.name)
            radio_layout.addWidget(radio_button)
            self.button_group.addButton(radio_button, button_id)

        self.button_group.buttonClicked[int].connect(
            self._change_details_field)
        self.button_group.button(0).setChecked(True)

        radio_layout.addStretch(1)
        group_layout.addWidget(radio_widget)

        # Editor widget
        font = QtGui.QFont()
        font.setFamily('Courier')
        font.setFixedPitch(True)
        #font.setPointSize(14)

        self.editor = QtGui.QPlainTextEdit()
        self.editor.setReadOnly(True)
        self.editor.setFont(font)
        group_layout.addWidget(self.editor)

        # Splitter parameters
        self.central_splitter.setCollapsible(0, False)
        self.central_splitter.setCollapsible(1, True)
        self.central_splitter.setSizes([400, 200])
        self.central_splitter.setStretchFactor(0, 10)
        self.central_splitter.setStretchFactor(1, 0)

    # End of setup_methods

    def _settings_group_name(self, prefix):
        """ The persistent settings are stored per combination of column names
            and windows instance number.
        """
        column_names = ",".join([col.name for col in self._attr_cols])
        settings_str = column_names + str(self._instance_nr)
        settings_grp = "{}_{}".format(prefix, hex(hash(settings_str)))
        logger.debug("  settings group is: {!r}".format(settings_grp))
        return settings_grp

    def _readModelSettings(self,
                           reset=False,
                           show_routine_attributes=None,
                           show_special_attributes=None):
        """ Reads the persistent model settings .
            The persistent settings (show_routine_attributes, show_special_attributes) can be \
            overridden by giving it a True or False value.
            If reset is True and the setting is None, True is used as default.
        """
        default_sra = True
        default_ssa = True
        if reset:
            logger.debug("Resetting persistent model settings")
            if show_routine_attributes is None:
                show_routine_attributes = default_sra
            if show_special_attributes is None:
                show_special_attributes = default_ssa
        else:
            logger.debug("Reading model settings for window: {:d}".format(
                self._instance_nr))
            settings = QtCore.QSettings()
            settings.beginGroup(self._settings_group_name('model'))
            if show_routine_attributes is None:
                show_routine_attributes = setting_str_to_bool(
                    settings.value("show_routine_attributes", default_sra))
            if show_special_attributes is None:
                show_special_attributes = setting_str_to_bool(
                    settings.value("show_special_attributes", default_ssa))
            settings.endGroup()
            logger.debug("read show_routine_attributes: {!r}".format(
                show_routine_attributes))
            logger.debug("read show_special_attributes: {!r}".format(
                show_special_attributes))
        return (show_routine_attributes, show_special_attributes)

    def _writeModelSettings(self):
        """ Writes the model settings to the persistent store
        """
        logger.debug("Writing model settings for window: {:d}".format(
            self._instance_nr))

        settings = QtCore.QSettings()
        settings.beginGroup(self._settings_group_name('model'))
        logger.debug("writing show_routine_attributes: {!r}".format(
            self._tree_model.getShowCallables()))
        logger.debug("wrting show_special_attributes: {!r}".format(
            self._tree_model.getShowSpecialAttributes()))
        settings.setValue("show_routine_attributes",
                          self._tree_model.getShowCallables())
        settings.setValue("show_special_attributes",
                          self._tree_model.getShowSpecialAttributes())
        settings.endGroup()

    def _readViewSettings(self, reset=False):
        """ Reads the persistent program settings
        
            :param reset: If True, the program resets to its default settings
        """
        pos = QtCore.QPoint(20 * self._instance_nr, 20 * self._instance_nr)
        window_size = QtCore.QSize(1024, 700)
        details_button_idx = 0

        header = self.obj_tree.header()
        header_restored = False

        if reset:
            logger.debug("Resetting persistent view settings")
        else:
            logger.debug("Reading view settings for window: {:d}".format(
                self._instance_nr))
            settings = QtCore.QSettings()
            settings.beginGroup(self._settings_group_name('view'))
            pos = settings.value("main_window/pos", pos)
            window_size = settings.value("main_window/size", window_size)
            details_button_idx = int(
                settings.value("details_button_idx", details_button_idx))
            self.central_splitter.restoreState(
                settings.value("central_splitter/state"))
            header_restored = self.obj_tree.read_view_settings(
                'table/header_state', settings, reset)
            settings.endGroup()

        if not header_restored:
            column_sizes = [col.width for col in self._attr_cols]
            column_visible = [col.col_visible for col in self._attr_cols]

            for idx, size in enumerate(column_sizes):
                if size > 0:  # Just in case
                    header.resizeSection(idx, size)

            for idx, visible in enumerate(column_visible):
                self.obj_tree.toggle_column_actions_group.actions(
                )[idx].setChecked(visible)

        self.resize(window_size)
        self.move(pos)
        button = self.button_group.button(details_button_idx)
        if button is not None:
            button.setChecked(True)

    def _writeViewSettings(self):
        """ Writes the view settings to the persistent store
        """
        logger.debug("Writing view settings for window: {:d}".format(
            self._instance_nr))

        settings = QtCore.QSettings()
        settings.beginGroup(self._settings_group_name('view'))
        self.obj_tree.write_view_settings("table/header_state", settings)
        settings.setValue("central_splitter/state",
                          self.central_splitter.saveState())
        settings.setValue("details_button_idx", self.button_group.checkedId())
        settings.setValue("main_window/pos", self.pos())
        settings.setValue("main_window/size", self.size())
        settings.endGroup()

    @QtCore.Slot(QtCore.QModelIndex, QtCore.QModelIndex)
    def _update_details(self, current_index, _previous_index):
        """ Shows the object details in the editor given an index.
        """
        tree_item = self._tree_model.treeItem(current_index)
        self._update_details_for_item(tree_item)

    def _change_details_field(self, _button_id=None):
        """ Changes the field that is displayed in the details pane
        """
        #logger.debug("_change_details_field: {}".format(_button_id))
        current_index = self.obj_tree.selectionModel().currentIndex()
        tree_item = self._tree_model.treeItem(current_index)
        self._update_details_for_item(tree_item)

    def _update_details_for_item(self, tree_item):
        """ Shows the object details in the editor given an tree_item
        """
        self.editor.setStyleSheet("color: black;")
        try:
            #obj = tree_item.obj
            button_id = self.button_group.checkedId()
            assert button_id >= 0, "No radio button selected. Please report this bug."
            attr_details = self._attr_details[button_id]
            data = attr_details.data_fn(tree_item)
            self.editor.setPlainText(data)
            self.editor.setWordWrapMode(attr_details.line_wrap)

        except StandardError, ex:
            self.editor.setStyleSheet("color: red;")
            stack_trace = traceback.format_exc()
            self.editor.setPlainText("{}{}".format(ex, stack_trace))
            self.editor.setWordWrapMode(
                QtGui.QTextOption.WrapAtWordBoundaryOrAnywhere)
            if DEBUGGING is True:
                raise
class ObjectBrowser(QtWidgets.QMainWindow):
    """ Object browser main application window.
    """
    _q_app = None  # Reference to the global application.
    _browsers = []  # Keep lists of browser windows.

    def __init__(
            self,
            obj,
            name='',
            attribute_columns=DEFAULT_ATTR_COLS,
            attribute_details=DEFAULT_ATTR_DETAILS,
            show_callable_attributes=None,  # None uses value from QSettings
            show_special_attributes=None,  # None uses value from QSettings
            auto_refresh=None,  # None uses value from QSettings
            refresh_rate=None,  # None uses value from QSettings
            reset=False):
        """ Constructor
        
            :param obj: any Python object or variable
            :param name: name of the object as it will appear in the root node
            :param attribute_columns: list of AttributeColumn objects that define which columns
                are present in the table and their defaults
            :param attribute_details: list of AttributeDetails objects that define which attributes
                can be selected in the details pane.
            :param show_callable_attributes: if True rows where the 'is attribute' and 'is callable'
                columns are both True, are displayed. Otherwise they are hidden. 
            :param show_special_attributes: if True rows where the 'is attribute' is True and
                the object name starts and ends with two underscores, are displayed. Otherwise 
                they are hidden.
            :param auto_refresh: If True, the contents refershes itsef every <refresh_rate> seconds.
            :param refresh_rate: number of seconds between automatic refreshes. Default = 2 .
            :param reset: If true the persistent settings, such as column widths, are reset. 
        """
        super(ObjectBrowser, self).__init__()

        self._instance_nr = self._add_instance()

        # Model
        self._attr_cols = attribute_columns
        self._attr_details = attribute_details

        (self._auto_refresh, self._refresh_rate, show_callable_attributes, show_special_attributes) = \
            self._readModelSettings(reset = reset,
                                    auto_refresh = auto_refresh,
                                    refresh_rate = refresh_rate,
                                    show_callable_attributes= show_callable_attributes,
                                    show_special_attributes = show_special_attributes)

        self._tree_model = TreeModel(obj, name, attr_cols=self._attr_cols)

        self._proxy_tree_model = TreeProxyModel(
            show_callable_attributes=show_callable_attributes,
            show_special_attributes=show_special_attributes)

        self._proxy_tree_model.setSourceModel(self._tree_model)
        #self._proxy_tree_model.setSortRole(RegistryTableModel.SORT_ROLE)
        self._proxy_tree_model.setDynamicSortFilter(True)
        #self._proxy_tree_model.setSortCaseSensitivity(Qt.CaseInsensitive)

        # Views
        self._setup_actions()
        self._setup_menu()
        self._setup_views()
        self.setWindowTitle("{} - {}".format(PROGRAM_NAME, name))

        self._readViewSettings(reset=reset)

        assert self._refresh_rate > 0, "refresh_rate must be > 0. Got: {}".format(
            self._refresh_rate)
        self._refresh_timer = QtCore.QTimer(self)
        self._refresh_timer.setInterval(self._refresh_rate * 1000)
        self._refresh_timer.timeout.connect(self.refresh)

        # Update views with model
        self.toggle_special_attribute_action.setChecked(
            show_special_attributes)
        self.toggle_callable_action.setChecked(show_callable_attributes)
        self.toggle_auto_refresh_action.setChecked(self._auto_refresh)

        # Select first row so that a hidden root node will not be selected.
        first_row_index = self._proxy_tree_model.firstItemIndex()
        self.obj_tree.setCurrentIndex(first_row_index)
        if self._tree_model.inspectedNodeIsVisible:
            self.obj_tree.expand(first_row_index)

    def refresh(self):
        """ Refreshes object brawser contents
        """
        logger.debug("Refreshing")
        self._tree_model.refreshTree()

    def _add_instance(self):
        """ Adds the browser window to the list of browser references.
            If a None is present in the list it is inserted at that position, otherwise
            it is appended to the list. The index number is returned.
            
            This mechanism is used so that repeatedly creating and closing windows does not
            increase the instance number, which is used in writing the persistent settings.
        """
        try:
            idx = self._browsers.index(None)
        except ValueError:
            self._browsers.append(self)
            idx = len(self._browsers) - 1
        else:
            self._browsers[idx] = self

        return idx

    def _remove_instance(self):
        """ Sets the reference in the browser list to None. 
        """
        idx = self._browsers.index(self)
        self._browsers[idx] = None

    def _make_show_column_function(self, column_idx):
        """ Creates a function that shows or hides a column."""
        show_column = lambda checked: self.obj_tree.setColumnHidden(
            column_idx, not checked)
        return show_column

    def _setup_actions(self):
        """ Creates the main window actions.
        """
        # Show/hide callable objects
        self.toggle_callable_action = \
            QtWidgets.QAction("Show callable attributes", self, checkable=True,
                          shortcut = QtGui.QKeySequence("Alt+C"),
                          statusTip = "Shows/hides attributes that are callable (functions, methods, etc)")
        self.toggle_callable_action.toggled.connect(
            self._proxy_tree_model.setShowCallables)

        # Show/hide special attributes
        self.toggle_special_attribute_action = \
            QtWidgets.QAction("Show __special__ attributes", self, checkable=True,
                          shortcut = QtGui.QKeySequence("Alt+S"),
                          statusTip = "Shows or hides __special__ attributes")
        self.toggle_special_attribute_action.toggled.connect(
            self._proxy_tree_model.setShowSpecialAttributes)

        # Toggle auto-refresh on/off
        self.toggle_auto_refresh_action = \
            QtWidgets.QAction("Auto-refresh", self, checkable=True,
                          statusTip = "Auto refresh every {} seconds".format(self._refresh_rate))
        self.toggle_auto_refresh_action.toggled.connect(
            self.toggle_auto_refresh)

        # Add another refresh action with a different short cut. An action must be added to
        # a visible widget for it to receive events. It is added to the main windows to prevent it
        # from being displayed again in the menu
        self.refresh_action_f5 = QtWidgets.QAction(self,
                                                   text="&Refresh2",
                                                   shortcut="F5")
        self.refresh_action_f5.triggered.connect(self.refresh)
        self.addAction(self.refresh_action_f5)

    def _setup_menu(self):
        """ Sets up the main menu.
        """
        file_menu = self.menuBar().addMenu("&File")
        file_menu.addAction("C&lose", self.close, "Ctrl+W")
        file_menu.addAction("E&xit", self.quit_application, "Ctrl+Q")
        if DEBUGGING is True:
            file_menu.addSeparator()
            file_menu.addAction("&Test", self.my_test, "Ctrl+T")

        view_menu = self.menuBar().addMenu("&View")
        view_menu.addAction("&Refresh", self.refresh, "Ctrl+R")
        view_menu.addAction(self.toggle_auto_refresh_action)

        view_menu.addSeparator()
        self.show_cols_submenu = view_menu.addMenu("Table columns")
        view_menu.addSeparator()
        view_menu.addAction(self.toggle_callable_action)
        view_menu.addAction(self.toggle_special_attribute_action)

        self.menuBar().addSeparator()
        help_menu = self.menuBar().addMenu("&Help")
        help_menu.addAction('&About', self.about)

    def _setup_views(self):
        """ Creates the UI widgets. 
        """
        self.central_splitter = QtWidgets.QSplitter(
            self, orientation=QtCore.Qt.Vertical)
        self.setCentralWidget(self.central_splitter)

        # Tree widget
        self.obj_tree = ToggleColumnTreeView()
        self.obj_tree.setAlternatingRowColors(True)
        self.obj_tree.setModel(self._proxy_tree_model)
        self.obj_tree.setSelectionBehavior(
            QtWidgets.QAbstractItemView.SelectRows)
        self.obj_tree.setUniformRowHeights(True)
        self.obj_tree.setAnimated(True)
        self.obj_tree.add_header_context_menu()

        # Stretch last column?
        # It doesn't play nice when columns are hidden and then shown again.
        obj_tree_header = self.obj_tree.header()
        obj_tree_header.setSectionsMovable(True)
        obj_tree_header.setStretchLastSection(False)
        for action in self.obj_tree.toggle_column_actions_group.actions():
            self.show_cols_submenu.addAction(action)

        self.central_splitter.addWidget(self.obj_tree)

        # Bottom pane
        bottom_pane_widget = QtWidgets.QWidget()
        bottom_layout = QtWidgets.QHBoxLayout()
        bottom_layout.setSpacing(0)
        bottom_layout.setContentsMargins(5, 5, 5, 5)  # left top right bottom
        bottom_pane_widget.setLayout(bottom_layout)
        self.central_splitter.addWidget(bottom_pane_widget)

        group_box = QtWidgets.QGroupBox("Details")
        bottom_layout.addWidget(group_box)

        group_layout = QtWidgets.QHBoxLayout()
        group_layout.setContentsMargins(2, 2, 2, 2)  # left top right bottom
        group_box.setLayout(group_layout)

        # Radio buttons
        radio_widget = QtWidgets.QWidget()
        radio_layout = QtWidgets.QVBoxLayout()
        radio_layout.setContentsMargins(0, 0, 0, 0)  # left top right bottom
        radio_widget.setLayout(radio_layout)

        self.button_group = QtWidgets.QButtonGroup(self)
        for button_id, attr_detail in enumerate(self._attr_details):
            radio_button = QtWidgets.QRadioButton(attr_detail.name)
            radio_layout.addWidget(radio_button)
            self.button_group.addButton(radio_button, button_id)

        self.button_group.buttonClicked[int].connect(
            self._change_details_field)
        self.button_group.button(0).setChecked(True)

        radio_layout.addStretch(1)
        group_layout.addWidget(radio_widget)

        # Editor widget
        font = QtGui.QFont()
        font.setFamily('Courier')
        font.setFixedPitch(True)
        #font.setPointSize(14)

        self.editor = QtWidgets.QPlainTextEdit()
        self.editor.setReadOnly(True)
        self.editor.setFont(font)
        group_layout.addWidget(self.editor)

        # Splitter parameters
        self.central_splitter.setCollapsible(0, False)
        self.central_splitter.setCollapsible(1, True)
        self.central_splitter.setSizes([400, 200])
        self.central_splitter.setStretchFactor(0, 10)
        self.central_splitter.setStretchFactor(1, 0)

        # Connect signals
        # Keep a temporary reference of the selection_model to prevent segfault in PySide.
        # See http://permalink.gmane.org/gmane.comp.lib.qt.pyside.devel/222
        selection_model = self.obj_tree.selectionModel()
        selection_model.currentChanged.connect(self._update_details)

    # End of setup_methods

    def _settings_group_name(self, postfix):
        """ Constructs a group name for the persistent settings.
            
            Because the columns in the main table are extendible, we must store the settings
            in a different group if a different combination of columns is used. Therefore the
            settings group name contains a hash that is calculated from the used column names.
            Furthermore the window number is included in the settings group name. Finally a
            postfix string is appended. 
        """
        column_names = ",".join([col.name for col in self._attr_cols])
        settings_str = column_names
        columns_hash = hashlib.md5(settings_str.encode('utf-8')).hexdigest()
        settings_grp = "{}_win{}_{}".format(columns_hash, self._instance_nr,
                                            postfix)
        return settings_grp

    def _readModelSettings(self,
                           reset=False,
                           auto_refresh=None,
                           refresh_rate=None,
                           show_callable_attributes=None,
                           show_special_attributes=None):
        """ Reads the persistent model settings .
            The persistent settings (show_callable_attributes, show_special_attributes) can be \
            overridden by giving it a True or False value.
            If reset is True and the setting is None, True is used as default.
        """
        default_auto_refresh = False
        default_refresh_rate = 2
        default_sra = True
        default_ssa = True
        if reset:
            logger.debug("Resetting persistent model settings")
            if refresh_rate is None:
                refresh_rate = default_refresh_rate
            if auto_refresh is None:
                auto_refresh = default_auto_refresh
            if show_callable_attributes is None:
                show_callable_attributes = default_sra
            if show_special_attributes is None:
                show_special_attributes = default_ssa
        else:
            logger.debug("Reading model settings for window: {:d}".format(
                self._instance_nr))
            settings = get_qsettings()
            settings.beginGroup(self._settings_group_name('model'))

            if auto_refresh is None:
                auto_refresh = setting_str_to_bool(
                    settings.value("auto_refresh", default_auto_refresh))
            logger.debug("read auto_refresh: {!r}".format(auto_refresh))

            if refresh_rate is None:
                refresh_rate = float(
                    settings.value("refresh_rate", default_refresh_rate))
            logger.debug("read refresh_rate: {!r}".format(refresh_rate))

            if show_callable_attributes is None:
                show_callable_attributes = setting_str_to_bool(
                    settings.value("show_callable_attributes", default_sra))
            logger.debug("read show_callable_attributes: {!r}".format(
                show_callable_attributes))

            if show_special_attributes is None:
                show_special_attributes = setting_str_to_bool(
                    settings.value("show_special_attributes", default_ssa))
            logger.debug("read show_special_attributes: {!r}".format(
                show_special_attributes))

            settings.endGroup()

        return (auto_refresh, refresh_rate, show_callable_attributes,
                show_special_attributes)

    def _writeModelSettings(self):
        """ Writes the model settings to the persistent store
        """
        logger.debug("Writing model settings for window: {:d}".format(
            self._instance_nr))

        settings = get_qsettings()
        settings.beginGroup(self._settings_group_name('model'))

        logger.debug("writing auto_refresh: {!r}".format(self._auto_refresh))
        settings.setValue("auto_refresh", self._auto_refresh)

        logger.debug("writing refresh_rate: {!r}".format(self._refresh_rate))
        settings.setValue("refresh_rate", self._refresh_rate)

        logger.debug("writing show_callable_attributes: {!r}".format(
            self._proxy_tree_model.getShowCallables()))
        settings.setValue("show_callable_attributes",
                          self._proxy_tree_model.getShowCallables())

        logger.debug("writing show_special_attributes: {!r}".format(
            self._proxy_tree_model.getShowSpecialAttributes()))
        settings.setValue("show_special_attributes",
                          self._proxy_tree_model.getShowSpecialAttributes())

        settings.endGroup()

    def _readViewSettings(self, reset=False):
        """ Reads the persistent program settings
        
            :param reset: If True, the program resets to its default settings
        """
        pos = QtCore.QPoint(20 * self._instance_nr, 20 * self._instance_nr)
        window_size = QtCore.QSize(1024, 700)
        details_button_idx = 0

        header = self.obj_tree.header()
        header_restored = False

        if reset:
            logger.debug("Resetting persistent view settings")
        else:
            logger.debug("Reading view settings for window: {:d}".format(
                self._instance_nr))
            settings = get_qsettings()
            settings.beginGroup(self._settings_group_name('view'))
            pos = settings.value("main_window/pos", pos)
            window_size = settings.value("main_window/size", window_size)
            details_button_idx = int(
                settings.value("details_button_idx", details_button_idx))
            splitter_state = settings.value("central_splitter/state")
            if splitter_state:
                self.central_splitter.restoreState(splitter_state)
            header_restored = self.obj_tree.read_view_settings(
                'table/header_state', settings, reset)
            settings.endGroup()

        if not header_restored:
            column_sizes = [col.width for col in self._attr_cols]
            column_visible = [col.col_visible for col in self._attr_cols]

            for idx, size in enumerate(column_sizes):
                if size > 0:  # Just in case
                    header.resizeSection(idx, size)

            for idx, visible in enumerate(column_visible):
                self.obj_tree.toggle_column_actions_group.actions(
                )[idx].setChecked(visible)

        self.resize(window_size)
        self.move(pos)
        button = self.button_group.button(details_button_idx)
        if button is not None:
            button.setChecked(True)

    def _writeViewSettings(self):
        """ Writes the view settings to the persistent store
        """
        logger.debug("Writing view settings for window: {:d}".format(
            self._instance_nr))

        settings = get_qsettings()
        settings.beginGroup(self._settings_group_name('view'))
        self.obj_tree.write_view_settings("table/header_state", settings)
        settings.setValue("central_splitter/state",
                          self.central_splitter.saveState())
        settings.setValue("details_button_idx", self.button_group.checkedId())
        settings.setValue("main_window/pos", self.pos())
        settings.setValue("main_window/size", self.size())
        settings.endGroup()

    @Slot(QtCore.QModelIndex, QtCore.QModelIndex)
    def _update_details(self, current_index, _previous_index):
        """ Shows the object details in the editor given an index.
        """
        tree_item = self._proxy_tree_model.treeItem(current_index)
        self._update_details_for_item(tree_item)

    def _change_details_field(self, _button_id=None):
        """ Changes the field that is displayed in the details pane
        """
        #logger.debug("_change_details_field: {}".format(_button_id))
        current_index = self.obj_tree.selectionModel().currentIndex()
        tree_item = self._proxy_tree_model.treeItem(current_index)
        self._update_details_for_item(tree_item)

    def _update_details_for_item(self, tree_item):
        """ Shows the object details in the editor given an tree_item
        """
        self.editor.setStyleSheet("color: black;")
        try:
            #obj = tree_item.obj
            button_id = self.button_group.checkedId()
            assert button_id >= 0, "No radio button selected. Please report this bug."
            attr_details = self._attr_details[button_id]
            data = attr_details.data_fn(tree_item)
            self.editor.setPlainText(data)
            self.editor.setWordWrapMode(attr_details.line_wrap)

        except Exception as ex:
            self.editor.setStyleSheet("color: red;")
            stack_trace = traceback.format_exc()
            self.editor.setPlainText("{}\n\n{}".format(ex, stack_trace))
            self.editor.setWordWrapMode(
                QtWidgets.QTextOption.WrapAtWordBoundaryOrAnywhere)

    def toggle_auto_refresh(self, checked):
        """ Toggles auto-refresh on/off.
        """
        if checked:
            logger.info("Auto-refresh on. Rate {:g} seconds".format(
                self._refresh_rate))
            self._refresh_timer.start()
        else:
            logger.info("Auto-refresh off")
            self._refresh_timer.stop()
        self._auto_refresh = checked

    def my_test(self):
        """ Function for testing """
        logger.debug("my_test")
        raise Exception("An exceptional exception occurred")

    def about(self):
        """ Shows the about message window. """
        message = ("{}: {}\n\nPython: {}\n{} (api: {}, qtpy: {})\n\n{}".format(
            PROGRAM_NAME, PROGRAM_VERSION, PYTHON_VERSION, QT_API_NAME, QT_API,
            QTPY_VERSION, PROGRAM_URL))
        QtWidgets.QMessageBox.about(self, "About {}".format(PROGRAM_NAME),
                                    message)

    def _finalize(self):
        """ Cleans up resources when this window is closed.
            Disconnects all signals for this window.
        """
        self._refresh_timer.stop()
        self._refresh_timer.timeout.disconnect(self.refresh)
        self.toggle_callable_action.toggled.disconnect(
            self._proxy_tree_model.setShowCallables)
        self.toggle_special_attribute_action.toggled.disconnect(
            self._proxy_tree_model.setShowSpecialAttributes)
        self.toggle_auto_refresh_action.toggled.disconnect(
            self.toggle_auto_refresh)
        self.refresh_action_f5.triggered.disconnect(self.refresh)
        self.button_group.buttonClicked[int].disconnect(
            self._change_details_field)
        selection_model = self.obj_tree.selectionModel()
        selection_model.currentChanged.disconnect(self._update_details)

    def closeEvent(self, event):
        """ Called when the window is closed
        """
        logger.debug("closeEvent")
        self._writeModelSettings()
        self._writeViewSettings()
        self._finalize()
        self.close()
        event.accept()
        self._remove_instance()
        logger.debug("Closed {} window {}".format(PROGRAM_NAME,
                                                  self._instance_nr))

    def quit_application(self):
        """ Closes all windows """
        logger.debug("Closing all windows")
        get_qapp().closeAllWindows()

    @classmethod
    def about_to_quit(cls):
        """ Called when application is about to quit
        """
        # Sanity check
        for idx, bw in enumerate(cls._browsers):
            if bw is not None:
                raise AssertionError(
                    "Reference not cleaned up: {}".format(idx))

        logger.debug("Quitting {}".format(PROGRAM_NAME))

    @classmethod
    def create_browser(cls, *args, **kwargs):
        """ Creates and shows and ObjectBrowser window.
        
            Creates the Qt Application object if it doesn't yet exist.
            
            The *args and **kwargs will be passed to the ObjectBrowser constructor.
            
            A (class attribute) reference to the browser window is kept to prevent it from being
            garbage-collected.
        """
        q_app = QtWidgets.QApplication.instance()
        if q_app is None:

            logger.debug("Creating QApplication instance")
            q_app = QtWidgets.QApplication(sys.argv)
            q_app.aboutToQuit.connect(cls.about_to_quit)
            q_app.lastWindowClosed.connect(q_app.quit)
        else:
            logger.debug("Reusing existing QApplication instance")

        cls._q_app = q_app  # keeping reference to prevent garbage collection.

        object_browser = cls(*args, **kwargs)
        object_browser.show()
        object_browser.raise_()
        return object_browser

    @classmethod
    def execute(cls):
        """ Start the Qt event loop.
        """
        assert cls._q_app is not None, "QApplication object doesn't exist yet."
        exit_code = start_qt_event_loop(cls._q_app)
        return exit_code

    @classmethod
    def browse(cls, *args, **kwargs):
        """ Create and run object browser.
            For this, the following three steps are done:
            1) Create QApplication object if it doesn't yet exist
            2) Create and show an ObjectBrowser window
            3) Start the Qt event loop.
        
            The *args and **kwargs will be passed to the ObjectBrowser constructor.
        """
        logger.info("Browsing with {} {}".format(PROGRAM_NAME,
                                                 PROGRAM_VERSION))
        logger.info("Using Python {}".format(PYTHON_VERSION))
        logger.info("Using {} (api {}, qtpy: {})".format(
            QT_API_NAME, QT_API, QTPY_VERSION))

        cls.create_browser(*args, **kwargs)
        exit_code = cls.execute()
        return exit_code
예제 #3
0
class ObjectBrowser(QtWidgets.QMainWindow):
    """ Object browser main application window.
    """
    _q_app = None   # Reference to the global application.
    _browsers = []  # Keep lists of browser windows.
    
    def __init__(self, obj,
                 name = '',
                 attribute_columns = DEFAULT_ATTR_COLS,
                 attribute_details = DEFAULT_ATTR_DETAILS,
                 show_callable_attributes = None,  # None uses value from QSettings
                 show_special_attributes = None,  # None uses value from QSettings
                 auto_refresh=None,  # None uses value from QSettings
                 refresh_rate=None,  # None uses value from QSettings
                 reset = False):
        """ Constructor
        
            :param obj: any Python object or variable
            :param name: name of the object as it will appear in the root node
            :param attribute_columns: list of AttributeColumn objects that define which columns
                are present in the table and their defaults
            :param attribute_details: list of AttributeDetails objects that define which attributes
                can be selected in the details pane.
            :param show_callable_attributes: if True rows where the 'is attribute' and 'is callable'
                columns are both True, are displayed. Otherwise they are hidden. 
            :param show_special_attributes: if True rows where the 'is attribute' is True and
                the object name starts and ends with two underscores, are displayed. Otherwise 
                they are hidden.
            :param auto_refresh: If True, the contents refershes itsef every <refresh_rate> seconds.
            :param refresh_rate: number of seconds between automatic refreshes. Default = 2 .
            :param reset: If true the persistent settings, such as column widths, are reset. 
        """
        super(ObjectBrowser, self).__init__()

        self._instance_nr = self._add_instance()
        
        # Model
        self._attr_cols = attribute_columns
        self._attr_details = attribute_details
        
        (self._auto_refresh, self._refresh_rate, show_callable_attributes, show_special_attributes) = \
            self._readModelSettings(reset = reset,
                                    auto_refresh = auto_refresh,
                                    refresh_rate = refresh_rate,
                                    show_callable_attributes= show_callable_attributes,
                                    show_special_attributes = show_special_attributes)

        self._tree_model = TreeModel(obj, name, attr_cols = self._attr_cols)
            
        self._proxy_tree_model = TreeProxyModel(
            show_callable_attributes= show_callable_attributes,
            show_special_attributes = show_special_attributes)
        
        self._proxy_tree_model.setSourceModel(self._tree_model)
        #self._proxy_tree_model.setSortRole(RegistryTableModel.SORT_ROLE)
        self._proxy_tree_model.setDynamicSortFilter(True) 
        #self._proxy_tree_model.setSortCaseSensitivity(Qt.CaseInsensitive)
                
        # Views
        self._setup_actions()
        self._setup_menu()
        self._setup_views()
        self.setWindowTitle("{} - {}".format(PROGRAM_NAME, name))

        self._readViewSettings(reset = reset)

        assert self._refresh_rate > 0, "refresh_rate must be > 0. Got: {}".format(self._refresh_rate)
        self._refresh_timer = QtCore.QTimer(self)
        self._refresh_timer.setInterval(self._refresh_rate * 1000)
        self._refresh_timer.timeout.connect(self.refresh)
        
        # Update views with model
        self.toggle_special_attribute_action.setChecked(show_special_attributes)
        self.toggle_callable_action.setChecked(show_callable_attributes)
        self.toggle_auto_refresh_action.setChecked(self._auto_refresh)
     
        # Select first row so that a hidden root node will not be selected.
        first_row_index = self._proxy_tree_model.firstItemIndex()
        self.obj_tree.setCurrentIndex(first_row_index)
        if self._tree_model.inspectedNodeIsVisible:
            self.obj_tree.expand(first_row_index)
        

    def refresh(self):
        """ Refreshes object brawser contents
        """
        logger.debug("Refreshing")
        self._tree_model.refreshTree()
        
        
    def _add_instance(self):
        """ Adds the browser window to the list of browser references.
            If a None is present in the list it is inserted at that position, otherwise
            it is appended to the list. The index number is returned.
            
            This mechanism is used so that repeatedly creating and closing windows does not
            increase the instance number, which is used in writing the persistent settings.
        """
        try:
            idx = self._browsers.index(None)
        except ValueError:
            self._browsers.append(self)
            idx = len(self._browsers) - 1
        else:
            self._browsers[idx] = self

        return idx


    def _remove_instance(self):
        """ Sets the reference in the browser list to None. 
        """
        idx = self._browsers.index(self)
        self._browsers[idx] = None
        
            
    def _make_show_column_function(self, column_idx):
        """ Creates a function that shows or hides a column."""
        show_column = lambda checked: self.obj_tree.setColumnHidden(column_idx, not checked)
        return show_column            


    def _setup_actions(self):
        """ Creates the main window actions.
        """
        # Show/hide callable objects
        self.toggle_callable_action = \
            QtWidgets.QAction("Show callable attributes", self, checkable=True,
                          shortcut = QtGui.QKeySequence("Alt+C"),
                          statusTip = "Shows/hides attributes that are callable (functions, methods, etc)")
        self.toggle_callable_action.toggled.connect(self._proxy_tree_model.setShowCallables)
                              
        # Show/hide special attributes
        self.toggle_special_attribute_action = \
            QtWidgets.QAction("Show __special__ attributes", self, checkable=True,
                          shortcut = QtGui.QKeySequence("Alt+S"),
                          statusTip = "Shows or hides __special__ attributes")
        self.toggle_special_attribute_action.toggled.connect(self._proxy_tree_model.setShowSpecialAttributes)

        # Toggle auto-refresh on/off
        self.toggle_auto_refresh_action = \
            QtWidgets.QAction("Auto-refresh", self, checkable=True,
                          statusTip = "Auto refresh every {} seconds".format(self._refresh_rate))
        self.toggle_auto_refresh_action.toggled.connect(self.toggle_auto_refresh)
                              
        # Add another refresh action with a different short cut. An action must be added to
        # a visible widget for it to receive events. It is added to the main windows to prevent it
        # from being displayed again in the menu
        self.refresh_action_f5 = QtWidgets.QAction(self, text="&Refresh2", shortcut="F5")
        self.refresh_action_f5.triggered.connect(self.refresh)
        self.addAction(self.refresh_action_f5) 
        
                              
    def _setup_menu(self):
        """ Sets up the main menu.
        """
        file_menu = self.menuBar().addMenu("&File")
        file_menu.addAction("C&lose", self.close, "Ctrl+W")
        file_menu.addAction("E&xit", self.quit_application, "Ctrl+Q")
        if DEBUGGING is True:
            file_menu.addSeparator()
            file_menu.addAction("&Test", self.my_test, "Ctrl+T")
        
        view_menu = self.menuBar().addMenu("&View")
        view_menu.addAction("&Refresh", self.refresh, "Ctrl+R")
        view_menu.addAction(self.toggle_auto_refresh_action)
        
        view_menu.addSeparator()
        self.show_cols_submenu = view_menu.addMenu("Table columns")
        view_menu.addSeparator()
        view_menu.addAction(self.toggle_callable_action)
        view_menu.addAction(self.toggle_special_attribute_action)
        
        self.menuBar().addSeparator()
        help_menu = self.menuBar().addMenu("&Help")
        help_menu.addAction('&About', self.about)


    def _setup_views(self):
        """ Creates the UI widgets. 
        """
        self.central_splitter = QtWidgets.QSplitter(self, orientation = QtCore.Qt.Vertical)
        self.setCentralWidget(self.central_splitter)

        # Tree widget
        self.obj_tree = ToggleColumnTreeView()
        self.obj_tree.setAlternatingRowColors(True)
        self.obj_tree.setModel(self._proxy_tree_model)
        self.obj_tree.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
        self.obj_tree.setUniformRowHeights(True)
        self.obj_tree.setAnimated(True)
        self.obj_tree.add_header_context_menu()
        
        # Stretch last column? 
        # It doesn't play nice when columns are hidden and then shown again.
        obj_tree_header = self.obj_tree.header()
        obj_tree_header.setSectionsMovable(True)
        obj_tree_header.setStretchLastSection(False)
        for action in self.obj_tree.toggle_column_actions_group.actions():
            self.show_cols_submenu.addAction(action)

        self.central_splitter.addWidget(self.obj_tree)

        # Bottom pane
        bottom_pane_widget = QtWidgets.QWidget()
        bottom_layout = QtWidgets.QHBoxLayout()
        bottom_layout.setSpacing(0)
        bottom_layout.setContentsMargins(5, 5, 5, 5) # left top right bottom
        bottom_pane_widget.setLayout(bottom_layout)
        self.central_splitter.addWidget(bottom_pane_widget)
        
        group_box = QtWidgets.QGroupBox("Details")
        bottom_layout.addWidget(group_box)
        
        group_layout = QtWidgets.QHBoxLayout()
        group_layout.setContentsMargins(2, 2, 2, 2) # left top right bottom
        group_box.setLayout(group_layout)
        
        # Radio buttons
        radio_widget = QtWidgets.QWidget()
        radio_layout = QtWidgets.QVBoxLayout()
        radio_layout.setContentsMargins(0, 0, 0, 0) # left top right bottom        
        radio_widget.setLayout(radio_layout) 

        self.button_group = QtWidgets.QButtonGroup(self)
        for button_id, attr_detail in enumerate(self._attr_details):
            radio_button = QtWidgets.QRadioButton(attr_detail.name)
            radio_layout.addWidget(radio_button)
            self.button_group.addButton(radio_button, button_id)

        self.button_group.buttonClicked[int].connect(self._change_details_field)
        self.button_group.button(0).setChecked(True)
                
        radio_layout.addStretch(1)
        group_layout.addWidget(radio_widget)

        # Editor widget
        font = QtGui.QFont()
        font.setFamily('Courier')
        font.setFixedPitch(True)
        #font.setPointSize(14)

        self.editor = QtWidgets.QPlainTextEdit()
        self.editor.setReadOnly(True)
        self.editor.setFont(font)
        group_layout.addWidget(self.editor)
        
        # Splitter parameters
        self.central_splitter.setCollapsible(0, False)
        self.central_splitter.setCollapsible(1, True)
        self.central_splitter.setSizes([400, 200])
        self.central_splitter.setStretchFactor(0, 10)
        self.central_splitter.setStretchFactor(1, 0)
               
        # Connect signals
        # Keep a temporary reference of the selection_model to prevent segfault in PySide.
        # See http://permalink.gmane.org/gmane.comp.lib.qt.pyside.devel/222
        selection_model = self.obj_tree.selectionModel() 
        selection_model.currentChanged.connect(self._update_details)

    # End of setup_methods
    
    
    def _settings_group_name(self, postfix):
        """ Constructs a group name for the persistent settings.
            
            Because the columns in the main table are extendible, we must store the settings
            in a different group if a different combination of columns is used. Therefore the
            settings group name contains a hash that is calculated from the used column names.
            Furthermore the window number is included in the settings group name. Finally a
            postfix string is appended. 
        """
        column_names = ",".join([col.name for col in self._attr_cols])
        settings_str = column_names
        columns_hash = hashlib.md5(settings_str.encode('utf-8')).hexdigest()
        settings_grp = "{}_win{}_{}".format(columns_hash, self._instance_nr, postfix)
        return settings_grp

                
    def _readModelSettings(self,
                           reset=False,
                           auto_refresh = None,
                           refresh_rate = None,
                           show_callable_attributes = None,
                           show_special_attributes = None):
        """ Reads the persistent model settings .
            The persistent settings (show_callable_attributes, show_special_attributes) can be \
            overridden by giving it a True or False value.
            If reset is True and the setting is None, True is used as default.
        """ 
        default_auto_refresh = False
        default_refresh_rate = 2
        default_sra = True
        default_ssa = True
        if reset:
            logger.debug("Resetting persistent model settings")
            if refresh_rate is None:
                refresh_rate = default_refresh_rate
            if auto_refresh is None:
                auto_refresh = default_auto_refresh
            if show_callable_attributes is None:
                show_callable_attributes = default_sra
            if show_special_attributes is None:
                show_special_attributes = default_ssa
        else:
            logger.debug("Reading model settings for window: {:d}".format(self._instance_nr))
            settings = get_qsettings()
            settings.beginGroup(self._settings_group_name('model'))

            if auto_refresh is None:
                auto_refresh = setting_str_to_bool(
                    settings.value("auto_refresh", default_auto_refresh))
            logger.debug("read auto_refresh: {!r}".format(auto_refresh))

            if refresh_rate is None:
                refresh_rate = float(settings.value("refresh_rate", default_refresh_rate))
            logger.debug("read refresh_rate: {!r}".format(refresh_rate))

            if show_callable_attributes is None:
                show_callable_attributes = setting_str_to_bool(
                    settings.value("show_callable_attributes", default_sra))
            logger.debug("read show_callable_attributes: {!r}".format(show_callable_attributes))
                
            if show_special_attributes is None:
                show_special_attributes = setting_str_to_bool(
                    settings.value("show_special_attributes", default_ssa))
            logger.debug("read show_special_attributes: {!r}".format(show_special_attributes))
            
            settings.endGroup()
                        
        return (auto_refresh, refresh_rate, show_callable_attributes, show_special_attributes)
                    
    
    def _writeModelSettings(self):
        """ Writes the model settings to the persistent store
        """         
        logger.debug("Writing model settings for window: {:d}".format(self._instance_nr))
        
        settings = get_qsettings()
        settings.beginGroup(self._settings_group_name('model'))

        logger.debug("writing auto_refresh: {!r}".format(self._auto_refresh))
        settings.setValue("auto_refresh", self._auto_refresh)
        
        logger.debug("writing refresh_rate: {!r}".format(self._refresh_rate))
        settings.setValue("refresh_rate", self._refresh_rate)

        logger.debug("writing show_callable_attributes: {!r}"
                     .format(self._proxy_tree_model.getShowCallables()))
        settings.setValue("show_callable_attributes", self._proxy_tree_model.getShowCallables())

        logger.debug("writing show_special_attributes: {!r}"
                     .format(self._proxy_tree_model.getShowSpecialAttributes()))
        settings.setValue("show_special_attributes", self._proxy_tree_model.getShowSpecialAttributes())
        
        settings.endGroup()
        
    
    def _readViewSettings(self, reset=False):
        """ Reads the persistent program settings
        
            :param reset: If True, the program resets to its default settings
        """ 
        pos = QtCore.QPoint(20 * self._instance_nr, 20 * self._instance_nr)
        window_size = QtCore.QSize(1024, 700)
        details_button_idx = 0
        
        header = self.obj_tree.header()
        header_restored = False
        
        if reset:
            logger.debug("Resetting persistent view settings")
        else:
            logger.debug("Reading view settings for window: {:d}".format(self._instance_nr))
            settings = get_qsettings()
            settings.beginGroup(self._settings_group_name('view'))
            pos = settings.value("main_window/pos", pos)
            window_size = settings.value("main_window/size", window_size)
            details_button_idx = int(settings.value("details_button_idx", details_button_idx))
            splitter_state = settings.value("central_splitter/state")
            if splitter_state:
                self.central_splitter.restoreState(splitter_state) 
            header_restored = self.obj_tree.read_view_settings('table/header_state', 
                                                               settings, reset) 
            settings.endGroup()

        if not header_restored:
            column_sizes = [col.width for col in self._attr_cols]
            column_visible = [col.col_visible for col in self._attr_cols]
        
            for idx, size in enumerate(column_sizes):
                if size > 0: # Just in case 
                    header.resizeSection(idx, size)
    
            for idx, visible in enumerate(column_visible):
                self.obj_tree.toggle_column_actions_group.actions()[idx].setChecked(visible)  
            
        self.resize(window_size)
        self.move(pos)
        button = self.button_group.button(details_button_idx)
        if button is not None:
            button.setChecked(True)



    def _writeViewSettings(self):
        """ Writes the view settings to the persistent store
        """         
        logger.debug("Writing view settings for window: {:d}".format(self._instance_nr))
        
        settings = get_qsettings()
        settings.beginGroup(self._settings_group_name('view'))
        self.obj_tree.write_view_settings("table/header_state", settings)
        settings.setValue("central_splitter/state", self.central_splitter.saveState())
        settings.setValue("details_button_idx", self.button_group.checkedId())
        settings.setValue("main_window/pos", self.pos())
        settings.setValue("main_window/size", self.size())
        settings.endGroup()
            

    @Slot(QtCore.QModelIndex, QtCore.QModelIndex)
    def _update_details(self, current_index, _previous_index):
        """ Shows the object details in the editor given an index.
        """
        tree_item = self._proxy_tree_model.treeItem(current_index)
        self._update_details_for_item(tree_item)

        
    def _change_details_field(self, _button_id=None):
        """ Changes the field that is displayed in the details pane
        """
        #logger.debug("_change_details_field: {}".format(_button_id))
        current_index = self.obj_tree.selectionModel().currentIndex()
        tree_item = self._proxy_tree_model.treeItem(current_index)
        self._update_details_for_item(tree_item)
        
            
    def _update_details_for_item(self, tree_item):
        """ Shows the object details in the editor given an tree_item
        """
        self.editor.setStyleSheet("color: black;")
        try:
            #obj = tree_item.obj
            button_id = self.button_group.checkedId()
            assert button_id >= 0, "No radio button selected. Please report this bug."
            attr_details = self._attr_details[button_id]
            data = attr_details.data_fn(tree_item)
            self.editor.setPlainText(data)
            self.editor.setWordWrapMode(attr_details.line_wrap)
            
        except Exception as ex:
            self.editor.setStyleSheet("color: red;")
            stack_trace = traceback.format_exc()
            self.editor.setPlainText("{}\n\n{}".format(ex, stack_trace))
            self.editor.setWordWrapMode(QtWidgets.QTextOption.WrapAtWordBoundaryOrAnywhere)

    def toggle_auto_refresh(self, checked):
        """ Toggles auto-refresh on/off.
        """
        if checked:
            logger.info("Auto-refresh on. Rate {:g} seconds".format(self._refresh_rate))
            self._refresh_timer.start()
        else:
            logger.info("Auto-refresh off")
            self._refresh_timer.stop()
        self._auto_refresh = checked        


    def my_test(self):
        """ Function for testing """
        logger.debug("my_test")
        raise Exception("An exceptional exception occurred")

        
    def about(self):
        """ Shows the about message window. """
        message = ("{}: {}\n\nPython: {}\n{} (api: {}, qtpy: {})\n\n{}"
                   .format(PROGRAM_NAME, PROGRAM_VERSION, PYTHON_VERSION,
                           QT_API_NAME, QT_API, QTPY_VERSION, PROGRAM_URL))
        QtWidgets.QMessageBox.about(self, "About {}".format(PROGRAM_NAME), message)


    def _finalize(self):
        """ Cleans up resources when this window is closed.
            Disconnects all signals for this window.
        """
        self._refresh_timer.stop()
        self._refresh_timer.timeout.disconnect(self.refresh)
        self.toggle_callable_action.toggled.disconnect(self._proxy_tree_model.setShowCallables)
        self.toggle_special_attribute_action.toggled.disconnect(self._proxy_tree_model.setShowSpecialAttributes)        
        self.toggle_auto_refresh_action.toggled.disconnect(self.toggle_auto_refresh)
        self.refresh_action_f5.triggered.disconnect(self.refresh)
        self.button_group.buttonClicked[int].disconnect(self._change_details_field)
        selection_model = self.obj_tree.selectionModel() 
        selection_model.currentChanged.disconnect(self._update_details)
        
        
    def closeEvent(self, event):
        """ Called when the window is closed
        """
        logger.debug("closeEvent")
        self._writeModelSettings()                
        self._writeViewSettings()
        self._finalize()                
        self.close()
        event.accept()
        self._remove_instance()
        logger.debug("Closed {} window {}".format(PROGRAM_NAME, self._instance_nr))


    def quit_application(self):
        """ Closes all windows """
        logger.debug("Closing all windows")
        get_qapp().closeAllWindows()


    @classmethod
    def about_to_quit(cls):
        """ Called when application is about to quit
        """
        # Sanity check
        for idx, bw in enumerate(cls._browsers):
            if bw is not None:
                raise AssertionError("Reference not cleaned up: {}".format(idx))

        logger.debug("Quitting {}".format(PROGRAM_NAME))
        
            
    @classmethod
    def create_browser(cls, *args, **kwargs):
        """ Creates and shows and ObjectBrowser window.
        
            Creates the Qt Application object if it doesn't yet exist.
            
            The *args and **kwargs will be passed to the ObjectBrowser constructor.
            
            A (class attribute) reference to the browser window is kept to prevent it from being
            garbage-collected.
        """
        q_app = QtWidgets.QApplication.instance()
        if q_app is None:

            logger.debug("Creating QApplication instance")
            q_app = QtWidgets.QApplication(sys.argv)
            q_app.aboutToQuit.connect(cls.about_to_quit)
            q_app.lastWindowClosed.connect(q_app.quit)
        else:
            logger.debug("Reusing existing QApplication instance")
            
        cls._q_app = q_app # keeping reference to prevent garbage collection. 
        
        object_browser = cls(*args, **kwargs)
        object_browser.show()
        object_browser.raise_()
        return object_browser

    
    @classmethod
    def execute(cls):
        """ Start the Qt event loop.
        """
        assert cls._q_app is not None, "QApplication object doesn't exist yet."
        exit_code = start_qt_event_loop(cls._q_app)
        return exit_code
    
    
    @classmethod
    def browse(cls, *args, **kwargs):
        """ Create and run object browser.
            For this, the following three steps are done:
            1) Create QApplication object if it doesn't yet exist
            2) Create and show an ObjectBrowser window
            3) Start the Qt event loop.
        
            The *args and **kwargs will be passed to the ObjectBrowser constructor.
        """
        logger.info("Browsing with {} {}".format(PROGRAM_NAME, PROGRAM_VERSION))
        logger.info("Using Python {}".format(PYTHON_VERSION))
        logger.info("Using {} (api {}, qtpy: {})".format(QT_API_NAME, QT_API, QTPY_VERSION))

        cls.create_browser(*args, **kwargs)
        exit_code = cls.execute()
        return exit_code
        
예제 #4
0
class ObjectBrowser(QtGui.QMainWindow):
    """ Object browser main application window.
    """
    _n_instances = 0
    
    def __init__(self, obj,  
                 name = '',
                 attribute_columns = DEFAULT_ATTR_COLS,  
                 attribute_details = DEFAULT_ATTR_DETAILS,  
                 show_routine_attributes = None,
                 show_special_attributes = None, 
                 reset = False):
        """ Constructor
        
            :param obj: any Python object or variable
            :param name: name of the object as it will appear in the root node
            :param attribute_columns: list of AttributeColumn objects that define which columns
                are present in the table and their defaults
            :param attribute_details: list of AttributeDetails objects that define which attributes
                can be selected in the details pane.
            :param show_routine_attributes: if True rows where the 'is attribute' and 'is routine'
                columns are both True, are displayed. Otherwise they are hidden. 
            :param show_special_attributes: if True rows where the 'is attribute' is True and
                the object name starts and ends with two underscores, are displayed. Otherwise 
                they are hidden.
            :param reset: If true the persistent settings, such as column widths, are reset. 
        """
        super(ObjectBrowser, self).__init__()

        ObjectBrowser._n_instances += 1
        self._instance_nr = self._n_instances        
        
        # Model
        self._attr_cols = attribute_columns
        self._attr_details = attribute_details
        
        (show_routine_attributes, 
         show_special_attributes) = self._readModelSettings(reset = reset, 
                                                            show_routine_attributes = show_routine_attributes,
                                                            show_special_attributes = show_special_attributes)
        self._tree_model = TreeModel(obj, 
                                     root_obj_name = name,
                                     attr_cols = self._attr_cols,  
                                     show_routine_attributes = show_routine_attributes, 
                                     show_special_attributes = show_special_attributes)
        # Views
        self._setup_actions()
        self._setup_menu()
        self._setup_views()
        self.setWindowTitle("{} - {}".format(PROGRAM_NAME, name))
        app = QtGui.QApplication.instance()
        app.lastWindowClosed.connect(app.quit) 

        self._readViewSettings(reset = reset)
        
        # Update views with model
        self.toggle_special_attribute_action.setChecked(show_special_attributes)
        self.toggle_callable_action.setChecked(show_routine_attributes)
     
        # Select first row so that a hidden root node will not be selected.
        first_row = self._tree_model.first_item_index()
        self.obj_tree.setCurrentIndex(first_row)
            
            
    def _make_show_column_function(self, column_idx):
        """ Creates a function that shows or hides a column."""
        show_column = lambda checked: self.obj_tree.setColumnHidden(column_idx, not checked)
        return show_column            


    def _setup_actions(self):
        """ Creates the main window actions.
        """
        # Show/hide callable objects
        self.toggle_callable_action = \
            QtGui.QAction("Show routine attributes", self, checkable=True, 
                          statusTip = "Shows/hides attributes that are routings (functions, methods, etc)")
        assert self.toggle_callable_action.toggled.connect(self.toggle_callables)
                              
        # Show/hide special attributes
        self.toggle_special_attribute_action = \
            QtGui.QAction("Show __special__ attributes", self, checkable=True, 
                          statusTip = "Shows or hides __special__ attributes")
        assert self.toggle_special_attribute_action.toggled.connect(self.toggle_special_attributes)
                              
                              
    def _setup_menu(self):
        """ Sets up the main menu.
        """
        file_menu = self.menuBar().addMenu("&File")
        file_menu.addAction("C&lose", self.close_window, "Ctrl+W")
        file_menu.addAction("E&xit", self.quit_application, "Ctrl+Q")
        if DEBUGGING is True:
            file_menu.addSeparator()
            file_menu.addAction("&Test", self.my_test, "Ctrl+T")
        
        view_menu = self.menuBar().addMenu("&View")
        self.show_cols_submenu = view_menu.addMenu("Table columns")
        view_menu.addSeparator()
        view_menu.addAction(self.toggle_callable_action)
        view_menu.addAction(self.toggle_special_attribute_action)
        
        self.menuBar().addSeparator()
        help_menu = self.menuBar().addMenu("&Help")
        help_menu.addAction('&About', self.about)


    def _setup_views(self):
        """ Creates the UI widgets. 
        """
        self.central_splitter = QtGui.QSplitter(self, orientation = QtCore.Qt.Vertical)
        self.setCentralWidget(self.central_splitter)
        central_layout = QtGui.QVBoxLayout()
        self.central_splitter.setLayout(central_layout)
        
        # Tree widget
        self.obj_tree = ToggleColumnTreeView()
        self.obj_tree.setAlternatingRowColors(True)
        self.obj_tree.setModel(self._tree_model)
        self.obj_tree.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
        self.obj_tree.setUniformRowHeights(True)
        self.obj_tree.setAnimated(True)
        self.obj_tree.add_header_context_menu()
        
        # Stretch last column? 
        # It doesn't play nice when columns are hidden and then shown again.
        obj_tree_header = self.obj_tree.header()
        obj_tree_header.setMovable(True)
        obj_tree_header.setStretchLastSection(False)
        for action in self.obj_tree.toggle_column_actions_group.actions():
            self.show_cols_submenu.addAction(action)

        central_layout.addWidget(self.obj_tree)

        # Bottom pane
        bottom_pane_widget = QtGui.QWidget()
        bottom_layout = QtGui.QHBoxLayout()
        bottom_layout.setSpacing(0)
        bottom_layout.setContentsMargins(5, 5, 5, 5) # left top right bottom
        bottom_pane_widget.setLayout(bottom_layout)
        central_layout.addWidget(bottom_pane_widget)
        
        group_box = QtGui.QGroupBox("Details")
        bottom_layout.addWidget(group_box)
        
        group_layout = QtGui.QHBoxLayout()
        group_layout.setContentsMargins(2, 2, 2, 2) # left top right bottom
        group_box.setLayout(group_layout)
        
        # Radio buttons
        radio_widget = QtGui.QWidget()
        radio_layout = QtGui.QVBoxLayout()
        radio_layout.setContentsMargins(0, 0, 0, 0) # left top right bottom        
        radio_widget.setLayout(radio_layout) 

        self.button_group = QtGui.QButtonGroup(self)
        for button_id, attr_detail in enumerate(self._attr_details):
            radio_button = QtGui.QRadioButton(attr_detail.name)
            radio_layout.addWidget(radio_button)
            self.button_group.addButton(radio_button, button_id)

        self.button_group.buttonClicked[int].connect(self._change_details_field)
        self.button_group.button(0).setChecked(True)
                
        radio_layout.addStretch(1)
        group_layout.addWidget(radio_widget)

        # Editor widget
        font = QtGui.QFont()
        font.setFamily('Courier')
        font.setFixedPitch(True)
        #font.setPointSize(14)

        self.editor = QtGui.QPlainTextEdit()
        self.editor.setReadOnly(True)
        self.editor.setFont(font)
        group_layout.addWidget(self.editor)
        
        # Splitter parameters
        self.central_splitter.setCollapsible(0, False)
        self.central_splitter.setCollapsible(1, True)
        self.central_splitter.setSizes([400, 200])
        self.central_splitter.setStretchFactor(0, 10)
        self.central_splitter.setStretchFactor(1, 0)
               
        # Connect signals
        selection_model = self.obj_tree.selectionModel()
        assert selection_model.currentChanged.connect(self._update_details)

    # End of setup_methods
    
    
    def _settings_group_name(self, prefix):
        """ The persistent settings are stored per combination of column names
            and windows instance number.
        """
        column_names = ",".join([col.name for col in self._attr_cols])
        settings_str = column_names + str(self._instance_nr)
        settings_grp = "{}_{}".format(prefix, hex(hash(settings_str)))
        logger.debug("  settings group is: {!r}".format(settings_grp))
        return settings_grp

                
    def _readModelSettings(self, 
                           reset=False, 
                           show_routine_attributes = None,
                           show_special_attributes = None):
        """ Reads the persistent model settings .
            The persistent settings (show_routine_attributes, show_special_attributes) can be \
            overridden by giving it a True or False value.
            If reset is True and the setting is None, True is used as default.
        """ 
        default_sra = True
        default_ssa = True
        if reset:
            logger.debug("Resetting persistent model settings")
            if show_routine_attributes is None:
                show_routine_attributes = default_sra
            if show_special_attributes is None:
                show_special_attributes = default_ssa
        else:
            logger.debug("Reading model settings for window: {:d}".format(self._instance_nr))
            settings = QtCore.QSettings()
            settings.beginGroup(self._settings_group_name('model'))
            if show_routine_attributes is None:
                show_routine_attributes = setting_str_to_bool(
                    settings.value("show_routine_attributes", default_sra))
            if show_special_attributes is None:
                show_special_attributes = setting_str_to_bool(
                    settings.value("show_special_attributes", default_ssa))
            settings.endGroup()
            logger.debug("read show_routine_attributes: {!r}".format(show_routine_attributes))
            logger.debug("read show_special_attributes: {!r}".format(show_special_attributes))
        return (show_routine_attributes, show_special_attributes)
                    
    
    def _writeModelSettings(self):
        """ Writes the model settings to the persistent store
        """         
        logger.debug("Writing model settings for window: {:d}".format(self._instance_nr))
        
        settings = QtCore.QSettings()
        settings.beginGroup(self._settings_group_name('model'))
        logger.debug("writing show_routine_attributes: {!r}".format(self._tree_model.getShowCallables()))
        logger.debug("wrting show_special_attributes: {!r}".format(self._tree_model.getShowSpecialAttributes()))
        settings.setValue("show_routine_attributes", self._tree_model.getShowCallables())
        settings.setValue("show_special_attributes", self._tree_model.getShowSpecialAttributes())
        settings.endGroup()
        
    
    def _readViewSettings(self, reset=False):
        """ Reads the persistent program settings
        
            :param reset: If True, the program resets to its default settings
        """ 
        pos = QtCore.QPoint(20 * self._instance_nr, 20 * self._instance_nr)
        window_size = QtCore.QSize(1024, 700)
        details_button_idx = 0
        
        header = self.obj_tree.header()
        header_restored = False
        
        if reset:
            logger.debug("Resetting persistent view settings")
        else:
            logger.debug("Reading view settings for window: {:d}".format(self._instance_nr))
            settings = QtCore.QSettings()
            settings.beginGroup(self._settings_group_name('view'))
            pos = settings.value("main_window/pos", pos)
            window_size = settings.value("main_window/size", window_size)
            details_button_idx = int(settings.value("details_button_idx", details_button_idx))
            self.central_splitter.restoreState(settings.value("central_splitter/state"))
            header_restored = self.obj_tree.read_view_settings('table/header_state', 
                                                               settings, reset) 
            settings.endGroup()

        if not header_restored:
            column_sizes = [col.width for col in self._attr_cols]
            column_visible = [col.col_visible for col in self._attr_cols]
        
            for idx, size in enumerate(column_sizes):
                if size > 0: # Just in case 
                    header.resizeSection(idx, size)
    
            for idx, visible in enumerate(column_visible):
                self.obj_tree.toggle_column_actions_group.actions()[idx].setChecked(visible)  
            
        self.resize(window_size)
        self.move(pos)
        button = self.button_group.button(details_button_idx)
        if button is not None:
            button.setChecked(True)



    def _writeViewSettings(self):
        """ Writes the view settings to the persistent store
        """         
        logger.debug("Writing view settings for window: {:d}".format(self._instance_nr))
        
        settings = QtCore.QSettings()
        settings.beginGroup(self._settings_group_name('view'))
        self.obj_tree.write_view_settings("table/header_state", settings)
        settings.setValue("central_splitter/state", self.central_splitter.saveState())
        settings.setValue("details_button_idx", self.button_group.checkedId())
        settings.setValue("main_window/pos", self.pos())
        settings.setValue("main_window/size", self.size())
        settings.endGroup()
            

    @QtCore.Slot(QtCore.QModelIndex, QtCore.QModelIndex)
    def _update_details(self, current_index, _previous_index):
        """ Shows the object details in the editor given an index.
        """
        tree_item = self._tree_model.treeItem(current_index)
        self._update_details_for_item(tree_item)

        
    def _change_details_field(self, _button_id=None):
        """ Changes the field that is displayed in the details pane
        """
        #logger.debug("_change_details_field: {}".format(_button_id))
        current_index = self.obj_tree.selectionModel().currentIndex()
        tree_item = self._tree_model.treeItem(current_index)
        self._update_details_for_item(tree_item)
        
            
    def _update_details_for_item(self, tree_item):
        """ Shows the object details in the editor given an tree_item
        """
        self.editor.setStyleSheet("color: black;")
        try:
            #obj = tree_item.obj
            button_id = self.button_group.checkedId()
            assert button_id >= 0, "No radio button selected. Please report this bug."
            attr_details = self._attr_details[button_id]
            data = attr_details.data_fn(tree_item)
            self.editor.setPlainText(data)
            self.editor.setWordWrapMode(attr_details.line_wrap)
            
        except StandardError, ex:
            self.editor.setStyleSheet("color: red;")
            stack_trace = traceback.format_exc()
            self.editor.setPlainText("{}\n\n{}".format(ex, stack_trace))
            self.editor.setWordWrapMode(QtGui.QTextOption.WrapAtWordBoundaryOrAnywhere)
            if DEBUGGING is True:
                raise