diff --git a/src/Gui/SoFCUnifiedSelection.cpp b/src/Gui/SoFCUnifiedSelection.cpp
index ff66d3a678..c853ce333b 100644
--- a/src/Gui/SoFCUnifiedSelection.cpp
+++ b/src/Gui/SoFCUnifiedSelection.cpp
@@ -782,6 +782,7 @@ void SoHighlightElementAction::initClass()
SO_ACTION_INIT_CLASS(SoHighlightElementAction,SoAction);
SO_ENABLE(SoHighlightElementAction, SoSwitchElement);
+ SO_ENABLE(SoHighlightElementAction, SoModelMatrixElement);
SO_ACTION_ADD_METHOD(SoNode,nullAction);
@@ -849,6 +850,7 @@ void SoSelectionElementAction::initClass()
SO_ACTION_INIT_CLASS(SoSelectionElementAction,SoAction);
SO_ENABLE(SoSelectionElementAction, SoSwitchElement);
+ SO_ENABLE(SoSelectionElementAction, SoModelMatrixElement);
SO_ACTION_ADD_METHOD(SoNode,nullAction);
diff --git a/src/Mod/AddonManager/AddonManager.py b/src/Mod/AddonManager/AddonManager.py
index 03e2f503b1..b234b3e538 100644
--- a/src/Mod/AddonManager/AddonManager.py
+++ b/src/Mod/AddonManager/AddonManager.py
@@ -54,10 +54,9 @@ from addonmanager_update_all_gui import UpdateAllGUI
import addonmanager_utilities as utils
import addonmanager_freecad_interface as fci
import AddonManager_rc # This is required by Qt, it's not unused
-from package_list import PackageList, PackageListItemModel
-from addonmanager_package_details_controller import PackageDetailsController
-from Widgets.addonmanager_widget_package_details_view import PackageDetailsView
+from composite_view import CompositeView
from Widgets.addonmanager_widget_global_buttons import WidgetGlobalButtonBar
+from package_list import PackageListItemModel
from Addon import Addon
from AddonStats import AddonStats
from manage_python_dependencies import (
@@ -138,11 +137,13 @@ class CommandAddonManager:
self.update_all_worker = None
self.developer_mode = None
self.installer_gui = None
+ self.composite_view = None
self.button_bar = None
self.update_cache = False
self.dialog = None
self.startup_sequence = []
+ self.packages_with_updates = set()
# Set up the connection checker
self.connection_checker = ConnectionCheckerGUI()
@@ -193,7 +194,7 @@ class CommandAddonManager:
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
w = pref.GetInt("WindowWidth", 800)
h = pref.GetInt("WindowHeight", 600)
- self.dialog.resize(w, h)
+ self.composite_view = CompositeView(self.dialog)
self.button_bar = WidgetGlobalButtonBar(self.dialog)
# If we are checking for updates automatically, hide the Check for updates button:
@@ -204,19 +205,11 @@ class CommandAddonManager:
self.button_bar.update_all_addons.hide()
# Set up the listing of packages using the model-view-controller architecture
- self.package_list = PackageList(self.dialog)
self.item_model = PackageListItemModel()
- self.package_list.setModel(self.item_model)
- self.dialog.layout().addWidget(self.package_list)
+ self.composite_view.setModel(self.item_model)
+ self.dialog.layout().addWidget(self.composite_view)
self.dialog.layout().addWidget(self.button_bar)
- # Package details start out hidden
- self.packageDetails = PackageDetailsView(self.dialog)
- self.package_details_controller = PackageDetailsController(self.packageDetails)
- self.packageDetails.hide()
- index = self.dialog.layout().indexOf(self.package_list)
- self.dialog.layout().insertWidget(index, self.packageDetails)
-
# set nice icons to everything, by theme with fallback to FreeCAD icons
self.dialog.setWindowIcon(QtGui.QIcon(":/icons/AddonManager.svg"))
@@ -243,17 +236,16 @@ class CommandAddonManager:
)
self.button_bar.python_dependencies.clicked.connect(self.show_python_updates_dialog)
self.button_bar.developer_tools.clicked.connect(self.show_developer_tools)
- self.package_list.ui.progressBar.stop_clicked.connect(self.stop_update)
- self.package_list.itemSelected.connect(self.table_row_activated)
- self.package_list.setEnabled(False)
- self.package_details_controller.execute.connect(self.executemacro)
- self.package_details_controller.install.connect(self.launch_installer_gui)
- self.package_details_controller.uninstall.connect(self.remove)
- self.package_details_controller.update.connect(self.update)
- self.package_details_controller.back.connect(self.on_buttonBack_clicked)
- self.package_details_controller.update_status.connect(self.status_updated)
+ self.composite_view.package_list.ui.progressBar.stop_clicked.connect(self.stop_update)
+ self.composite_view.package_list.setEnabled(False)
+ self.composite_view.execute.connect(self.executemacro)
+ self.composite_view.install.connect(self.launch_installer_gui)
+ self.composite_view.uninstall.connect(self.remove)
+ self.composite_view.update.connect(self.update)
+ self.composite_view.update_status.connect(self.status_updated)
# center the dialog over the FreeCAD window
+ self.dialog.resize(w, h)
mw = FreeCADGui.getMainWindow()
self.dialog.move(
mw.frameGeometry().topLeft() + mw.rect().center() - self.dialog.rect().center()
@@ -400,14 +392,11 @@ class CommandAddonManager:
self.check_python_updates,
self.fetch_addon_stats,
self.fetch_addon_score,
+ self.select_addon,
]
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
if pref.GetBool("DownloadMacros", False):
self.startup_sequence.append(self.load_macro_metadata)
- selection = pref.GetString("SelectedAddon", "")
- if selection:
- self.startup_sequence.insert(2, functools.partial(self.select_addon, selection))
- pref.SetString("SelectedAddon", "")
self.number_of_progress_regions = len(self.startup_sequence)
self.current_progress_region = 0
self.do_next_startup_phase()
@@ -428,7 +417,7 @@ class CommandAddonManager:
)
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
pref.SetString("LastCacheUpdate", date.today().isoformat())
- self.package_list.item_filter.invalidateFilter()
+ self.composite_view.package_list.item_filter.invalidateFilter()
def populate_packages_table(self) -> None:
self.item_model.clear()
@@ -481,8 +470,8 @@ class CommandAddonManager:
f.write(json.dumps(self.package_cache, indent=" "))
def activate_table_widgets(self) -> None:
- self.package_list.setEnabled(True)
- self.package_list.ui.view_bar.search.setFocus()
+ self.composite_view.package_list.setEnabled(True)
+ self.composite_view.package_list.ui.view_bar.search.setFocus()
self.do_next_startup_phase()
def populate_macros(self) -> None:
@@ -581,17 +570,12 @@ class CommandAddonManager:
else:
self.do_next_startup_phase()
- def select_addon(self, name: str) -> None:
- found = False
- for addon in self.item_model.repos:
- if addon.name == name:
- self.table_row_activated(addon)
- found = True
- break
- if not found:
- FreeCAD.Console.PrintWarning(
- translate("AddonsInstaller", "Could not find addon '{}' to select\n").format(name)
- )
+ def select_addon(self) -> None:
+ prefs = fci.Preferences()
+ selection = prefs.get("SelectedAddon")
+ if selection:
+ self.composite_view.package_list.select_addon(selection)
+ prefs.set("SelectedAddon", "")
self.do_next_startup_phase()
def check_updates(self) -> None:
@@ -648,7 +632,11 @@ class CommandAddonManager:
if number_of_updates:
self.button_bar.set_number_of_available_updates(number_of_updates)
- elif hasattr(self, "check_worker") and self.check_worker.isRunning():
+ elif (
+ hasattr(self, "check_worker")
+ and self.check_worker is not None
+ and self.check_worker.isRunning()
+ ):
self.button_bar.update_all_addons.setText(
translate("AddonsInstaller", "Checking for updates...")
)
@@ -698,14 +686,14 @@ class CommandAddonManager:
self.get_addon_score_worker.update_addon_score.connect(self.update_addon_score)
self.get_addon_score_worker.start()
else:
- self.package_list.ui.view_bar.set_rankings_available(False)
+ self.composite_view.package_list.ui.view_bar.set_rankings_available(False)
self.do_next_startup_phase()
def update_addon_score(self, addon: Addon):
self.item_model.reload_item(addon)
def score_fetched_successfully(self):
- self.package_list.ui.view_bar.set_rankings_available(True)
+ self.composite_view.package_list.ui.view_bar.set_rankings_available(True)
def show_developer_tools(self) -> None:
"""Display the developer tools dialog"""
@@ -780,27 +768,11 @@ class CommandAddonManager:
return addonicon
- def table_row_activated(self, selected_repo: Addon) -> None:
- """a row was activated, show the relevant data"""
-
- self.package_list.hide()
- self.packageDetails.show()
- self.package_details_controller.show_repo(selected_repo)
-
def show_information(self, message: str) -> None:
"""shows generic text in the information pane"""
- self.package_list.ui.progressBar.set_status(message)
- self.package_list.ui.progressBar.repaint()
-
- def show_workbench(self, repo: Addon) -> None:
- self.package_list.hide()
- self.packageDetails.show()
- self.package_details_controller.show_repo(repo)
-
- def on_buttonBack_clicked(self) -> None:
- self.packageDetails.hide()
- self.package_list.show()
+ self.composite_view.package_list.ui.progressBar.set_status(message)
+ self.composite_view.package_list.ui.progressBar.repaint()
def append_to_repos_list(self, repo: Addon) -> None:
"""this function allows threads to update the main list of workbenches"""
@@ -815,7 +787,7 @@ class CommandAddonManager:
else:
repo.set_status(Addon.Status.NO_UPDATE_AVAILABLE)
self.item_model.reload_item(repo)
- self.package_details_controller.show_repo(repo)
+ self.composite_view.package_details_controller.show_repo(repo)
def launch_installer_gui(self, addon: Addon) -> None:
if self.installer_gui is not None:
@@ -861,12 +833,12 @@ class CommandAddonManager:
def hide_progress_widgets(self) -> None:
"""hides the progress bar and related widgets"""
- self.package_list.ui.progressBar.hide()
- self.package_list.ui.view_bar.search.setFocus()
+ self.composite_view.package_list.ui.progressBar.hide()
+ self.composite_view.package_list.ui.view_bar.search.setFocus()
def show_progress_widgets(self) -> None:
- if self.package_list.ui.progressBar.isHidden():
- self.package_list.ui.progressBar.show()
+ if self.composite_view.package_list.ui.progressBar.isHidden():
+ self.composite_view.package_list.ui.progressBar.show()
def update_progress_bar(self, current_value: int, max_value: int) -> None:
"""Update the progress bar, showing it if it's hidden"""
@@ -883,10 +855,10 @@ class CommandAddonManager:
completed_region_portion = (self.current_progress_region - 1) * region_size
current_region_portion = (float(current_value) / float(max_value)) * region_size
value = completed_region_portion + current_region_portion
- self.package_list.ui.progressBar.set_value(
+ self.composite_view.package_list.ui.progressBar.set_value(
value * 10
) # Out of 1000 segments, so it moves sort of smoothly
- self.package_list.ui.progressBar.repaint()
+ self.composite_view.package_list.ui.progressBar.repaint()
def stop_update(self) -> None:
self.cleanup_workers()
@@ -910,7 +882,7 @@ class CommandAddonManager:
if repo.status() == Addon.Status.PENDING_RESTART:
self.restart_required = True
self.item_model.reload_item(repo)
- self.package_details_controller.show_repo(repo)
+ self.composite_view.package_details_controller.show_repo(repo)
if repo in self.packages_with_updates:
self.packages_with_updates.remove(repo)
self.enable_updates(len(self.packages_with_updates))
diff --git a/src/Mod/AddonManager/CMakeLists.txt b/src/Mod/AddonManager/CMakeLists.txt
index 042ff80363..a311537cbf 100644
--- a/src/Mod/AddonManager/CMakeLists.txt
+++ b/src/Mod/AddonManager/CMakeLists.txt
@@ -31,6 +31,7 @@ SET(AddonManager_SRCS
addonmanager_macro_parser.py
addonmanager_metadata.py
addonmanager_package_details_controller.py
+ addonmanager_preferences_defaults.json
addonmanager_pyside_interface.py
addonmanager_readme_controller.py
addonmanager_update_all_gui.py
@@ -47,6 +48,7 @@ SET(AddonManager_SRCS
change_branch.py
change_branch.ui
compact_view.py
+ composite_view.py
dependency_resolution_dialog.ui
developer_mode.ui
developer_mode_add_content.ui
@@ -69,7 +71,6 @@ SET(AddonManager_SRCS
loading.html
manage_python_dependencies.py
NetworkManager.py
- addonmanager_package_details_controller.py
package_list.py
PythonDependencyUpdateDialog.ui
select_toolbar_dialog.ui
diff --git a/src/Mod/AddonManager/TODO.md b/src/Mod/AddonManager/TODO.md
index 2c51912303..b1d3e7735f 100644
--- a/src/Mod/AddonManager/TODO.md
+++ b/src/Mod/AddonManager/TODO.md
@@ -1,14 +1,8 @@
# Addon Manager Future Work
-* Restructure widgets into logical groups to better enable showing and hiding those groups all at once.
* Reduce coupling between data and UI, switching logical groupings of widgets into a MVC or similar framework.
* Particularly in the addons list
-* Download Addon statistics from central location.
- * Allow sorting on those statistics.
-* Download a "rank" from user-specified locations.
- * Allow sorting on that rank.
* Implement a server-side cache of Addon metadata.
* Implement an "offline mode" that does not attempt to use remote data for anything.
* When installing a Preference Pack, offer to apply it once installed, and to undo after that.
* Better support "headless" mode, with no GUI.
-* Add "Composite" display mode, showing compact list and details at the same time
diff --git a/src/Mod/AddonManager/Widgets/addonmanager_widget_addon_buttons.py b/src/Mod/AddonManager/Widgets/addonmanager_widget_addon_buttons.py
index 21583e8850..8c65c5998c 100644
--- a/src/Mod/AddonManager/Widgets/addonmanager_widget_addon_buttons.py
+++ b/src/Mod/AddonManager/Widgets/addonmanager_widget_addon_buttons.py
@@ -75,6 +75,8 @@ class WidgetAddonButtons(QtWidgets.QWidget):
self.retranslateUi(None)
def _setup_ui(self):
+ if self.layout():
+ self.setLayout(None) # TODO: Check this
self.horizontal_layout = QtWidgets.QHBoxLayout()
self.horizontal_layout.setContentsMargins(0, 0, 0, 0)
self.back = QtWidgets.QToolButton(self)
@@ -98,6 +100,9 @@ class WidgetAddonButtons(QtWidgets.QWidget):
self.horizontal_layout.addWidget(self.change_branch)
self.setLayout(self.horizontal_layout)
+ def set_show_back_button(self, show: bool) -> None:
+ self.back.setVisible(show)
+
def _set_icons(self):
self.back.setIcon(QtGui.QIcon.fromTheme("back", QtGui.QIcon(":/icons/button_left.svg")))
diff --git a/src/Mod/AddonManager/Widgets/addonmanager_widget_package_details_view.py b/src/Mod/AddonManager/Widgets/addonmanager_widget_package_details_view.py
index ce83b6af6e..fbe892cbb5 100644
--- a/src/Mod/AddonManager/Widgets/addonmanager_widget_package_details_view.py
+++ b/src/Mod/AddonManager/Widgets/addonmanager_widget_package_details_view.py
@@ -65,6 +65,7 @@ class MessageType(Enum):
@dataclass
class UpdateInformation:
+ unchecked: bool = True
check_in_progress: bool = False
update_available: bool = False
detached_head: bool = False
@@ -112,6 +113,7 @@ class PackageDetailsView(QtWidgets.QWidget):
self.vertical_layout.addWidget(self.message_label)
self.vertical_layout.addWidget(self.location_label)
self.vertical_layout.addWidget(self.readme_browser)
+ self.button_bar.hide() # Start with no bar
def set_location(self, location: Optional[str]):
if location is not None:
@@ -274,6 +276,8 @@ class PackageDetailsView(QtWidgets.QWidget):
def _get_update_status_string(self) -> str:
if self.update_info.check_in_progress:
return translate("AddonsInstaller", "Update check in progress") + "."
+ elif self.update_info.unchecked:
+ return ""
if self.update_info.detached_head:
return (
translate(
diff --git a/src/Mod/AddonManager/Widgets/addonmanager_widget_readme_browser.py b/src/Mod/AddonManager/Widgets/addonmanager_widget_readme_browser.py
index 8049a5a434..0677e6f305 100644
--- a/src/Mod/AddonManager/Widgets/addonmanager_widget_readme_browser.py
+++ b/src/Mod/AddonManager/Widgets/addonmanager_widget_readme_browser.py
@@ -63,6 +63,7 @@ class WidgetReadmeBrowser(QtWidgets.QTextBrowser):
def setMarkdown(self, md: str):
"""Provides an optional fallback to the markdown library for older versions of Qt (prior to 5.15) that did not
have native markdown support. Lacking that, plaintext is displayed."""
+ geometry = self.geometry()
if hasattr(super(), "setMarkdown"):
super().setMarkdown(md)
else:
@@ -76,6 +77,7 @@ class WidgetReadmeBrowser(QtWidgets.QTextBrowser):
FreeCAD.Console.Warning(
"Qt < 5.15 and no `import markdown` -- falling back to plain text display\n"
)
+ self.setGeometry(geometry)
def set_resource(self, resource_url: str, image: Optional[QtGui.QImage]):
"""Once a resource has been fetched (or the fetch has failed), this method should be used to inform the widget
diff --git a/src/Mod/AddonManager/Widgets/addonmanager_widget_view_selector.py b/src/Mod/AddonManager/Widgets/addonmanager_widget_view_selector.py
index 2bfa935bd8..dca81ec6f8 100644
--- a/src/Mod/AddonManager/Widgets/addonmanager_widget_view_selector.py
+++ b/src/Mod/AddonManager/Widgets/addonmanager_widget_view_selector.py
@@ -119,7 +119,6 @@ class WidgetViewSelector(QtWidgets.QWidget):
self.composite_button.setIcon(
QtGui.QIcon.fromTheme("composite_button", QtGui.QIcon(":/icons/composite_view.svg"))
)
- self.composite_button.hide() # TODO: Implement this view
self.horizontal_layout.addWidget(self.compact_button)
self.horizontal_layout.addWidget(self.expanded_button)
diff --git a/src/Mod/AddonManager/addonmanager_git.py b/src/Mod/AddonManager/addonmanager_git.py
index 3a63d74619..e252283493 100644
--- a/src/Mod/AddonManager/addonmanager_git.py
+++ b/src/Mod/AddonManager/addonmanager_git.py
@@ -442,10 +442,8 @@ class GitManager:
on the Mac actually requires us to check for that installation."""
try:
subprocess.check_output(["xcode-select", "-p"])
- fci.Console.PrintMessage("XCode command line tools are installed: git is available\n")
return True
except subprocess.CalledProcessError:
- fci.Console.PrintMessage("XCode command line tools are not installed: not using git\n")
return False
def _synchronous_call_git(self, args: List[str]) -> str:
diff --git a/src/Mod/AddonManager/addonmanager_package_details_controller.py b/src/Mod/AddonManager/addonmanager_package_details_controller.py
index 13cb5c90b4..9c934039fb 100644
--- a/src/Mod/AddonManager/addonmanager_package_details_controller.py
+++ b/src/Mod/AddonManager/addonmanager_package_details_controller.py
@@ -55,7 +55,6 @@ class PackageDetailsController(QtCore.QObject):
update = QtCore.Signal(Addon)
execute = QtCore.Signal(Addon)
update_status = QtCore.Signal(Addon)
- check_for_update = QtCore.Signal(Addon)
def __init__(self, widget=None):
super().__init__()
@@ -63,9 +62,10 @@ class PackageDetailsController(QtCore.QObject):
self.readme_controller = ReadmeController(self.ui.readme_browser)
self.worker = None
self.addon = None
- self.status_update_thread = None
+ self.update_check_thread = None
self.original_disabled_state = None
self.original_status = None
+ self.check_for_update_worker = None
try:
self.git_manager = GitManager()
except NoGitFound:
@@ -76,9 +76,6 @@ class PackageDetailsController(QtCore.QObject):
self.ui.button_bar.install.clicked.connect(lambda: self.install.emit(self.addon))
self.ui.button_bar.uninstall.clicked.connect(lambda: self.uninstall.emit(self.addon))
self.ui.button_bar.update.clicked.connect(lambda: self.update.emit(self.addon))
- self.ui.button_bar.check_for_update.clicked.connect(
- lambda: self.check_for_update.emit(self.addon)
- )
self.ui.button_bar.change_branch.clicked.connect(self.change_branch_clicked)
self.ui.button_bar.enable.clicked.connect(self.enable_clicked)
self.ui.button_bar.disable.clicked.connect(self.disable_clicked)
@@ -89,6 +86,10 @@ class PackageDetailsController(QtCore.QObject):
self.addon = repo
self.readme_controller.set_addon(repo)
self.original_disabled_state = self.addon.is_disabled()
+ if repo is not None:
+ self.ui.button_bar.show()
+ else:
+ self.ui.button_bar.hide()
if self.worker is not None:
if not self.worker.isFinished():
@@ -99,6 +100,7 @@ class PackageDetailsController(QtCore.QObject):
self.ui.set_installed(installed)
update_info = UpdateInformation()
if installed:
+ update_info.unchecked = self.addon.status() == Addon.Status.UNCHECKED
update_info.update_available = self.addon.status() == Addon.Status.UPDATE_AVAILABLE
update_info.check_in_progress = False # TODO: Implement the "check in progress" status
if repo.metadata:
@@ -117,25 +119,30 @@ class PackageDetailsController(QtCore.QObject):
self.update_macro_info(repo)
if repo.status() == Addon.Status.UNCHECKED:
- if not self.status_update_thread:
- self.status_update_thread = QtCore.QThread()
- self.status_create_addon_list_worker = CheckSingleUpdateWorker(repo)
- self.status_create_addon_list_worker.moveToThread(self.status_update_thread)
- self.status_update_thread.finished.connect(
- self.status_create_addon_list_worker.deleteLater
+ self.ui.button_bar.check_for_update.show()
+ self.ui.button_bar.check_for_update.setText(
+ translate("AddonsInstaller", "Check for " "update")
)
- self.check_for_update.connect(self.status_create_addon_list_worker.do_work)
- self.status_create_addon_list_worker.update_status.connect(self.display_repo_status)
- self.status_update_thread.start()
- update_info.check_in_progress = True
- self.ui.set_update_available(update_info)
- self.check_for_update.emit(self.addon)
+ self.ui.button_bar.check_for_update.setEnabled(True)
+ if not self.update_check_thread:
+ self.update_check_thread = QtCore.QThread()
+ self.check_for_update_worker = CheckSingleUpdateWorker(repo)
+ self.check_for_update_worker.moveToThread(self.update_check_thread)
+ self.update_check_thread.finished.connect(self.check_for_update_worker.deleteLater)
+ self.ui.button_bar.check_for_update.clicked.connect(
+ self.check_for_update_worker.do_work
+ )
+ self.check_for_update_worker.update_status.connect(self.display_repo_status)
+ self.update_check_thread.start()
+ else:
+ self.ui.button_bar.check_for_update.hide()
flags = WarningFlags()
flags.required_freecad_version = self.requires_newer_freecad()
flags.obsolete = repo.obsolete
flags.python2 = repo.python2
self.ui.set_warning_flags(flags)
+ self.set_change_branch_button_state()
def requires_newer_freecad(self) -> Optional[Version]:
"""If the current package is not installed, returns the first supported version of
@@ -159,7 +166,7 @@ class PackageDetailsController(QtCore.QObject):
"""The change branch button is only available for installed Addons that have a .git directory
and in runs where the git is available."""
- self.ui.button_bar.change_branch_button.hide()
+ self.ui.button_bar.change_branch.hide()
pref = fci.ParamGet("User parameter:BaseApp/Preferences/Addons")
show_switcher = pref.GetBool("ShowBranchSwitcher", False)
@@ -186,7 +193,7 @@ class PackageDetailsController(QtCore.QObject):
# If all four above checks passed, then it's possible for us to switch
# branches, if there are any besides the one we are on: show the button
- self.ui.button_bar.change_branch_button.show()
+ self.ui.button_bar.change_branch.show()
def update_macro_info(self, repo: Addon) -> None:
if not repo.macro.url:
@@ -256,3 +263,7 @@ class PackageDetailsController(QtCore.QObject):
self.addon.set_status(Addon.Status.PENDING_RESTART)
self.ui.set_new_branch(name)
self.update_status.emit(self.addon)
+
+ def display_repo_status(self, addon):
+ self.update_status.emit(self.addon)
+ self.show_repo(self.addon)
diff --git a/src/Mod/AddonManager/addonmanager_preferences_defaults.json b/src/Mod/AddonManager/addonmanager_preferences_defaults.json
index c785c154a6..783d699b2b 100644
--- a/src/Mod/AddonManager/addonmanager_preferences_defaults.json
+++ b/src/Mod/AddonManager/addonmanager_preferences_defaults.json
@@ -5,6 +5,7 @@
"AddonsStatsURL": "https://freecad.org/addon_stats.json",
"AutoCheck": false,
"BlockedMacros": "BOLTS,WorkFeatures,how to install,documentation,PartsLibrary,FCGear",
+ "CompositeSplitterState": "",
"CustomRepoHash": "",
"CustomRepositories": "",
"CustomToolbarName": "Auto-Created Macro Toolbar",
diff --git a/src/Mod/AddonManager/composite_view.py b/src/Mod/AddonManager/composite_view.py
index b89041adad..bba9dff9ba 100644
--- a/src/Mod/AddonManager/composite_view.py
+++ b/src/Mod/AddonManager/composite_view.py
@@ -23,7 +23,15 @@
""" Provides a class for showing the list view and detail view at the same time. """
-import addonmanager_freecad_interface
+import base64
+
+from addonmanager_freecad_interface import Preferences
+
+from Addon import Addon
+from Widgets.addonmanager_widget_package_details_view import PackageDetailsView
+from addonmanager_package_details_controller import PackageDetailsController
+from Widgets.addonmanager_widget_view_selector import AddonManagerDisplayStyle
+from package_list import PackageList
# Get whatever version of PySide we can
try:
@@ -43,14 +51,110 @@ 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."""
+ view. Depending on the view mode selected, these may all be displayed at once, or selecting
+ an addon in the list may case the list to hide and the detail view to show."""
+
+ install = QtCore.Signal(Addon)
+ uninstall = QtCore.Signal(Addon)
+ update = QtCore.Signal(Addon)
+ execute = QtCore.Signal(Addon)
+ update_status = QtCore.Signal(Addon)
+ check_for_update = QtCore.Signal(Addon)
def __init__(self, parent=None):
super().__init__(parent)
+ self.package_details = PackageDetailsView(self)
+ self.package_details_controller = PackageDetailsController(self.package_details)
+ self.package_list = PackageList(self)
+ prefs = Preferences()
+ self.display_style = prefs.get("ViewStyle")
+ self.main_layout = QtWidgets.QHBoxLayout(self)
+ self.splitter = QtWidgets.QSplitter(self)
+ self.splitter.addWidget(self.package_list)
+ self.package_list.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
+ self.splitter.addWidget(self.package_details)
+ self.package_details.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
+ self.splitter.setOrientation(QtCore.Qt.Horizontal)
+ self.splitter.setContentsMargins(0, 0, 0, 0)
+ self.splitter.setSizePolicy(
+ QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding
+ )
+ self.main_layout.addWidget(self.splitter)
+ self.layout().setContentsMargins(0, 0, 0, 0)
+ self._setup_ui()
+ self._setup_connections()
+ self._restore_splitter_state()
- # 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
+ def _save_splitter_state(self):
+ """Write the splitter state into an Addon manager preference, CompositeSplitterState"""
+ prefs = Preferences()
+ state = self.splitter.saveState()
+ encoded = base64.b64encode(state).decode("ASCII")
+ prefs.set("CompositeSplitterState", encoded)
+
+ def _restore_splitter_state(self):
+ """Restore the splitter state from CompositeSplitterState"""
+ prefs = Preferences()
+ encoded = prefs.get("CompositeSplitterState")
+ if encoded:
+ state = base64.b64decode(encoded)
+ self.splitter.restoreState(state)
+
+ def setModel(self, model):
+ self.package_list.setModel(model)
+
+ def set_display_style(self, style: AddonManagerDisplayStyle):
+ self.display_style = style
+ self._setup_ui()
+
+ def _setup_ui(self):
+ if self.display_style == AddonManagerDisplayStyle.EXPANDED:
+ self._setup_expanded_ui()
+ elif self.display_style == AddonManagerDisplayStyle.COMPACT:
+ self._setup_compact_ui()
+ elif self.display_style == AddonManagerDisplayStyle.COMPOSITE:
+ self._setup_composite_ui()
+ else:
+ raise RuntimeError("Invalid display style")
+ self.package_list.set_view_style(self.display_style)
+
+ def _setup_expanded_ui(self):
+ self.package_list.show()
+ self.package_details.hide()
+ self.package_details.button_bar.set_show_back_button(True)
+
+ def _setup_compact_ui(self):
+ self.package_list.show()
+ self.package_details.hide()
+ self.package_details.button_bar.set_show_back_button(True)
+
+ def _setup_composite_ui(self):
+ self.package_list.show()
+ self.package_details.show()
+ self.package_details.button_bar.set_show_back_button(False)
+
+ def _setup_connections(self):
+ self.package_list.itemSelected.connect(self.addon_selected)
+ self.package_details_controller.back.connect(self._back_button_clicked)
+ self.package_details_controller.install.connect(self.install)
+ self.package_details_controller.uninstall.connect(self.uninstall)
+ self.package_details_controller.update.connect(self.update)
+ self.package_details_controller.execute.connect(self.execute)
+ self.package_details_controller.update_status.connect(self.update_status)
+ self.package_list.ui.view_bar.view_changed.connect(self.set_display_style)
+ self.splitter.splitterMoved.connect(self._splitter_moved)
+
+ def addon_selected(self, addon):
+ self.package_details_controller.show_repo(addon)
+ if self.display_style != AddonManagerDisplayStyle.COMPOSITE:
+ self.package_list.hide()
+ self.package_details.show()
+ self.package_details.button_bar.set_show_back_button(True)
+
+ def _back_button_clicked(self):
+ if self.display_style != AddonManagerDisplayStyle.COMPOSITE:
+ self.package_list.show()
+ self.package_details.hide()
+
+ def _splitter_moved(self, position: int, index: int) -> None:
+ self._save_splitter_state()
diff --git a/src/Mod/AddonManager/package_list.py b/src/Mod/AddonManager/package_list.py
index 78c7086239..c7a2787aab 100644
--- a/src/Mod/AddonManager/package_list.py
+++ b/src/Mod/AddonManager/package_list.py
@@ -65,7 +65,6 @@ class PackageList(QtWidgets.QWidget):
self.ui.listPackages.setItemDelegate(self.item_delegate)
self.ui.listPackages.clicked.connect(self.on_listPackages_clicked)
- self.ui.view_bar.view_changed.connect(self.set_view_style)
self.ui.view_bar.filter_changed.connect(self.update_status_filter)
self.ui.view_bar.search_changed.connect(self.item_filter.setFilterRegularExpression)
self.ui.view_bar.sort_changed.connect(self.item_filter.setSortRole)
@@ -105,6 +104,20 @@ class PackageList(QtWidgets.QWidget):
)
self.item_filter.setHideUnlicensed(pref.GetBool("HideUnlicensed", False))
+ def select_addon(self, addon_name: str):
+ for index, addon in enumerate(self.item_model.repos):
+ if addon.name == addon_name:
+ row_index = self.item_model.createIndex(index, 0)
+ if self.item_filter.filterAcceptsRow(index):
+ self.ui.listPackages.setCurrentIndex(row_index)
+ else:
+ FreeCAD.Console.PrintLog(
+ f"Addon {addon_name} is not visible given current "
+ "filter: not selecting it."
+ )
+ return
+ FreeCAD.Console.PrintLog(f"Could not find addon '{addon_name}' to select it")
+
def on_listPackages_clicked(self, index: QtCore.QModelIndex):
"""Determine what addon was selected and emit the itemSelected signal with it as
an argument."""
@@ -124,10 +137,10 @@ class PackageList(QtWidgets.QWidget):
def set_view_style(self, style: AddonManagerDisplayStyle) -> None:
"""Set the style (compact or expanded) of the list"""
- self.item_model.layoutAboutToBeChanged.emit()
+ if self.item_model:
+ self.item_model.layoutAboutToBeChanged.emit()
self.item_delegate.set_view(style)
- # TODO: Update to support composite
- if style == AddonManagerDisplayStyle.COMPACT:
+ if style == AddonManagerDisplayStyle.COMPACT or style == AddonManagerDisplayStyle.COMPOSITE:
self.ui.listPackages.setSpacing(2)
self.ui.listPackages.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerItem)
self.ui.listPackages.verticalScrollBar().setSingleStep(-1)
@@ -135,7 +148,8 @@ class PackageList(QtWidgets.QWidget):
self.ui.listPackages.setSpacing(5)
self.ui.listPackages.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
self.ui.listPackages.verticalScrollBar().setSingleStep(24)
- self.item_model.layoutChanged.emit()
+ if self.item_model:
+ self.item_model.layoutChanged.emit()
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
pref.SetInt("ViewStyle", style)
@@ -288,6 +302,9 @@ class PackageListItemDelegate(QtWidgets.QStyledItemDelegate):
elif self.displayStyle == AddonManagerDisplayStyle.COMPACT:
self.widget = self.compact
self._setup_compact_view(repo)
+ elif self.displayStyle == AddonManagerDisplayStyle.COMPOSITE:
+ self.widget = self.compact # For now re-use the compact list
+ self._setup_composite_view(repo)
self.widget.adjustSize()
def _setup_expanded_view(self, addon: Addon) -> None:
@@ -334,6 +351,22 @@ class PackageListItemDelegate(QtWidgets.QStyledItemDelegate):
else:
self.widget.ui.labelDescription.setText(self._get_sort_label_text(addon))
+ def _setup_composite_view(self, addon: Addon) -> None:
+ self.widget.ui.labelPackageName.setText(f"{addon.display_name}")
+ self.widget.ui.labelIcon.setPixmap(addon.icon.pixmap(QtCore.QSize(16, 16)))
+ self.widget.ui.labelStatus.setText(self.get_compact_update_string(addon))
+ self.widget.ui.labelIcon.setText("")
+ if addon.metadata:
+ self.widget.ui.labelVersion.setText(f"v{addon.metadata.version}")
+ elif addon.macro:
+ self._set_macro_version_label(addon)
+ else:
+ self.widget.ui.labelVersion.setText("")
+ if self.sort_order != SortOptions.Alphabetical:
+ self.widget.ui.labelDescription.setText(self._get_sort_label_text(addon))
+ else:
+ self.widget.ui.labelDescription.setText("")
+
def _set_package_maintainer_label(self, addon: Addon):
maintainers = addon.metadata.maintainer
maintainers_string = ""
@@ -395,14 +428,13 @@ class PackageListItemDelegate(QtWidgets.QStyledItemDelegate):
return ""
def _get_compact_description(self, addon: Addon) -> str:
+ description = ""
if addon.metadata:
- trimmed_text = addon.metadata.description
- # TODO: Un-hardcode the 25 character limiter
- return trimmed_text.replace("\r\n", " ")[:25] + "..."
- if addon.macro and addon.macro.comment:
- trimmed_text = addon.macro.comment
- return trimmed_text.replace("\r\n", " ")[:25] + "..."
- return ""
+ description = addon.metadata.description
+ elif addon.macro and addon.macro.comment:
+ description = addon.macro.comment
+ trimmed_text, _, _ = description.partition(".")
+ return trimmed_text.replace("\n", " ")
@staticmethod
def get_compact_update_string(repo: Addon) -> str:
diff --git a/src/Mod/Draft/draftguitools/gui_annotationstyleeditor.py b/src/Mod/Draft/draftguitools/gui_annotationstyleeditor.py
index 012f12badc..d0545687b7 100644
--- a/src/Mod/Draft/draftguitools/gui_annotationstyleeditor.py
+++ b/src/Mod/Draft/draftguitools/gui_annotationstyleeditor.py
@@ -212,20 +212,20 @@ class AnnotationStyleEditor(gui_base.GuiCommandSimplest):
elif index == 1:
# Add new... entry
reply = QtWidgets.QInputDialog.getText(None,
- "Create new style",
- "Style name:")
+ translate("draft", "Create new style"),
+ translate("draft", "Style name:"))
if reply[1]:
# OK or Enter pressed
name = reply[0].strip()
if name == "":
QtWidgets.QMessageBox.information(None,
- "Style name required",
- "No style name specified")
+ translate("draft", "Style name required"),
+ translate("draft", "No style name specified"))
self.form.comboBoxStyles.setCurrentIndex(0)
elif name in self.styles:
QtWidgets.QMessageBox.information(None,
- "Style exists",
- "This style name already exists")
+ translate("draft", "Style exists"),
+ translate("draft", "This style name already exists"))
self.form.comboBoxStyles.setCurrentIndex(0)
else:
# create new style from current editor values
@@ -253,10 +253,10 @@ class AnnotationStyleEditor(gui_base.GuiCommandSimplest):
if self.get_style_users(style):
reply = QtWidgets.QMessageBox.question(None,
- "Style in use",
- "This style is used by some objects in this document. Are you sure?",
- QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
- QtWidgets.QMessageBox.No)
+ translate("draft", "Style in use"),
+ translate("draft", "This style is used by some objects in this document. Are you sure?"),
+ QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
+ QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.No:
return
self.form.comboBoxStyles.removeItem(index)
@@ -268,8 +268,8 @@ class AnnotationStyleEditor(gui_base.GuiCommandSimplest):
style = self.form.comboBoxStyles.itemText(index)
reply = QtWidgets.QInputDialog.getText(None,
- "Rename style",
- "New name:",
+ translate("draft", "Rename style"),
+ translate("draft", "New name:"),
QtWidgets.QLineEdit.Normal,
style)
if reply[1]:
@@ -277,8 +277,8 @@ class AnnotationStyleEditor(gui_base.GuiCommandSimplest):
newname = reply[0]
if newname in self.styles:
reply = QtWidgets.QMessageBox.information(None,
- "Style exists",
- "This style name already exists")
+ translate("draft", "Style exists"),
+ translate("draft", "This style name already exists"))
else:
self.form.comboBoxStyles.setItemText(index, newname)
value = self.styles[style]
diff --git a/src/Mod/Draft/draftutils/params.py b/src/Mod/Draft/draftutils/params.py
index 8839452c7f..c46dbac31f 100644
--- a/src/Mod/Draft/draftutils/params.py
+++ b/src/Mod/Draft/draftutils/params.py
@@ -393,6 +393,7 @@ def _get_param_dictionary():
# Arch parameters that are not in the preferences:
param_dict["Mod/Arch"] = {
+ "applyConstructionStyle": ("bool", True),
"ClaimHosted": ("bool", True),
"CustomIfcSchema": ("string", ""), # importIFClegacy.py
"createIfcGroups": ("bool", False), # importIFClegacy.py
diff --git a/src/Mod/Part/App/FaceMakerBullseye.cpp b/src/Mod/Part/App/FaceMakerBullseye.cpp
index 1937ae69d8..72ac281225 100644
--- a/src/Mod/Part/App/FaceMakerBullseye.cpp
+++ b/src/Mod/Part/App/FaceMakerBullseye.cpp
@@ -71,7 +71,7 @@ void FaceMakerBullseye::Build_Essence()
//validity check
for (TopoDS_Wire& w : myWires) {
if (!BRep_Tool::IsClosed(w))
- throw Base::ValueError("Wire is not closed.");
+ throw Base::ValueError(QT_TRANSLATE_NOOP("Exception", "Wire is not closed."));
}
diff --git a/src/Mod/Path/Path/Base/Gui/GetPoint.py b/src/Mod/Path/Path/Base/Gui/GetPoint.py
index 9a2a94bd35..2e4c4f917e 100644
--- a/src/Mod/Path/Path/Base/Gui/GetPoint.py
+++ b/src/Mod/Path/Path/Base/Gui/GetPoint.py
@@ -206,7 +206,7 @@ class TaskPanel:
if cleanup:
self.removeGlobalCallbacks()
- FreeCADGui.Snapper.off(True)
+ FreeCADGui.Snapper.off()
if self.buttonBox:
self.buttonBox.setEnabled(True)
self.removeEscapeShortcut()
diff --git a/src/Mod/Path/Path/Op/Area.py b/src/Mod/Path/Path/Op/Area.py
index 5a3756cacd..c887839b64 100644
--- a/src/Mod/Path/Path/Op/Area.py
+++ b/src/Mod/Path/Path/Op/Area.py
@@ -272,6 +272,10 @@ class ObjectOp(PathOp.ObjectOp):
# Note that emitting preambles between moves breaks some dressups and prevents path optimization on some controllers
pathParams["preamble"] = False
+ # disable path sorting for offset and zigzag-offset paths
+ if hasattr(obj, "OffsetPattern") and obj.OffsetPattern in ["ZigZagOffset", "Offset"] and hasattr(obj, "MinTravel") and not obj.MinTravel:
+ pathParams["sort_mode"] = 0
+
if not self.areaOpRetractTool(obj):
pathParams["threshold"] = 2.001 * self.radius
diff --git a/src/Mod/Path/Path/Op/Gui/PocketBase.py b/src/Mod/Path/Path/Op/Gui/PocketBase.py
index b45410242f..f6ebd7635d 100644
--- a/src/Mod/Path/Path/Op/Gui/PocketBase.py
+++ b/src/Mod/Path/Path/Op/Gui/PocketBase.py
@@ -93,19 +93,10 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
if not (FeatureRestMachining & self.pocketFeatures()):
form.useRestMachining.hide()
- # if True:
- # # currently doesn't have an effect or is experimental
- # form.minTravel.hide()
return form
def updateMinTravel(self, obj, setModel=True):
- if obj.UseStartPoint:
- self.form.minTravel.setEnabled(True)
- else:
- self.form.minTravel.setChecked(False)
- self.form.minTravel.setEnabled(False)
-
if setModel and obj.MinTravel != self.form.minTravel.isChecked():
obj.MinTravel = self.form.minTravel.isChecked()
diff --git a/src/Mod/Path/libarea/Area.cpp b/src/Mod/Path/libarea/Area.cpp
index 4346eb18a0..3b0f12b8c3 100644
--- a/src/Mod/Path/libarea/Area.cpp
+++ b/src/Mod/Path/libarea/Area.cpp
@@ -624,11 +624,108 @@ void CArea::MakePocketToolpath(std::list &curve_list, const CAreaPocketP
if(params.mode == SingleOffsetPocketMode || params.mode == ZigZagThenSingleOffsetPocketMode)
{
+ // if there are already curves, attempt to start the offset from the current tool position
+ bool done = false;
+ if (!curve_list.empty() && !curve_list.back().m_vertices.empty()) {
+ // find the closest curve to the start point
+ const Point start = curve_list.back().m_vertices.back().m_p;
+ auto curve_itmin = a_offset.m_curves.begin();
+ double dmin = Point::tolerance;
+ for (auto it = a_offset.m_curves.begin(); it != a_offset.m_curves.end(); it++) {
+ const double dist = it->NearestPoint(start).dist(start);
+ if (dist < dmin) {
+ dmin = dist;
+ curve_itmin = it;
+ }
+ }
+
+ // if the start point is on that curve (within Point::tolerance), do the profile starting on that curve
+ if (dmin < Point::tolerance) {
+ // split the curve into two parts -- starting with this point, and ending with this point
+ CCurve startCurve;
+ CCurve endCurve;
+
+ std::list spans;
+ curve_itmin->GetSpans(spans);
+ int imin = -1;
+ double dmin = std::numeric_limits::max();
+ Point nmin;
+ Span smin;
+ {
+ int i = 0;
+ for (auto it = spans.begin(); it != spans.end(); i++, it++) {
+ const Point nearest = it->NearestPoint(start);
+ const double dist = nearest.dist(start);
+ if (dist < dmin) {
+ dmin = dist;
+ imin = i;
+ nmin = nearest;
+ smin = *it;
+ }
+ }
+ }
+
+ startCurve.append(CVertex(nmin));
+ endCurve.append(curve_itmin->m_vertices.front());
+ {
+ int i =0;
+ for (auto it = spans.begin(); it != spans.end(); i++, it++) {
+ if (i < imin) {
+ endCurve.append(it->m_v);
+ } else if (i > imin) {
+ startCurve.append(it->m_v);
+ } else {
+ if (nmin != endCurve.m_vertices.back().m_p) {
+ endCurve.append(CVertex(smin.m_v.m_type, nmin, smin.m_v.m_c, smin.m_v.m_user_data));
+ }
+ if (nmin != it->m_v.m_p) {
+ startCurve.append(CVertex(smin.m_v.m_type, it->m_v.m_p, smin.m_v.m_c, smin.m_v.m_user_data));
+ }
+ }
+ }
+ }
+
+ // append curves to the curve list: start curve, other curves wrapping around, end curve
+ const auto appendCurve = [&curve_list](const CCurve &curve) {
+ if (curve_list.size() > 0 && curve_list.back().m_vertices.back().m_p == curve.m_vertices.front().m_p) {
+ auto it = curve.m_vertices.begin();
+ for (it++; it != curve.m_vertices.end(); it++) {
+ curve_list.back().append(*it);
+ }
+ } else {
+ curve_list.push_back(curve);
+ }
+ };
+
+ if (startCurve.m_vertices.size() > 1) {
+ appendCurve(startCurve);
+ }
+ {
+ auto it = curve_itmin;
+ for(it++; it != a_offset.m_curves.end(); it++) {
+ appendCurve(*it);
+ }
+ }
+ for(auto it = a_offset.m_curves.begin(); it != curve_itmin; it++) {
+ appendCurve(*it);
+ }
+ if (endCurve.m_vertices.size() > 1) {
+ appendCurve(endCurve);
+ }
+
+
+ done = true;
+ }
+ }
+
// add the single offset too
- for(std::list::iterator It = a_offset.m_curves.begin(); It != a_offset.m_curves.end(); It++)
+ if (!done)
{
- CCurve& curve = *It;
- curve_list.push_back(curve);
+ for(std::list::iterator It = a_offset.m_curves.begin(); It != a_offset.m_curves.end(); It++)
+ {
+ CCurve& curve = *It;
+ curve_list.push_back(curve);
+ }
}
}
}