diff --git a/src/Mod/AddonManager/AddonManager.py b/src/Mod/AddonManager/AddonManager.py index 13fcfb6e0f..fad624cbdb 100644 --- a/src/Mod/AddonManager/AddonManager.py +++ b/src/Mod/AddonManager/AddonManager.py @@ -481,7 +481,7 @@ class CommandAddonManager: def activate_table_widgets(self) -> None: self.packageList.setEnabled(True) - self.packageList.ui.lineEditFilter.setFocus() + self.packageList.ui.search_box.setFocus() self.do_next_startup_phase() def populate_macros(self) -> None: @@ -824,7 +824,7 @@ class CommandAddonManager: self.dialog.labelStatusInfo.hide() self.dialog.progressBar.hide() self.dialog.buttonPauseUpdate.hide() - self.packageList.ui.lineEditFilter.setFocus() + self.packageList.ui.search_box.setFocus() def show_progress_widgets(self) -> None: if self.dialog.progressBar.isHidden(): diff --git a/src/Mod/AddonManager/CMakeLists.txt b/src/Mod/AddonManager/CMakeLists.txt index bedb988620..1948b69d14 100644 --- a/src/Mod/AddonManager/CMakeLists.txt +++ b/src/Mod/AddonManager/CMakeLists.txt @@ -1,5 +1,6 @@ IF (BUILD_GUI) PYSIDE_WRAP_RC(AddonManager_QRC_SRCS Resources/AddonManager.qrc) + add_subdirectory(Widgets) ENDIF (BUILD_GUI) SET(AddonManager_SRCS diff --git a/src/Mod/AddonManager/Resources/AddonManager.qrc b/src/Mod/AddonManager/Resources/AddonManager.qrc index 2d006a46b0..e9b5a90e90 100644 --- a/src/Mod/AddonManager/Resources/AddonManager.qrc +++ b/src/Mod/AddonManager/Resources/AddonManager.qrc @@ -64,6 +64,7 @@ icons/workfeature_workbench_icon.svg icons/yaml-workspace_workbench_icon.svg icons/compact_view.svg + icons/composite_view.svg icons/expanded_view.svg licenses/Apache-2.0.txt licenses/BSD-2-Clause.txt diff --git a/src/Mod/AddonManager/Resources/icons/composite_view.svg b/src/Mod/AddonManager/Resources/icons/composite_view.svg new file mode 100644 index 0000000000..7f1bf475be --- /dev/null +++ b/src/Mod/AddonManager/Resources/icons/composite_view.svg @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/src/Mod/AddonManager/Widgets/CMakeLists.txt b/src/Mod/AddonManager/Widgets/CMakeLists.txt new file mode 100644 index 0000000000..2f1f6762a5 --- /dev/null +++ b/src/Mod/AddonManager/Widgets/CMakeLists.txt @@ -0,0 +1,22 @@ +SET(AddonManagerWidget_SRCS + __init__.py + addonmanager_widget_filter_selector.py + addonmanager_widget_search.py + addonmanager_widget_top_bar.py + addonmanager_widget_view_selector.py +) + +SOURCE_GROUP("" FILES ${AddonManagerWidget_SRCS}) + +ADD_CUSTOM_TARGET(AddonManagerWidget ALL + SOURCES ${AddonManagerWidget_SRCS} +) + +fc_copy_sources(AddonManagerWidget "${CMAKE_BINARY_DIR}/Mod/AddonManager/Widgets" ${AddonManagerWidget_SRCS}) + +INSTALL( + FILES + ${AddonManagerWidget_SRCS} + DESTINATION + Mod/AddonManager/Widgets +) diff --git a/src/Mod/AddonManager/Widgets/__init__.py b/src/Mod/AddonManager/Widgets/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Mod/AddonManager/Widgets/addonmanager_widget_filter_selector.py b/src/Mod/AddonManager/Widgets/addonmanager_widget_filter_selector.py new file mode 100644 index 0000000000..906dc02e57 --- /dev/null +++ b/src/Mod/AddonManager/Widgets/addonmanager_widget_filter_selector.py @@ -0,0 +1,243 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# *************************************************************************** +# * * +# * Copyright (c) 2022-2024 FreeCAD Project Association * +# * * +# * This file is part of FreeCAD. * +# * * +# * FreeCAD is free software: you can redistribute it and/or modify it * +# * under the terms of the GNU Lesser General Public License as * +# * published by the Free Software Foundation, either version 2.1 of the * +# * License, or (at your option) any later version. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, but * +# * WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * +# * Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Lesser General Public * +# * License along with FreeCAD. If not, see * +# * . * +# * * +# *************************************************************************** + +""" Defines a QWidget-derived class for displaying the view selection buttons. """ + +from enum import IntEnum + +try: + import FreeCAD + + translate = FreeCAD.Qt.translate +except ImportError: + FreeCAD = None + + def translate(_: str, text: str): + return text + + +# Get whatever version of PySide we can +try: + import PySide # Use the FreeCAD wrapper +except ImportError: + try: + import PySide6 # Outside FreeCAD, try Qt6 first + + PySide = PySide6 + except ImportError: + import PySide2 # Fall back to Qt5 (if this fails, Python will kill this module's import) + + PySide = PySide2 + +from PySide import QtCore, QtWidgets + + +class FilterType(IntEnum): + """There are currently two sections in this drop down, for two different types of filters.""" + + PACKAGE_CONTENTS = 0 + INSTALLATION_STATUS = 1 + + +class StatusFilter(IntEnum): + """Predefined filters for status""" + + ANY = 0 + INSTALLED = 1 + NOT_INSTALLED = 2 + UPDATE_AVAILABLE = 3 + + +class ContentFilter(IntEnum): + """Predefined filters for addon content type""" + + ANY = 0 + WORKBENCH = 1 + MACRO = 2 + PREFERENCE_PACK = 3 + + +class Filter: + def __init__(self): + self.status_filter = StatusFilter.ANY + self.content_filter = ContentFilter.ANY + + +class WidgetFilterSelector(QtWidgets.QComboBox): + """A label and menu for selecting what sort of addons are displayed""" + + filter_changed = QtCore.Signal(object) # technically, actually class Filter + + def __init__(self, parent: QtWidgets.QWidget = None): + super().__init__(parent) + self.addon_type_index = 0 + self.installation_status_index = 0 + self._setup_ui() + self._setup_connections() + self.retranslateUi(None) + self.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents) + + def _setup_ui(self): + self._build_menu() + + def _build_menu(self): + self.clear() + self.addItem(translate("AddonsInstaller", "Filter by...")) + self.insertSeparator(self.count()) + self.addItem(translate("AddonsInstaller", "Addon Type")) + self.addon_type_index = self.count() - 1 + self.addItem( + translate("AddonsInstaller", "Any"), (FilterType.PACKAGE_CONTENTS, ContentFilter.ANY) + ) + self.addItem( + translate("AddonsInstaller", "Workbench"), + (FilterType.PACKAGE_CONTENTS, ContentFilter.WORKBENCH), + ) + self.addItem( + translate("AddonsInstaller", "Macro"), + (FilterType.PACKAGE_CONTENTS, ContentFilter.MACRO), + ) + self.addItem( + translate("AddonsInstaller", "Preference Pack"), + (FilterType.PACKAGE_CONTENTS, ContentFilter.PREFERENCE_PACK), + ) + self.insertSeparator(self.count()) + self.addItem(translate("AddonsInstaller", "Installation Status")) + self.installation_status_index = self.count() - 1 + self.addItem( + translate("AddonsInstaller", "Any"), (FilterType.INSTALLATION_STATUS, StatusFilter.ANY) + ) + self.addItem( + translate("AddonsInstaller", "Not installed"), + (FilterType.INSTALLATION_STATUS, StatusFilter.NOT_INSTALLED), + ) + self.addItem( + translate("AddonsInstaller", "Installed"), + (FilterType.INSTALLATION_STATUS, StatusFilter.INSTALLED), + ) + self.addItem( + translate("AddonsInstaller", "Update available"), + (FilterType.INSTALLATION_STATUS, StatusFilter.UPDATE_AVAILABLE), + ) + model: QtCore.QAbstractItemModel = self.model() + for row in range(model.rowCount()): + if row <= self.addon_type_index: + model.item(row).setEnabled(False) + elif row < self.installation_status_index: + item = model.item(row) + item.setCheckState(QtCore.Qt.Unchecked) + elif row == self.installation_status_index: + model.item(row).setEnabled(False) + else: + item = model.item(row) + item.setCheckState(QtCore.Qt.Unchecked) + + for row in range(model.rowCount()): + data = self.itemData(row) + if data: + item = model.item(row) + if data[0] == FilterType.PACKAGE_CONTENTS and data[1] == ContentFilter.ANY: + item.setCheckState(QtCore.Qt.Checked) + elif data[0] == FilterType.INSTALLATION_STATUS and data[1] == StatusFilter.ANY: + item.setCheckState(QtCore.Qt.Checked) + else: + item.setCheckState(QtCore.Qt.Unchecked) + + def set_contents_filter(self, contents_filter: ContentFilter): + model = self.model() + for row in range(model.rowCount()): + item = model.item(row) + user_data = self.itemData(row) + if user_data and user_data[0] == FilterType.PACKAGE_CONTENTS: + if user_data[1] == contents_filter: + item.setCheckState(QtCore.Qt.Checked) + else: + item.setCheckState(QtCore.Qt.Unchecked) + + def set_status_filter(self, status_filter: StatusFilter): + model = self.model() + for row in range(model.rowCount()): + item = model.item(row) + user_data = self.itemData(row) + if user_data and user_data[0] == FilterType.INSTALLATION_STATUS: + if user_data[1] == status_filter: + item.setCheckState(QtCore.Qt.Checked) + else: + item.setCheckState(QtCore.Qt.Unchecked) + + def _setup_connections(self): + self.activated.connect(self._selected) + + def retranslateUi(self, _): + self._build_menu() + + def _selected(self, row: int): + if row == 0: + return + if row == self.installation_status_index or row == self.addon_type_index: + self.setCurrentIndex(0) + return + model = self.model() + selected_data = self.itemData(row) + if not selected_data: + return + selected_row_type = selected_data[0] + + for row in range(model.rowCount()): + item = model.item(row) + user_data = self.itemData(row) + if user_data and user_data[0] == selected_row_type: + if user_data[1] == selected_data[1]: + item.setCheckState(QtCore.Qt.Checked) + else: + item.setCheckState(QtCore.Qt.Unchecked) + self._emit_current_filter() + self.setCurrentIndex(0) + self._update_first_row_text() + + def _emit_current_filter(self): + model = self.model() + new_filter = Filter() + for row in range(model.rowCount()): + item = model.item(row) + data = self.itemData(row) + if data and item.checkState() == QtCore.Qt.Checked: + if data[0] == FilterType.INSTALLATION_STATUS: + new_filter.status_filter = data[1] + elif data[0] == FilterType.PACKAGE_CONTENTS: + new_filter.content_filter = data[1] + self.filter_changed.emit(new_filter) + + def _update_first_row_text(self): + model = self.model() + state1 = "" + state2 = "" + for row in range(model.rowCount()): + item = model.item(row) + if item.checkState() == QtCore.Qt.Checked: + if not state1: + state1 = item.text() + else: + state2 = item.text() + break + model.item(0).setText(translate("AddonsInstaller", "Filter") + f": {state1}, {state2}") diff --git a/src/Mod/AddonManager/Widgets/addonmanager_widget_search.py b/src/Mod/AddonManager/Widgets/addonmanager_widget_search.py new file mode 100644 index 0000000000..12c0ffd52b --- /dev/null +++ b/src/Mod/AddonManager/Widgets/addonmanager_widget_search.py @@ -0,0 +1,103 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# *************************************************************************** +# * * +# * Copyright (c) 2022-2024 FreeCAD Project Association * +# * * +# * This file is part of FreeCAD. * +# * * +# * FreeCAD is free software: you can redistribute it and/or modify it * +# * under the terms of the GNU Lesser General Public License as * +# * published by the Free Software Foundation, either version 2.1 of the * +# * License, or (at your option) any later version. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, but * +# * WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * +# * Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Lesser General Public * +# * License along with FreeCAD. If not, see * +# * . * +# * * +# *************************************************************************** + +""" Defines a QWidget-derived class for displaying the view selection buttons. """ + +try: + import FreeCAD + + translate = FreeCAD.Qt.translate +except ImportError: + FreeCAD = None + + def translate(_: str, text: str): + return text + + +# Get whatever version of PySide we can +try: + import PySide # Use the FreeCAD wrapper +except ImportError: + try: + import PySide6 # Outside FreeCAD, try Qt6 first + + PySide = PySide6 + except ImportError: + import PySide2 # Fall back to Qt5 (if this fails, Python will kill this module's import) + + PySide = PySide2 + +from PySide import QtCore, QtGui, QtWidgets + + +class WidgetSearch(QtWidgets.QWidget): + """A widget for selecting the Addon Manager's primary view mode""" + + search_changed = QtCore.Signal(str) + + def __init__(self, parent: QtWidgets.QWidget = None): + super().__init__(parent) + self._setup_ui() + self._setup_connections() + self.retranslateUi(None) + + def _setup_ui(self): + self.horizontal_layout = QtWidgets.QHBoxLayout() + self.filter_line_edit = QtWidgets.QLineEdit(self) + self.filter_line_edit.setClearButtonEnabled(True) + self.horizontal_layout.addWidget(self.filter_line_edit) + self.filter_validity_label = QtWidgets.QLabel(self) + self.horizontal_layout.addWidget(self.filter_validity_label) + self.filter_validity_label.hide() # This widget starts hidden + self.setLayout(self.horizontal_layout) + + def _setup_connections(self): + self.filter_line_edit.textChanged.connect(self.set_text_filter) + + def set_text_filter(self, text_filter: str) -> None: + """Set the current filter. If the filter is valid, this will emit a filter_changed + signal. text_filter may be regular expression.""" + + if text_filter: + test_regex = QtCore.QRegularExpression(text_filter) + if test_regex.isValid(): + self.filter_validity_label.setToolTip( + translate("AddonsInstaller", "Filter is valid") + ) + icon = QtGui.QIcon.fromTheme("ok", QtGui.QIcon(":/icons/edit_OK.svg")) + self.filter_validity_label.setPixmap(icon.pixmap(16, 16)) + else: + self.filter_validity_label.setToolTip( + translate("AddonsInstaller", "Filter regular expression is invalid") + ) + icon = QtGui.QIcon.fromTheme("cancel", QtGui.QIcon(":/icons/edit_Cancel.svg")) + self.filter_validity_label.setPixmap(icon.pixmap(16, 16)) + self.filter_validity_label.show() + else: + self.filter_validity_label.hide() + self.search_changed.emit(text_filter) + + def retranslateUi(self, _): + self.filter_line_edit.setPlaceholderText( + QtCore.QCoreApplication.translate("AddonsInstaller", "Filter", None) + ) diff --git a/src/Mod/AddonManager/Widgets/addonmanager_widget_top_bar.py b/src/Mod/AddonManager/Widgets/addonmanager_widget_top_bar.py new file mode 100644 index 0000000000..d1c6993d7e --- /dev/null +++ b/src/Mod/AddonManager/Widgets/addonmanager_widget_top_bar.py @@ -0,0 +1,159 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# *************************************************************************** +# * * +# * Copyright (c) 2022-2024 FreeCAD Project Association * +# * * +# * This file is part of FreeCAD. * +# * * +# * FreeCAD is free software: you can redistribute it and/or modify it * +# * under the terms of the GNU Lesser General Public License as * +# * published by the Free Software Foundation, either version 2.1 of the * +# * License, or (at your option) any later version. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, but * +# * WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * +# * Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Lesser General Public * +# * License along with FreeCAD. If not, see * +# * . * +# * * +# *************************************************************************** + +""" Defines a class derived from QWidget for displaying the bar at the top of the addons list. """ + +from enum import IntEnum +from addonmanager_widget_view_selector import WidgetViewSelector + +# Get whatever version of PySide we can +try: + import PySide # Use the FreeCAD wrapper +except ImportError: + try: + import PySide6 # Outside FreeCAD, try Qt6 first + + PySide = PySide6 + except ImportError: + import PySide2 # Fall back to Qt5 (if this fails, Python will kill this module's import) + + PySide = PySide2 + +from PySide import QtCore, QtWidgets + +translate = FreeCAD.Qt.translate + + +class StatusFilter(IntEnum): + """Predefined filers""" + + ANY = 0 + INSTALLED = 1 + NOT_INSTALLED = 2 + UPDATE_AVAILABLE = 3 + + +# pylint: disable=too-few-public-methods + + +class WidgetTopBar(QtWidgets.QWidget): + """A widget to display the buttons at the top of the Addon manager, for changing the view, + filtering, and sorting.""" + + view_changed = QtCore.Signal(int) + filter_changed = QtCore.Signal(str) + search_changed = QtCore.Signal(str) + + def __init__(self, parent=None): + super().__init__(parent) + self.horizontal_layout = None + self.package_type_label = None + self.package_type_combobox = None + self._setup_ui() + self._setup_connections() + self.retranslateUi() + + def _setup_ui(self): + self.horizontal_layout = QtWidgets.QHBoxLayout() + + self.view_selector = WidgetViewSelector(self) + self.horizontal_layout.addWidget(self.view_selector) + + self.package_type_label = QtWidgets.QLabel(self) + self.horizontal_layout.addWidget(self.package_type_label) + + self.package_type_combobox = QtWidgets.QComboBox(self) + self.horizontal_layout.addWidget(self.package_type_combobox) + + self.status_combo_box = QtWidgets.QComboBox(self) + self.horizontal_layout.addWidget(self.status_combo_box) + + self.filter_line_edit = QtWidgets.QLineEdit(self) + self.horizontal_layout.addWidget(self.filter_line_edit) + + # Only shows when the user types in a filter + self.ui.filter_validity_label = QtWidgets.QLabel(self) + self.horizontal_layout.addWidget(self.filter_validity_label) + self.ui.filter_validity_label.hide() + + def _setup_connections(self): + self.ui.view_selector.view_changed.connect(self.view_changed.emit) + + # Set up the view the same as the last time: + pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") + package_type = pref.GetInt("PackageTypeSelection", 1) + self.ui.comboPackageType.setCurrentIndex(package_type) + status = pref.GetInt("StatusSelection", 0) + self.ui.comboStatus.setCurrentIndex(status) + + def update_type_filter(self, type_filter: int) -> None: + """hide/show rows corresponding to the type filter + + type_filter is an integer: 0 for all, 1 for workbenches, 2 for macros, + and 3 for preference packs + + """ + + self.item_filter.setPackageFilter(type_filter) + pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") + pref.SetInt("PackageTypeSelection", type_filter) + + def update_status_filter(self, status_filter: int) -> None: + """hide/show rows corresponding to the status filter + + status_filter is an integer: 0 for any, 1 for installed, 2 for not installed, + and 3 for update available + + """ + + self.item_filter.setStatusFilter(status_filter) + pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") + pref.SetInt("StatusSelection", status_filter) + + def update_text_filter(self, text_filter: str) -> None: + """filter name and description by the regex specified by text_filter""" + + if text_filter: + if hasattr(self.item_filter, "setFilterRegularExpression"): # Added in Qt 5.12 + test_regex = QtCore.QRegularExpression(text_filter) + else: + test_regex = QtCore.QRegExp(text_filter) + if test_regex.isValid(): + self.ui.labelFilterValidity.setToolTip( + translate("AddonsInstaller", "Filter is valid") + ) + icon = QtGui.QIcon.fromTheme("ok", QtGui.QIcon(":/icons/edit_OK.svg")) + self.ui.labelFilterValidity.setPixmap(icon.pixmap(16, 16)) + else: + self.ui.labelFilterValidity.setToolTip( + translate("AddonsInstaller", "Filter regular expression is invalid") + ) + icon = QtGui.QIcon.fromTheme("cancel", QtGui.QIcon(":/icons/edit_Cancel.svg")) + self.ui.labelFilterValidity.setPixmap(icon.pixmap(16, 16)) + self.ui.labelFilterValidity.show() + else: + self.ui.labelFilterValidity.hide() + if hasattr(self.item_filter, "setFilterRegularExpression"): # Added in Qt 5.12 + self.item_filter.setFilterRegularExpression(text_filter) + else: + self.item_filter.setFilterRegExp(text_filter) diff --git a/src/Mod/AddonManager/Widgets/addonmanager_widget_view_selector.py b/src/Mod/AddonManager/Widgets/addonmanager_widget_view_selector.py new file mode 100644 index 0000000000..7bcb9ff7ba --- /dev/null +++ b/src/Mod/AddonManager/Widgets/addonmanager_widget_view_selector.py @@ -0,0 +1,152 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# *************************************************************************** +# * * +# * Copyright (c) 2022-2024 FreeCAD Project Association * +# * * +# * This file is part of FreeCAD. * +# * * +# * FreeCAD is free software: you can redistribute it and/or modify it * +# * under the terms of the GNU Lesser General Public License as * +# * published by the Free Software Foundation, either version 2.1 of the * +# * License, or (at your option) any later version. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, but * +# * WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * +# * Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Lesser General Public * +# * License along with FreeCAD. If not, see * +# * . * +# * * +# *************************************************************************** + +""" Defines a QWidget-derived class for displaying the view selection buttons. """ + +from enum import IntEnum + +try: + import FreeCAD + + translate = FreeCAD.Qt.translate +except ImportError: + FreeCAD = None + + def translate(context: str, text: str): + return text + + +# Get whatever version of PySide we can +try: + import PySide # Use the FreeCAD wrapper +except ImportError: + try: + import PySide6 # Outside FreeCAD, try Qt6 first + + PySide = PySide6 + except ImportError: + import PySide2 # Fall back to Qt5 (if this fails, Python will kill this module's import) + + PySide = PySide2 + +from PySide import QtCore, QtGui, QtWidgets + + +class AddonManagerDisplayStyle(IntEnum): + """The display mode of the Addon Manager""" + + COMPACT = 0 + EXPANDED = 1 + COMPOSITE = 2 + + +class WidgetViewSelector(QtWidgets.QWidget): + """A widget for selecting the Addon Manager's primary view mode""" + + view_changed = QtCore.Signal(int) + + def __init__(self, parent: QtWidgets.QWidget = None): + super().__init__(parent) + self.horizontal_layout = None + self.composite_button = None + self.expanded_button = None + self.compact_button = None + self._setup_ui() + self._setup_connections() + + def set_current_view(self, view: AddonManagerDisplayStyle): + """Set the current selection. Does NOT emit a view_changed signal, only changes the + interface display.""" + self.compact_button.setChecked(False) + self.expanded_button.setChecked(False) + self.composite_button.setChecked(False) + if view == AddonManagerDisplayStyle.COMPACT: + self.compact_button.setChecked(True) + elif view == AddonManagerDisplayStyle.EXPANDED: + self.expanded_button.setChecked(True) + elif view == AddonManagerDisplayStyle.COMPOSITE: + self.composite_button.setChecked(True) + else: + if FreeCAD is not None: + FreeCAD.Console.PrintWarning(f"Unrecognized display style {view}") + + def _setup_ui(self): + self.horizontal_layout = QtWidgets.QHBoxLayout() + self.compact_button = QtWidgets.QToolButton(self) + self.compact_button.setObjectName("compact_button") + self.compact_button.setIcon( + QtGui.QIcon.fromTheme("back", QtGui.QIcon(":/icons/compact_view.svg")) + ) + self.compact_button.setCheckable(True) + self.compact_button.setAutoExclusive(True) + + self.expanded_button = QtWidgets.QToolButton(self) + self.expanded_button.setObjectName("expanded_button") + self.expanded_button.setCheckable(True) + self.expanded_button.setChecked(True) + self.expanded_button.setAutoExclusive(True) + self.expanded_button.setIcon( + QtGui.QIcon.fromTheme("expanded_view", QtGui.QIcon(":/icons/expanded_view.svg")) + ) + + self.composite_button = QtWidgets.QToolButton(self) + self.composite_button.setObjectName("expanded_button") + self.composite_button.setCheckable(True) + self.composite_button.setChecked(True) + self.composite_button.setAutoExclusive(True) + self.composite_button.setIcon( + QtGui.QIcon.fromTheme("composite_button", QtGui.QIcon(":/icons/composite_view.svg")) + ) + + self.horizontal_layout.addWidget(self.compact_button) + self.horizontal_layout.addWidget(self.expanded_button) + self.horizontal_layout.addWidget(self.composite_button) + + self.compact_button.clicked.connect( + lambda: self.view_changed.emit(AddonManagerDisplayStyle.COMPACT) + ) + self.expanded_button.clicked.connect( + lambda: self.view_changed.emit(AddonManagerDisplayStyle.EXPANDED) + ) + self.composite_button.clicked.connect( + lambda: self.view_changed.emit(AddonManagerDisplayStyle.COMPOSITE) + ) + + self.setLayout(self.horizontal_layout) + self.retranslateUi(None) + + def _setup_connections(self): + self.compact_button.clicked.connect( + lambda: self.view_changed.emit(AddonManagerDisplayStyle.COMPACT) + ) + self.expanded_button.clicked.connect( + lambda: self.view_changed.emit(AddonManagerDisplayStyle.EXPANDED) + ) + self.composite_button.clicked.connect( + lambda: self.view_changed.emit(AddonManagerDisplayStyle.COMPOSITE) + ) + + def retranslateUi(self, _): + self.composite_button.setToolTip(translate("AddonsInstaller", "Composite view")) + self.expanded_button.setToolTip(translate("AddonsInstaller", "Expanded view")) + self.compact_button.setToolTip(translate("AddonsInstaller", "Compact view")) diff --git a/src/Mod/AddonManager/compact_view.py b/src/Mod/AddonManager/compact_view.py index 2f9d549910..d92e6c4b8a 100644 --- a/src/Mod/AddonManager/compact_view.py +++ b/src/Mod/AddonManager/compact_view.py @@ -42,7 +42,12 @@ class Ui_CompactView(object): self.labelPackageName = QLabel(CompactView) self.labelPackageName.setObjectName("labelPackageName") + self.labelPackageNameSpacer = QLabel(CompactView) + self.labelPackageNameSpacer.setText(" — ") + self.labelPackageNameSpacer.setObjectName("labelPackageNameSpacer") + self.horizontalLayout_2.addWidget(self.labelPackageName) + self.horizontalLayout_2.addWidget(self.labelPackageNameSpacer) self.labelVersion = QLabel(CompactView) self.labelVersion.setObjectName("labelVersion") diff --git a/src/Mod/AddonManager/composite_view.py b/src/Mod/AddonManager/composite_view.py new file mode 100644 index 0000000000..b89041adad --- /dev/null +++ b/src/Mod/AddonManager/composite_view.py @@ -0,0 +1,56 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# *************************************************************************** +# * * +# * Copyright (c) 2022-2024 FreeCAD Project Association * +# * * +# * This file is part of FreeCAD. * +# * * +# * FreeCAD is free software: you can redistribute it and/or modify it * +# * under the terms of the GNU Lesser General Public License as * +# * published by the Free Software Foundation, either version 2.1 of the * +# * License, or (at your option) any later version. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, but * +# * WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * +# * Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Lesser General Public * +# * License along with FreeCAD. If not, see * +# * . * +# * * +# *************************************************************************** + +""" Provides a class for showing the list view and detail view at the same time. """ + +import addonmanager_freecad_interface + +# Get whatever version of PySide we can +try: + import PySide # Use the FreeCAD wrapper +except ImportError: + try: + import PySide6 # Outside FreeCAD, try Qt6 first + + PySide = PySide6 + except ImportError: + import PySide2 # Fall back to Qt5 (if this fails, Python will kill this module's import) + + PySide = PySide2 + +from PySide import QtCore, QtWidgets + + +class CompositeView(QtWidgets.QWidget): + """A widget that displays the Addon Manager's top bar, the list of Addons, and the detail + view, all on a single pane (with no switching). Detail view is shown in its "icon-only" mode + for the installation, etc. buttons. The bottom bar remains visible throughout.""" + + def __init__(self, parent=None): + super().__init__(parent) + + # TODO: Refactor the Addon Manager's display into four custom widgets: + # 1) The top bar showing the filter and search + # 2) The package list widget, which can take three forms (expanded, compact, and list) + # 3) The installer bar, which can take two forms (text and icon) + # 4) The bottom bar diff --git a/src/Mod/AddonManager/package_list.py b/src/Mod/AddonManager/package_list.py index f6676feaa5..6279990c9c 100644 --- a/src/Mod/AddonManager/package_list.py +++ b/src/Mod/AddonManager/package_list.py @@ -37,6 +37,9 @@ from expanded_view import Ui_ExpandedView import addonmanager_utilities as utils from addonmanager_metadata import get_first_supported_freecad_version, Version +from Widgets.addonmanager_widget_view_selector import WidgetViewSelector, AddonManagerDisplayStyle +from Widgets.addonmanager_widget_search import WidgetSearch +from Widgets.addonmanager_widget_filter_selector import WidgetFilterSelector, StatusFilter, Filter translate = FreeCAD.Qt.translate @@ -44,22 +47,6 @@ translate = FreeCAD.Qt.translate # pylint: disable=too-few-public-methods -class ListDisplayStyle(IntEnum): - """The display mode of the list""" - - COMPACT = 0 - EXPANDED = 1 - - -class StatusFilter(IntEnum): - """Predefined filers""" - - ANY = 0 - INSTALLED = 1 - NOT_INSTALLED = 2 - UPDATE_AVAILABLE = 3 - - class PackageList(QtWidgets.QWidget): """A widget that shows a list of packages and various widgets to control the display of the list""" @@ -77,25 +64,16 @@ class PackageList(QtWidgets.QWidget): self.ui.listPackages.setItemDelegate(self.item_delegate) self.ui.listPackages.clicked.connect(self.on_listPackages_clicked) - self.ui.comboPackageType.currentIndexChanged.connect(self.update_type_filter) - self.ui.comboStatus.currentIndexChanged.connect(self.update_status_filter) - self.ui.lineEditFilter.textChanged.connect(self.update_text_filter) - self.ui.buttonCompactLayout.clicked.connect( - lambda: self.set_view_style(ListDisplayStyle.COMPACT) - ) - self.ui.buttonExpandedLayout.clicked.connect( - lambda: self.set_view_style(ListDisplayStyle.EXPANDED) - ) - - # Only shows when the user types in a filter - self.ui.labelFilterValidity.hide() + self.ui.filter_selector.filter_changed.connect(self.update_status_filter) + self.ui.search_box.search_changed.connect(self.item_filter.setFilterRegularExpression) + self.ui.view_selector.view_changed.connect(self.set_view_style) # Set up the view the same as the last time: pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") package_type = pref.GetInt("PackageTypeSelection", 1) - self.ui.comboPackageType.setCurrentIndex(package_type) status = pref.GetInt("StatusSelection", 0) - self.ui.comboStatus.setCurrentIndex(status) + self.ui.filter_selector.set_contents_filter(package_type) + self.ui.filter_selector.set_status_filter(status) # Pre-init of other members: self.item_model = None @@ -107,12 +85,9 @@ class PackageList(QtWidgets.QWidget): self.item_filter.sort(0) pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") - style = pref.GetInt("ViewStyle", ListDisplayStyle.EXPANDED) + style = pref.GetInt("ViewStyle", AddonManagerDisplayStyle.EXPANDED) self.set_view_style(style) - if style == ListDisplayStyle.EXPANDED: - self.ui.buttonExpandedLayout.setChecked(True) - else: - self.ui.buttonCompactLayout.setChecked(True) + self.ui.view_selector.set_current_view(style) self.item_filter.setHidePy2(pref.GetBool("HidePy2", True)) self.item_filter.setHideObsolete(pref.GetBool("HideObsolete", True)) @@ -125,63 +100,21 @@ class PackageList(QtWidgets.QWidget): selected_repo = self.item_model.repos[source_selection.row()] self.itemSelected.emit(selected_repo) - def update_type_filter(self, type_filter: int) -> None: - """hide/show rows corresponding to the type filter + def update_status_filter(self, new_filter: Filter) -> None: + """hide/show rows corresponding to the specified filter""" - type_filter is an integer: 0 for all, 1 for workbenches, 2 for macros, - and 3 for preference packs - - """ - - self.item_filter.setPackageFilter(type_filter) + self.item_filter.setStatusFilter(new_filter.status_filter) + self.item_filter.setPackageFilter(new_filter.content_filter) pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") - pref.SetInt("PackageTypeSelection", type_filter) + pref.SetInt("StatusSelection", new_filter.status_filter) + pref.SetInt("PackageTypeSelection", new_filter.content_filter) - def update_status_filter(self, status_filter: int) -> None: - """hide/show rows corresponding to the status filter - - status_filter is an integer: 0 for any, 1 for installed, 2 for not installed, - and 3 for update available - - """ - - self.item_filter.setStatusFilter(status_filter) - pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") - pref.SetInt("StatusSelection", status_filter) - - def update_text_filter(self, text_filter: str) -> None: - """filter name and description by the regex specified by text_filter""" - - if text_filter: - if hasattr(self.item_filter, "setFilterRegularExpression"): # Added in Qt 5.12 - test_regex = QtCore.QRegularExpression(text_filter) - else: - test_regex = QtCore.QRegExp(text_filter) - if test_regex.isValid(): - self.ui.labelFilterValidity.setToolTip( - translate("AddonsInstaller", "Filter is valid") - ) - icon = QtGui.QIcon.fromTheme("ok", QtGui.QIcon(":/icons/edit_OK.svg")) - self.ui.labelFilterValidity.setPixmap(icon.pixmap(16, 16)) - else: - self.ui.labelFilterValidity.setToolTip( - translate("AddonsInstaller", "Filter regular expression is invalid") - ) - icon = QtGui.QIcon.fromTheme("cancel", QtGui.QIcon(":/icons/edit_Cancel.svg")) - self.ui.labelFilterValidity.setPixmap(icon.pixmap(16, 16)) - self.ui.labelFilterValidity.show() - else: - self.ui.labelFilterValidity.hide() - if hasattr(self.item_filter, "setFilterRegularExpression"): # Added in Qt 5.12 - self.item_filter.setFilterRegularExpression(text_filter) - else: - self.item_filter.setFilterRegExp(text_filter) - - def set_view_style(self, style: ListDisplayStyle) -> None: + def set_view_style(self, style: AddonManagerDisplayStyle) -> None: """Set the style (compact or expanded) of the list""" self.item_model.layoutAboutToBeChanged.emit() self.item_delegate.set_view(style) - if style == ListDisplayStyle.COMPACT: + # TODO: Update to support composite + if style == AddonManagerDisplayStyle.COMPACT: self.ui.listPackages.setSpacing(2) else: self.ui.listPackages.setSpacing(5) @@ -324,12 +257,12 @@ class PackageListItemDelegate(QtWidgets.QStyledItemDelegate): def __init__(self, parent=None): super().__init__(parent) - self.displayStyle = ListDisplayStyle.EXPANDED + self.displayStyle = AddonManagerDisplayStyle.EXPANDED self.expanded = ExpandedView() self.compact = CompactView() self.widget = self.expanded - def set_view(self, style: ListDisplayStyle) -> None: + def set_view(self, style: AddonManagerDisplayStyle) -> None: """Set the view of to style""" if not self.displayStyle == style: self.displayStyle = style @@ -343,7 +276,7 @@ class PackageListItemDelegate(QtWidgets.QStyledItemDelegate): def update_content(self, index): """Creates the display of the content for a given index.""" repo = index.data(PackageListItemModel.DataAccessRole) - if self.displayStyle == ListDisplayStyle.EXPANDED: + if self.displayStyle == AddonManagerDisplayStyle.EXPANDED: self.widget = self.expanded self.widget.ui.labelPackageName.setText(f"

{repo.display_name}

") self.widget.ui.labelIcon.setPixmap(repo.icon.pixmap(QtCore.QSize(48, 48))) @@ -353,23 +286,23 @@ class PackageListItemDelegate(QtWidgets.QStyledItemDelegate): self.widget.ui.labelIcon.setPixmap(repo.icon.pixmap(QtCore.QSize(16, 16))) self.widget.ui.labelIcon.setText("") - if self.displayStyle == ListDisplayStyle.EXPANDED: + if self.displayStyle == AddonManagerDisplayStyle.EXPANDED: self.widget.ui.labelTags.setText("") if repo.metadata: self.widget.ui.labelDescription.setText(repo.metadata.description) self.widget.ui.labelVersion.setText(f"v{repo.metadata.version}") - if self.displayStyle == ListDisplayStyle.EXPANDED: + if self.displayStyle == AddonManagerDisplayStyle.EXPANDED: self._setup_expanded_package(repo) elif repo.macro and repo.macro.parsed: self._setup_macro(repo) else: self.widget.ui.labelDescription.setText("") self.widget.ui.labelVersion.setText("") - if self.displayStyle == ListDisplayStyle.EXPANDED: + if self.displayStyle == AddonManagerDisplayStyle.EXPANDED: self.widget.ui.labelMaintainer.setText("") # Update status - if self.displayStyle == ListDisplayStyle.EXPANDED: + if self.displayStyle == AddonManagerDisplayStyle.EXPANDED: self.widget.ui.labelStatus.setText(self.get_expanded_update_string(repo)) else: self.widget.ui.labelStatus.setText(self.get_compact_update_string(repo)) @@ -417,7 +350,7 @@ class PackageListItemDelegate(QtWidgets.QStyledItemDelegate): + repo.macro.date ) self.widget.ui.labelVersion.setText("" + version_string + "") - if self.displayStyle == ListDisplayStyle.EXPANDED: + if self.displayStyle == AddonManagerDisplayStyle.EXPANDED: if repo.macro.author: caption = translate("AddonsInstaller", "Author") self.widget.ui.labelMaintainer.setText(caption + ": " + repo.macro.author) @@ -665,65 +598,23 @@ class Ui_PackageList: self.verticalLayout.setObjectName("verticalLayout") self.horizontalLayout_6 = QtWidgets.QHBoxLayout() self.horizontalLayout_6.setObjectName("horizontalLayout_6") - self.buttonCompactLayout = QtWidgets.QToolButton(form) - self.buttonCompactLayout.setObjectName("buttonCompactLayout") - self.buttonCompactLayout.setCheckable(True) - self.buttonCompactLayout.setAutoExclusive(True) - self.buttonCompactLayout.setIcon( - QtGui.QIcon.fromTheme("expanded_view", QtGui.QIcon(":/icons/compact_view.svg")) - ) - self.horizontalLayout_6.addWidget(self.buttonCompactLayout) - - self.buttonExpandedLayout = QtWidgets.QToolButton(form) - self.buttonExpandedLayout.setObjectName("buttonExpandedLayout") - self.buttonExpandedLayout.setCheckable(True) - self.buttonExpandedLayout.setChecked(True) - self.buttonExpandedLayout.setAutoExclusive(True) - self.buttonExpandedLayout.setIcon( - QtGui.QIcon.fromTheme("expanded_view", QtGui.QIcon(":/icons/expanded_view.svg")) - ) - - self.horizontalLayout_6.addWidget(self.buttonExpandedLayout) + self.view_selector = WidgetViewSelector(form) + self.horizontalLayout_6.addWidget(self.view_selector) self.labelPackagesContaining = QtWidgets.QLabel(form) self.labelPackagesContaining.setObjectName("labelPackagesContaining") self.horizontalLayout_6.addWidget(self.labelPackagesContaining) - self.comboPackageType = QtWidgets.QComboBox(form) - self.comboPackageType.addItem("") - self.comboPackageType.addItem("") - self.comboPackageType.addItem("") - self.comboPackageType.addItem("") - self.comboPackageType.setObjectName("comboPackageType") + self.filter_selector = WidgetFilterSelector(form) + self.filter_selector.setObjectName("filter_selector") + self.horizontalLayout_6.addWidget(self.filter_selector) - self.horizontalLayout_6.addWidget(self.comboPackageType) + self.search_box = WidgetSearch(form) + self.search_box.setObjectName("search_box") - self.labelStatus = QtWidgets.QLabel(form) - self.labelStatus.setObjectName("labelStatus") - - self.horizontalLayout_6.addWidget(self.labelStatus) - - self.comboStatus = QtWidgets.QComboBox(form) - self.comboStatus.addItem("") - self.comboStatus.addItem("") - self.comboStatus.addItem("") - self.comboStatus.addItem("") - self.comboStatus.setObjectName("comboStatus") - - self.horizontalLayout_6.addWidget(self.comboStatus) - - self.lineEditFilter = QtWidgets.QLineEdit(form) - self.lineEditFilter.setObjectName("lineEditFilter") - self.lineEditFilter.setClearButtonEnabled(True) - - self.horizontalLayout_6.addWidget(self.lineEditFilter) - - self.labelFilterValidity = QtWidgets.QLabel(form) - self.labelFilterValidity.setObjectName("labelFilterValidity") - - self.horizontalLayout_6.addWidget(self.labelFilterValidity) + self.horizontalLayout_6.addWidget(self.search_box) self.verticalLayout.addLayout(self.horizontalLayout_6) @@ -744,44 +635,4 @@ class Ui_PackageList: QtCore.QMetaObject.connectSlotsByName(form) def retranslateUi(self, _): - self.labelPackagesContaining.setText( - QtCore.QCoreApplication.translate("AddonsInstaller", "Show Addons containing:", None) - ) - self.comboPackageType.setItemText( - 0, QtCore.QCoreApplication.translate("AddonsInstaller", "All", None) - ) - self.comboPackageType.setItemText( - 1, QtCore.QCoreApplication.translate("AddonsInstaller", "Workbenches", None) - ) - self.comboPackageType.setItemText( - 2, QtCore.QCoreApplication.translate("AddonsInstaller", "Macros", None) - ) - self.comboPackageType.setItemText( - 3, - QtCore.QCoreApplication.translate("AddonsInstaller", "Preference Packs", None), - ) - self.labelStatus.setText( - QtCore.QCoreApplication.translate("AddonsInstaller", "Status:", None) - ) - self.comboStatus.setItemText( - StatusFilter.ANY, - QtCore.QCoreApplication.translate("AddonsInstaller", "Any", None), - ) - self.comboStatus.setItemText( - StatusFilter.INSTALLED, - QtCore.QCoreApplication.translate("AddonsInstaller", "Installed", None), - ) - self.comboStatus.setItemText( - StatusFilter.NOT_INSTALLED, - QtCore.QCoreApplication.translate("AddonsInstaller", "Not installed", None), - ) - self.comboStatus.setItemText( - StatusFilter.UPDATE_AVAILABLE, - QtCore.QCoreApplication.translate("AddonsInstaller", "Update available", None), - ) - self.lineEditFilter.setPlaceholderText( - QtCore.QCoreApplication.translate("AddonsInstaller", "Filter", None) - ) - self.labelFilterValidity.setText( - QtCore.QCoreApplication.translate("AddonsInstaller", "OK", None) - ) + pass