Example #1
0
    def setup_model(self, packages, data, metadata_links=None):
        """Setup model content."""
        self.proxy_model = MultiColumnSortFilterProxy(self)
        self.source_model = CondaPackagesModel(self, packages, data)
        self.proxy_model.setSourceModel(self.source_model)
        self.setModel(self.proxy_model)
        self.metadata_links = metadata_links if metadata_links else {}

        # FIXME: packages sizes... move to a better place?
        packages_sizes = {}
        for name in packages:
            packages_sizes[name] = packages[name].get('size')
        self._packages_sizes = packages_sizes

        # Custom Proxy Model setup
        self.proxy_model.setDynamicSortFilter(True)

        def filter_text(row, text, status):
            """Filter text helper function."""
            in_name = all(t in row[C.COL_NAME].lower()
                          for t in to_text_string(text).lower().split())
            in_desc = all(t in row[C.COL_DESCRIPTION].lower()
                          for t in to_text_string(text).split())
            return in_name or in_desc

        def filter_status(row, text, status):
            """Filter status helper function."""
            test1 = to_text_string(row[C.COL_STATUS]) in to_text_string(status)
            test2 = to_text_string(row[C.COL_ACTION]) in to_text_string(status)
            return test1 or test2

        self.model().add_filter_function('text-search', filter_text)
        self.model().add_filter_function('status-search', filter_status)

        # Signals and slots
        self.verticalScrollBar().valueChanged.connect(self.resize_rows)

        self.hide_columns()
        self.resize_rows()
        self.refresh_actions()
Example #2
0
class TableCondaPackages(QTableView):
    """Conda packages table view."""

    WIDTH_TYPE = 24
    WIDTH_NAME = 120
    WIDTH_ACTIONS = 24
    WIDTH_VERSION = 90

    sig_status_updated = Signal(object, object, object, object)
    sig_conda_action_requested = Signal(str, int, str, object, object)
    sig_pip_action_requested = Signal(str, int)
    sig_actions_updated = Signal(int)
    sig_next_focus = Signal()
    sig_previous_focus = Signal()

    def __init__(self, parent):
        """Conda packages table view."""
        super(TableCondaPackages, self).__init__(parent)
        self._parent = parent
        self._searchbox = u''
        self._filterbox = C.ALL
        self._delegate = CustomDelegate(self)
        self.row_count = None
        self._advanced_mode = True
        self._current_hover_row = None
        self._menu = None
        self._palette = {}

        # To manage icon states
        self._model_index_clicked = None
        self.valid = False
        self.column_ = None
        self.current_index = None

        # To prevent triggering the keyrelease after closing a dialog
        # but hititng enter on it
        self.pressed_here = False

        self.source_model = None
        self.proxy_model = None

        self.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.setAlternatingRowColors(True)
        self.setSelectionMode(QAbstractItemView.NoSelection)
        self.verticalHeader().hide()
        self.setSortingEnabled(True)
        self.setMouseTracking(True)

        self._delegate.current_row = self.current_row
        self._delegate.current_hover_row = self.current_hover_row
        self._delegate.update_index = self.update
        self._delegate.has_focus_or_context = self.has_focus_or_context
        self.setItemDelegate(self._delegate)
        self.setShowGrid(False)
        self.setWordWrap(True)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

        # Header setup
        self._hheader = self.horizontalHeader()
        # This should be handled by qtpy but is not working
        if PYQT5:
            self._hheader.setSectionResizeMode(QHeaderView.Fixed)
        elif PYQT4:
            try:
                self._hheader.setSectionResizeMode(QHeaderView.Fixed)
            except Exception:
                self._hheader.setResizeMode(QHeaderView.Fixed)
        self._hheader.setStyleSheet(
            """QHeaderView {border: 0px; border-radius: 0px;};""")
        self.sortByColumn(C.COL_NAME, Qt.AscendingOrder)
        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.hide_columns()

    def setup_model(self, packages, data, metadata_links=None):
        """Setup model content."""
        self.proxy_model = MultiColumnSortFilterProxy(self)
        self.source_model = CondaPackagesModel(self, packages, data)
        self.proxy_model.setSourceModel(self.source_model)
        self.setModel(self.proxy_model)
        self.metadata_links = metadata_links if metadata_links else {}

        # FIXME: packages sizes... move to a better place?
        packages_sizes = {}
        for name in packages:
            packages_sizes[name] = packages[name].get('size')
        self._packages_sizes = packages_sizes

        # Custom Proxy Model setup
        self.proxy_model.setDynamicSortFilter(True)

        def filter_text(row, text, status):
            """Filter text helper function."""
            in_name = all(t in row[C.COL_NAME].lower()
                          for t in to_text_string(text).lower().split())
            in_desc = all(t in row[C.COL_DESCRIPTION].lower()
                          for t in to_text_string(text).split())
            return in_name or in_desc

        def filter_status(row, text, status):
            """Filter status helper function."""
            test1 = to_text_string(row[C.COL_STATUS]) in to_text_string(status)
            test2 = to_text_string(row[C.COL_ACTION]) in to_text_string(status)
            return test1 or test2

        self.model().add_filter_function('text-search', filter_text)
        self.model().add_filter_function('status-search', filter_status)

        # Signals and slots
        self.verticalScrollBar().valueChanged.connect(self.resize_rows)

        self.hide_columns()
        self.resize_rows()
        self.refresh_actions()

    def resize_rows(self):
        """Resize rows to fit the content."""
        delta_y = 10
        height = self.height()
        y = 0
        while y < height:
            row = self.rowAt(y)
            self.resizeRowToContents(row)
            row_height = self.rowHeight(row)
            self.setRowHeight(row, row_height + delta_y)
            y += self.rowHeight(row) + delta_y

    def hide_columns(self):
        """Hide unused columns."""
        for col in C.COLUMNS:
            self.showColumn(col)

        hide = HIDE_COLUMNS
        if self._advanced_mode:
            columns = C.ACTION_COLUMNS[:]
            columns.remove(C.COL_ACTION)
            hide += columns
        else:
            hide += [C.COL_ACTION]

        for col in hide:
            self.hideColumn(col)

    def filter_changed(self):
        """Trigger the filter."""
        group = self._filterbox
        text = self._searchbox

        if group in [C.ALL]:
            group = '-'.join([
                to_text_string(C.INSTALLED),
                to_text_string(C.UPGRADABLE),
                to_text_string(C.NOT_INSTALLED),
                to_text_string(C.DOWNGRADABLE),
                to_text_string(C.MIXGRADABLE)
            ])
        elif group in [C.INSTALLED]:
            group = '-'.join([
                to_text_string(C.INSTALLED),
                to_text_string(C.UPGRADABLE),
                to_text_string(C.DOWNGRADABLE),
                to_text_string(C.MIXGRADABLE)
            ])
        elif group in [C.UPGRADABLE]:
            group = '-'.join(
                [to_text_string(C.UPGRADABLE),
                 to_text_string(C.MIXGRADABLE)])
        elif group in [C.DOWNGRADABLE]:
            group = '-'.join([
                to_text_string(C.DOWNGRADABLE),
                to_text_string(C.MIXGRADABLE)
            ])
        elif group in [C.SELECTED]:
            group = '-'.join([
                to_text_string(C.ACTION_INSTALL),
                to_text_string(C.ACTION_REMOVE),
                to_text_string(C.ACTION_UPGRADE),
                to_text_string(C.ACTION_DOWNGRADE),
            ])
        else:
            group = to_text_string(group)

        if self.proxy_model is not None:
            self.proxy_model.set_filter(text, group)
            self.resize_rows()

        # Update label count
        count = self.verticalHeader().count()
        if count == 0:
            count_text = _("0 packages available ")
        elif count == 1:
            count_text = _("1 package available ")
        elif count > 1:
            count_text = to_text_string(count) + _(" packages available ")

        if text != '':
            count_text = count_text + _('matching "{0}"').format(text)

        # Give information on selected packages
        selected_text = ''
        if self.source_model:
            action_count = self.source_model.get_action_count()
            if action_count:
                plural = 's' if action_count != 1 else ''
                selected_text = '{0} package{1} selected'.format(
                    action_count, plural)

        self.sig_status_updated.emit(count_text, selected_text, None, None)

    def search_string_changed(self, text):
        """Update the search string text."""
        text = to_text_string(text)
        self._searchbox = text
        self.filter_changed()

    def filter_status_changed(self, text):
        """Update the type string selection."""
        self._filterbox = C.COMBOBOX_VALUES.get(text)
        self.filter_changed()

    def resizeEvent(self, event):
        """Override Qt method."""
        w = self.width()
        width_start = 8
        width_end = 0

        if self._advanced_mode:
            action_cols = [C.COL_ACTION]
        else:
            action_cols = [
                C.COL_UPGRADE, C.COL_INSTALL, C.COL_REMOVE, C.COL_DOWNGRADE
            ]

        self.setColumnWidth(C.COL_START, width_start)
        self.setColumnWidth(C.COL_PACKAGE_TYPE, self.WIDTH_TYPE)
        self.setColumnWidth(C.COL_NAME, self.WIDTH_NAME)
        self.setColumnWidth(C.COL_VERSION, self.WIDTH_VERSION)
        w_new = w - (width_start + self.WIDTH_ACTIONS + self.WIDTH_TYPE +
                     self.WIDTH_NAME + self.WIDTH_VERSION +
                     (len(action_cols)) * self.WIDTH_ACTIONS + width_end) + 10
        self.setColumnWidth(C.COL_DESCRIPTION, w_new)
        self.setColumnWidth(C.COL_END, width_end)

        for col in action_cols:
            self.setColumnWidth(col, self.WIDTH_ACTIONS)
        QTableView.resizeEvent(self, event)
        self.resize_rows()

    def update_visible_rows(self):
        """Update range of visible rows close to selected row."""
        current_index = self.currentIndex()
        row = current_index.row()

        if self.proxy_model:
            for r in range(row - 50, row + 50):
                for co in C.COLUMNS:
                    index = self.proxy_model.index(r, co)
                    self.update(index)
            self.resize_rows()

    def current_row(self):
        """Return the currently selected row."""
        if self._menu and self._menu.isVisible():
            return self.currentIndex().row()
        elif self.hasFocus():
            return self.currentIndex().row()
        else:
            return -1

    def current_hover_row(self):
        """Return the currently hovered row."""
        return self._current_hover_row

    def has_focus_or_context(self):
        """Return if the table has focus of if the context menu is on."""
        return self.hasFocus() or (self._menu and self._menu.isVisible())

    def mouseMoveEvent(self, event):
        """Override Qt method."""
        super(TableCondaPackages, self).mouseMoveEvent(event)
        pos = event.pos()
        self._current_hover_row = self.rowAt(pos.y())

    def leaveEvent(self, event):
        """Override Qt method."""
        super(TableCondaPackages, self).leaveEvent(event)
        self._current_hover_row = None
        self.repaint()

    def keyPressEvent(self, event):
        """Override Qt method."""
        index = self.currentIndex()
        key = event.key()
        rows = self.verticalHeader().count()

        if MAC:
            if key == Qt.Key_Home:
                self.scrollToTop()
                self.setCurrentIndex(self.model().index(0, 0))
            elif key == Qt.Key_End:
                self.scrollToBottom()
                self.setCurrentIndex(self.model().index(rows - 1, 0))
            elif key == Qt.Key_Up:
                previous = index.row() - 1 if index.row() - 1 > 0 else 0
                self.setCurrentIndex(self.model().index(previous, 0))
            elif key == Qt.Key_Down:
                next_ = index.row() + 1 if index.row() + 1 < rows else rows - 1
                self.setCurrentIndex(self.model().index(next_, 0))

        if key in [Qt.Key_Enter, Qt.Key_Return]:
            # self.action_pressed(index)
            self.setCurrentIndex(
                self.proxy_model.index(index.row(), C.COL_ACTION))
            self.pressed_here = True
        elif key in [Qt.Key_Tab]:
            new_row = index.row() + 1
            if not self.proxy_model or new_row == self.proxy_model.rowCount():
                self.sig_next_focus.emit()
            else:
                new_index = self.proxy_model.index(new_row, 0)
                self.setCurrentIndex(new_index)
        elif key in [Qt.Key_Backtab]:
            new_row = index.row() - 1
            if new_row < 0:
                self.sig_previous_focus.emit()
            else:
                new_index = self.proxy_model.index(new_row, 0)
                self.setCurrentIndex(new_index)
        else:
            QTableView.keyPressEvent(self, event)

        self.update_visible_rows()

    def keyReleaseEvent(self, event):
        """Override Qt method."""
        QTableView.keyReleaseEvent(self, event)
        key = event.key()
        index = self.currentIndex()
        if key in [Qt.Key_Enter, Qt.Key_Return] and self.pressed_here:
            self.context_menu_requested(event)
        elif key in [Qt.Key_Menu]:
            self.setCurrentIndex(
                self.proxy_model.index(index.row(), C.COL_ACTION))
            self.context_menu_requested(event, right_click=True)
        self.pressed_here = False
        self.update_visible_rows()

    def mousePressEvent(self, event):
        """Override Qt method."""
        QTableView.mousePressEvent(self, event)

        pos = QPoint(event.x(), event.y())
        index = self.indexAt(pos)
        model = self.source_model

        if self.proxy_model is None or self.source_model is None:
            return

        model_index = self.proxy_model.mapToSource(index)
        row = model_index.row()

        if row == -1:
            return

        column = model_index.column()
        row_data = self.source_model.row(row)
        remove_actions = bool(self.source_model.count_remove_actions())
        install_actions = bool(self.source_model.count_install_actions())

        action = row_data[C.COL_ACTION]
        status = row_data[C.COL_STATUS]

        right_click = event.button() == Qt.RightButton
        left_click = event.button() == Qt.LeftButton

        if column == C.COL_ACTION:
            if right_click or (left_click and status != C.NOT_INSTALLED):
                self.context_menu_requested(event)
            elif left_click and status == C.NOT_INSTALLED:
                # 1-click install/uncheck if not installed
                if action == C.ACTION_NONE and not remove_actions:
                    self.set_action_status(model_index, C.ACTION_INSTALL)
                elif status:
                    self.set_action_status(model_index, C.ACTION_NONE)

        elif (column == C.COL_VERSION and model.is_upgradable(model_index)
              and left_click and not install_actions and not remove_actions):
            # 1-click update
            self.set_action_status(model_index, C.ACTION_UPDATE)

        self.update_visible_rows()

    def mouseReleaseEvent(self, event):
        """Override Qt method."""
        self.update_visible_rows()

    def set_action_status(self,
                          model_index,
                          status=C.ACTION_NONE,
                          version=None):
        """Set model index action status."""
        self.source_model.set_action_status(model_index, status, version)
        self.filter_changed()
        self.refresh_actions()

    def context_menu_requested(self, event, right_click=False):
        """Custom context menu."""
        if self.proxy_model is None:
            return

        self._menu = QMenu(self)
        left_click = not right_click
        index = self.currentIndex()
        model_index = self.proxy_model.mapToSource(index)
        row_data = self.source_model.row(model_index.row())
        column = model_index.column()
        name = row_data[C.COL_NAME]
        # package_type = row_data[C.COL_PACKAGE_TYPE]
        versions = self.source_model.get_package_versions(name)
        current_version = self.source_model.get_package_version(name)
        action_version = row_data[C.COL_ACTION_VERSION]
        package_status = row_data[C.COL_STATUS]
        package_type = row_data[C.COL_PACKAGE_TYPE]

        remove_actions = bool(self.source_model.count_remove_actions())
        install_actions = bool(self.source_model.count_install_actions())
        update_actions = bool(self.source_model.count_update_actions())

        if column in [C.COL_ACTION] and left_click:
            is_installable = self.source_model.is_installable(model_index)
            is_removable = self.source_model.is_removable(model_index)
            is_upgradable = self.source_model.is_upgradable(model_index)

            action_status = self.source_model.action_status(model_index)
            actions = []
            action_unmark = create_action(
                self,
                _('Unmark'),
                triggered=lambda: self.set_action_status(
                    model_index, C.ACTION_NONE, current_version))
            action_install = create_action(
                self,
                _('Mark for installation'),
                toggled=lambda: self.set_action_status(model_index, C.
                                                       ACTION_INSTALL))
            action_update = create_action(
                self,
                _('Mark for update'),
                toggled=lambda: self.set_action_status(model_index, C.
                                                       ACTION_UPDATE, None))
            action_remove = create_action(
                self,
                _('Mark for removal'),
                toggled=lambda: self.set_action_status(
                    model_index, C.ACTION_REMOVE, current_version))
            version_actions = []
            for version in reversed(versions):

                def trigger(model_index=model_index,
                            action=C.ACTION_INSTALL,
                            version=version):
                    return lambda: self.set_action_status(
                        model_index, status=action, version=version)

                if version == current_version:
                    version_action = create_action(
                        self,
                        version,
                        icon=QIcon(),
                        triggered=trigger(model_index, C.ACTION_INSTALL,
                                          version))
                    if not is_installable:
                        version_action.setCheckable(True)
                        version_action.setChecked(True)
                        version_action.setDisabled(True)
                elif version != current_version:
                    if ((version in versions and versions.index(version)) >
                        (current_version in versions
                         and versions.index(current_version))):
                        upgrade_or_downgrade_action = C.ACTION_UPGRADE
                    else:
                        upgrade_or_downgrade_action = C.ACTION_DOWNGRADE

                    if is_installable:
                        upgrade_or_downgrade_action = C.ACTION_INSTALL

                    version_action = create_action(
                        self,
                        version,
                        icon=QIcon(),
                        triggered=trigger(model_index,
                                          upgrade_or_downgrade_action,
                                          version))
                if action_version == version:
                    version_action.setCheckable(True)
                    version_action.setChecked(True)

                version_actions.append(version_action)

            install_versions_menu = QMenu(
                'Mark for specific version '
                'installation', self)
            add_actions(install_versions_menu, version_actions)
            actions = [
                action_unmark, action_install, action_update, action_remove
            ]
            actions += [None, install_versions_menu]

            # Disable firing of signals, while setting the checked status
            for ac in actions + version_actions:
                if ac:
                    ac.blockSignals(True)

            if action_status == C.ACTION_NONE:
                action_unmark.setEnabled(False)
                action_install.setEnabled(is_installable)
                action_update.setEnabled(is_upgradable)
                action_remove.setEnabled(is_removable)

                if install_actions:
                    # Invalidate remove and update if install actions selected
                    action_update.setDisabled(True)
                    action_remove.setDisabled(True)
                elif remove_actions:
                    # Invalidate install/update if remove actions already
                    action_install.setDisabled(True)
                    action_update.setDisabled(True)
                elif update_actions:
                    # Invalidate install/update if remove actions already
                    action_install.setDisabled(True)
                    action_remove.setDisabled(True)

                install_versions_menu.setDisabled(False)
            elif action_status == C.ACTION_INSTALL:
                action_unmark.setEnabled(True)
                action_install.setEnabled(False)
                action_install.setChecked(True)
                action_update.setEnabled(False)
                action_remove.setEnabled(False)
            elif action_status == C.ACTION_REMOVE:
                action_unmark.setEnabled(True)
                action_install.setEnabled(False)
                action_update.setEnabled(False)
                action_remove.setEnabled(False)
                action_remove.setChecked(True)
            elif action_status == C.ACTION_UPDATE:
                action_unmark.setEnabled(True)
                action_install.setEnabled(False)
                action_update.setEnabled(False)
                action_update.setChecked(True)
                action_remove.setEnabled(False)
            elif action_status in [C.ACTION_UPGRADE, C.ACTION_DOWNGRADE]:
                action_unmark.setEnabled(True)
                action_install.setEnabled(False)
                action_update.setEnabled(False)
                action_update.setChecked(False)
                action_remove.setEnabled(False)
                install_versions_menu.setEnabled(False)

            if package_status == C.NOT_INSTALLED:
                action_remove.setEnabled(False)
                action_update.setEnabled(False)

            if package_type == C.PIP_PACKAGE:
                action_unmark.setEnabled(False)
                action_install.setEnabled(False)
                action_update.setEnabled(False)
                action_remove.setEnabled(False)

            # Enable firing of signals, while setting the checked status
            for ac in actions + version_actions:
                if ac:
                    ac.blockSignals(False)

                install_versions_menu.setDisabled(True)

            install_versions_menu.setEnabled(
                len(version_actions) > 1 and not remove_actions
                and not update_actions)
        elif right_click:
            license_ = row_data[C.COL_LICENSE]

            metadata = self.metadata_links.get(name, {})
            pypi = metadata.get('pypi', '')
            home = metadata.get('home', '')
            dev = metadata.get('dev', '')
            docs = metadata.get('docs', '')

            q_pypi = QIcon(get_image_path('python.png'))
            q_home = QIcon(get_image_path('home.png'))
            q_docs = QIcon(get_image_path('conda_docs.png'))

            if 'git' in dev:
                q_dev = QIcon(get_image_path('conda_github.png'))
            elif 'bitbucket' in dev:
                q_dev = QIcon(get_image_path('conda_bitbucket.png'))
            else:
                q_dev = QIcon()

            if 'mit' in license_.lower():
                lic = 'http://opensource.org/licenses/MIT'
            elif 'bsd' == license_.lower():
                lic = 'http://opensource.org/licenses/BSD-3-Clause'
            else:
                lic = None

            actions = []

            if license_ != '':
                actions.append(
                    create_action(self,
                                  _('License: ' + license_),
                                  icon=QIcon(),
                                  triggered=lambda: self.open_url(lic)))
                actions.append(None)

            if pypi != '':
                actions.append(
                    create_action(self,
                                  _('Python Package Index'),
                                  icon=q_pypi,
                                  triggered=lambda: self.open_url(pypi)))
            if home != '':
                actions.append(
                    create_action(self,
                                  _('Homepage'),
                                  icon=q_home,
                                  triggered=lambda: self.open_url(home)))
            if docs != '':
                actions.append(
                    create_action(self,
                                  _('Documentation'),
                                  icon=q_docs,
                                  triggered=lambda: self.open_url(docs)))
            if dev != '':
                actions.append(
                    create_action(self,
                                  _('Development'),
                                  icon=q_dev,
                                  triggered=lambda: self.open_url(dev)))
        if actions and len(actions) > 1:
            # self._menu = QMenu(self)
            add_actions(self._menu, actions)

            if event.type() == QEvent.KeyRelease:
                rect = self.visualRect(index)
                global_pos = self.viewport().mapToGlobal(rect.bottomRight())
            else:
                pos = QPoint(event.x(), event.y())
                global_pos = self.viewport().mapToGlobal(pos)

            self._menu.popup(global_pos)

    def get_actions(self):
        """Return currently selected actions."""
        if self.source_model:
            return self.source_model.get_actions()

    def clear_actions(self):
        """Clear selected actions."""
        index = self.currentIndex()
        if self.source_model:
            self.source_model.clear_actions()
            self.refresh_actions()
            self.filter_changed()
        self.setFocus()
        self.setCurrentIndex(index)

    def refresh_actions(self):
        """Refresh package selected actions."""
        if self.source_model:
            actions_per_package_type = self.source_model.get_actions()
            number_of_actions = 0
            for type_ in actions_per_package_type:
                actions = actions_per_package_type[type_]
                for key in actions:
                    data = actions[key]
                    number_of_actions += len(data)
            self.sig_actions_updated.emit(number_of_actions)

    def update_style_sheet(self, style_sheet=None):
        """Update custom CSS style sheet."""
        if style_sheet is None:
            self.style_sheet = load_style_sheet()
        else:
            self.style_sheet = style_sheet
        self.setStyleSheet(self.style_sheet)

    @staticmethod
    def open_url(url):
        """
        Open link from action in default operating system browser.

        ADD TRACKING!.
        """
        if url:
            QDesktopServices.openUrl(QUrl(url))