Merge branch 'main' into main

This commit is contained in:
HowThatWorks
2024-03-04 16:54:14 +01:00
committed by GitHub
20 changed files with 365 additions and 147 deletions

View File

@@ -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);

View File

@@ -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))

View File

@@ -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

View File

@@ -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

View File

@@ -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")))

View File

@@ -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(

View File

@@ -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

View File

@@ -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)

View File

@@ -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:

View File

@@ -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)

View File

@@ -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",

View File

@@ -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()

View File

@@ -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"<b>{addon.display_name}</b>")
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"<i>v{addon.metadata.version}</i>")
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:

View File

@@ -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]

View File

@@ -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

View File

@@ -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."));
}

View File

@@ -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()

View File

@@ -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

View File

@@ -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()

View File

@@ -624,11 +624,108 @@ void CArea::MakePocketToolpath(std::list<CCurve> &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<Span> spans;
curve_itmin->GetSpans(spans);
int imin = -1;
double dmin = std::numeric_limits<double>::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<CCurve>::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<CCurve>::iterator It = a_offset.m_curves.begin(); It != a_offset.m_curves.end(); It++)
{
CCurve& curve = *It;
curve_list.push_back(curve);
}
}
}
}