Addon Manager: Auto-create toolbar button
When installing a macro, prompt user to install a toolbar button automatically. Fills in the details of the button using the macro's metadata, including an icon if the __icon__ metadata variable points to a file. Also: * Support XPM data for macro icon * Support online icons * Fix bug in macro uninstall * Cleaned up macro code
This commit is contained in:
@@ -41,6 +41,10 @@ import AddonManager_rc
|
||||
from package_list import PackageList, PackageListItemModel
|
||||
from package_details import PackageDetails
|
||||
from AddonManagerRepo import AddonManagerRepo
|
||||
from install_to_toolbar import (
|
||||
ask_to_install_toolbar_button,
|
||||
remove_custom_toolbar_button,
|
||||
)
|
||||
|
||||
from NetworkManager import HAVE_QTNETWORK, InitializeNetworkManager
|
||||
|
||||
@@ -839,8 +843,27 @@ class CommandAddonManager:
|
||||
path += "_workbench_icon.svg"
|
||||
default_icon = QtGui.QIcon(":/icons/document-package.svg")
|
||||
elif repo.repo_type == AddonManagerRepo.RepoType.MACRO:
|
||||
path += "_macro_icon.svg"
|
||||
default_icon = QtGui.QIcon(":/icons/document-python.svg")
|
||||
if repo.macro and repo.macro.icon:
|
||||
if os.path.isabs(repo.macro.icon):
|
||||
path = repo.macro.icon
|
||||
default_icon = QtGui.QIcon(":/icons/document-python.svg")
|
||||
else:
|
||||
path = os.path.join(
|
||||
os.path.dirname(repo.macro.src_filename), repo.macro.icon
|
||||
)
|
||||
default_icon = QtGui.QIcon(":/icons/document-python.svg")
|
||||
elif repo.macro and repo.macro.xpm:
|
||||
cache_path = FreeCAD.getUserCachePath()
|
||||
am_path = os.path.join(cache_path, "AddonManager", "MacroIcons")
|
||||
os.makedirs(am_path, exist_ok=True)
|
||||
path = os.path.join(am_path, repo.name + "_icon.xpm")
|
||||
if not os.path.exists(path):
|
||||
with open(path, "w") as f:
|
||||
f.write(repo.macro.xpm)
|
||||
default_icon = QtGui.QIcon(repo.macro.xpm)
|
||||
else:
|
||||
path += "_macro_icon.svg"
|
||||
default_icon = QtGui.QIcon(":/icons/document-python.svg")
|
||||
elif repo.repo_type == AddonManagerRepo.RepoType.PACKAGE:
|
||||
# The cache might not have been downloaded yet, check to see if it's there...
|
||||
if os.path.isfile(repo.get_cached_icon_filename()):
|
||||
@@ -1399,6 +1422,8 @@ class CommandAddonManager:
|
||||
repo.set_status(AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE)
|
||||
self.item_model.reload_item(repo)
|
||||
self.packageDetails.show_repo(repo)
|
||||
if repo.repo_type == AddonManagerRepo.RepoType.MACRO:
|
||||
ask_to_install_toolbar_button(repo)
|
||||
|
||||
def on_installation_failed(self, _: AddonManagerRepo, message: str) -> None:
|
||||
self.hide_progress_widgets()
|
||||
@@ -1526,13 +1551,24 @@ class CommandAddonManager:
|
||||
elif repo.repo_type == AddonManagerRepo.RepoType.MACRO:
|
||||
macro = repo.macro
|
||||
if macro.remove():
|
||||
remove_custom_toolbar_button(repo)
|
||||
FreeCAD.Console.PrintMessage(
|
||||
translate("AddonsInstaller", "Successfully uninstalled {}").format(
|
||||
repo.name
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
self.item_model.update_item_status(
|
||||
repo.name, AddonManagerRepo.UpdateStatus.NOT_INSTALLED
|
||||
)
|
||||
self.packageDetails.show_repo(repo)
|
||||
else:
|
||||
self.dialog.textBrowserReadMe.setText(
|
||||
translate("AddonsInstaller", "Macro could not be removed.")
|
||||
FreeCAD.Console.PrintMessage(
|
||||
translate(
|
||||
"AddonsInstaller",
|
||||
"Failed to uninstall {}. Please remove manually.",
|
||||
).format(repo.name)
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -3,27 +3,30 @@ IF (BUILD_GUI)
|
||||
ENDIF (BUILD_GUI)
|
||||
|
||||
SET(AddonManager_SRCS
|
||||
Init.py
|
||||
InitGui.py
|
||||
add_toolbar_button_dialog.ui
|
||||
AddonManager.py
|
||||
AddonManagerRepo.py
|
||||
AddonManager.ui
|
||||
addonmanager_macro.py
|
||||
addonmanager_utilities.py
|
||||
addonmanager_workers.py
|
||||
AddonManager.ui
|
||||
AddonManagerOptions.ui
|
||||
AddonManagerRepo.py
|
||||
ALLOWED_PYTHON_PACKAGES.txt
|
||||
change_branch.py
|
||||
change_branch.ui
|
||||
first_run.ui
|
||||
compact_view.py
|
||||
dependency_resolution_dialog.ui
|
||||
expanded_view.py
|
||||
NetworkManager.py
|
||||
package_list.py
|
||||
package_details.py
|
||||
first_run.ui
|
||||
Init.py
|
||||
InitGui.py
|
||||
install_to_toolbar.py
|
||||
loading.html
|
||||
NetworkManager.py
|
||||
package_details.py
|
||||
package_list.py
|
||||
TestAddonManagerApp.py
|
||||
select_toolbar_dialog.ui
|
||||
)
|
||||
IF (BUILD_GUI)
|
||||
LIST(APPEND AddonManager_SRCS TestAddonManagerGui.py)
|
||||
|
||||
105
src/Mod/AddonManager/add_toolbar_button_dialog.ui
Normal file
105
src/Mod/AddonManager/add_toolbar_button_dialog.ui
Normal file
@@ -0,0 +1,105 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>add_toolbar_button_dialog</class>
|
||||
<widget class="QDialog" name="add_toolbar_button_dialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>257</width>
|
||||
<height>62</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Add button?</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Add a toolbar button for this macro?</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonYes">
|
||||
<property name="text">
|
||||
<string>Yes</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonNo">
|
||||
<property name="text">
|
||||
<string>No</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonNever">
|
||||
<property name="text">
|
||||
<string>Never</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonYes</sender>
|
||||
<signal>clicked()</signal>
|
||||
<receiver>add_toolbar_button_dialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>47</x>
|
||||
<y>40</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>128</x>
|
||||
<y>30</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonNo</sender>
|
||||
<signal>clicked()</signal>
|
||||
<receiver>add_toolbar_button_dialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>128</x>
|
||||
<y>40</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>128</x>
|
||||
<y>30</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonNever</sender>
|
||||
<signal>clicked()</signal>
|
||||
<receiver>add_toolbar_button_dialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>209</x>
|
||||
<y>40</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>128</x>
|
||||
<y>30</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
@@ -28,11 +28,11 @@ import codecs
|
||||
import shutil
|
||||
import time
|
||||
from urllib.parse import urlparse
|
||||
import tempfile
|
||||
from typing import Dict, Tuple, List, Union
|
||||
|
||||
import FreeCAD
|
||||
import NetworkManager
|
||||
from PySide2 import QtCore
|
||||
|
||||
translate = FreeCAD.Qt.translate
|
||||
|
||||
@@ -68,6 +68,7 @@ class Macro(object):
|
||||
self.src_filename = ""
|
||||
self.author = ""
|
||||
self.icon = ""
|
||||
self.xpm = "" # Possible alternate icon data
|
||||
self.other_files = []
|
||||
self.parsed = False
|
||||
|
||||
@@ -115,9 +116,9 @@ class Macro(object):
|
||||
# __Files__
|
||||
# __Author__
|
||||
# __Date__
|
||||
# __Icon__
|
||||
max_lines_to_search = 200
|
||||
line_counter = 0
|
||||
ic = re.IGNORECASE # Shorten the line for Black
|
||||
|
||||
string_search_mapping = {
|
||||
"__comment__": "comment",
|
||||
@@ -127,23 +128,29 @@ class Macro(object):
|
||||
"__author__": "author",
|
||||
"__date__": "date",
|
||||
"__icon__": "icon",
|
||||
"__xpm__": "xpm",
|
||||
}
|
||||
|
||||
string_search_regex = re.compile(r"\s*(['\"])(.*)\1")
|
||||
f = io.StringIO(code)
|
||||
while f and line_counter < max_lines_to_search:
|
||||
line = f.readline()
|
||||
if not line:
|
||||
break
|
||||
if QtCore.QThread.currentThread().isInterruptionRequested():
|
||||
return
|
||||
line_counter += 1
|
||||
# if not line.startswith("__"):
|
||||
# # Speed things up a bit... this comparison is very cheap
|
||||
# continue
|
||||
if not line.startswith("__"):
|
||||
# Speed things up a bit... this comparison is very cheap
|
||||
continue
|
||||
|
||||
lowercase_line = line.lower()
|
||||
for key, value in string_search_mapping.items():
|
||||
if lowercase_line.startswith(key):
|
||||
_, _, after_equals = line.partition("=")
|
||||
match = re.match(string_search_regex, after_equals)
|
||||
if match:
|
||||
# We do NOT support triple-quoted strings, except for the icon XPM data
|
||||
if match and '"""' not in after_equals:
|
||||
if type(self.__dict__[value]) == str:
|
||||
self.__dict__[value] = match.group(2)
|
||||
elif type(self.__dict__[value]) == list:
|
||||
@@ -177,6 +184,32 @@ class Macro(object):
|
||||
)
|
||||
self.version = str(after_equals).strip()
|
||||
break
|
||||
elif key == "__icon__" or key == "__xpm__":
|
||||
# If this is an icon, it's possible that the icon was actually directly specified
|
||||
# in the file as XPM data. This data **must** be between triple double quotes in
|
||||
# order for the Addon Manager to recognize it.
|
||||
if '"""' in after_equals:
|
||||
_, _, xpm_data = after_equals.partition('"""')
|
||||
while True:
|
||||
line = f.readline()
|
||||
if not line:
|
||||
FreeCAD.Console.PrintError(
|
||||
translate(
|
||||
"AddonsInstaller",
|
||||
"Syntax error while reading {} from macro {}",
|
||||
).format(key, self.name)
|
||||
+ "\n"
|
||||
)
|
||||
break
|
||||
if '"""' in line:
|
||||
last_line, _, _ = line.partition('"""')
|
||||
xpm_data += last_line
|
||||
break
|
||||
else:
|
||||
xpm_data += line
|
||||
self.xpm = xpm_data
|
||||
break
|
||||
|
||||
FreeCAD.Console.PrintError(
|
||||
translate(
|
||||
"AddonsInstaller",
|
||||
@@ -273,6 +306,27 @@ class Macro(object):
|
||||
self.author = self.parse_desc("Author: ")
|
||||
if not self.date:
|
||||
self.date = self.parse_desc("Last modified: ")
|
||||
if self.icon.startswith("http://") or self.icon.startswith("https://"):
|
||||
# Technically we don't claim to support this, but some macro authors are
|
||||
# doing it anyway, so let's give it a shot...
|
||||
FreeCAD.Console.PrintMessage(
|
||||
translate(
|
||||
"AddonsInstaller", "Attempting to fetch macro icon from {}"
|
||||
).format(self.icon)
|
||||
+ "\n"
|
||||
)
|
||||
p = NetworkManager.AM_NETWORK_MANAGER.blocking_get(self.icon)
|
||||
if p:
|
||||
cache_path = FreeCAD.getUserCachePath()
|
||||
am_path = os.path.join(cache_path, "AddonManager", "MacroIcons")
|
||||
os.makedirs(am_path, exist_ok=True)
|
||||
_, _, filename = self.icon.rpartition("/")
|
||||
base, _, extension = filename.rpartition(".")
|
||||
constructed_name = os.path.join(am_path, base + "." + extension)
|
||||
with open(constructed_name, "wb") as f:
|
||||
f.write(p.data())
|
||||
self.icon_source = self.icon
|
||||
self.icon = constructed_name
|
||||
|
||||
def parse_desc(self, line_start: str) -> Union[str, None]:
|
||||
components = self.desc.split(">")
|
||||
@@ -306,15 +360,43 @@ class Macro(object):
|
||||
# self.src_filename.
|
||||
base_dir = os.path.dirname(self.src_filename)
|
||||
warnings = []
|
||||
|
||||
if self.xpm:
|
||||
xpm_file = os.path.join(base_dir, self.name + "_icon.xpm")
|
||||
with open(xpm_file, "w") as f:
|
||||
f.write(self.xpm)
|
||||
if self.icon:
|
||||
if os.path.isabs(self.icon):
|
||||
dst_file = os.path.normpath(
|
||||
os.path.join(macro_dir, os.path.basename(self.icon))
|
||||
)
|
||||
try:
|
||||
shutil.copy(self.icon, dst_file)
|
||||
except IOError:
|
||||
warnings.append(f"Failed to copy icon to {dst_file}")
|
||||
elif self.icon not in self.other_files:
|
||||
self.other_files.append(self.icon)
|
||||
|
||||
for other_file in self.other_files:
|
||||
dst_dir = os.path.join(macro_dir, os.path.dirname(other_file))
|
||||
if not other_file:
|
||||
continue
|
||||
if os.path.isabs(other_file):
|
||||
dst_dir = macro_dir
|
||||
else:
|
||||
dst_dir = os.path.join(macro_dir, os.path.dirname(other_file))
|
||||
if not os.path.isdir(dst_dir):
|
||||
try:
|
||||
os.makedirs(dst_dir)
|
||||
except OSError:
|
||||
return False, [f"Failed to create {dst_dir}"]
|
||||
src_file = os.path.normpath(os.path.join(base_dir, other_file))
|
||||
dst_file = os.path.normpath(os.path.join(macro_dir, other_file))
|
||||
if os.path.isabs(other_file):
|
||||
src_file = other_file
|
||||
dst_file = os.path.normpath(
|
||||
os.path.join(macro_dir, os.path.basename(other_file))
|
||||
)
|
||||
else:
|
||||
src_file = os.path.normpath(os.path.join(base_dir, other_file))
|
||||
dst_file = os.path.normpath(os.path.join(macro_dir, other_file))
|
||||
if not os.path.isfile(src_file):
|
||||
warnings.append(
|
||||
translate(
|
||||
@@ -351,19 +433,35 @@ class Macro(object):
|
||||
os.remove(macro_path_with_macro_prefix)
|
||||
# Remove related files, which are supposed to be given relative to
|
||||
# self.src_filename.
|
||||
if self.xpm:
|
||||
xpm_file = os.path.join(macro_dir, self.name + "_icon.xpm")
|
||||
if os.path.exists(xpm_file):
|
||||
os.remove(xpm_file)
|
||||
for other_file in self.other_files:
|
||||
if not other_file:
|
||||
continue
|
||||
FreeCAD.Console.PrintMessage(f"{other_file}...")
|
||||
dst_file = os.path.join(macro_dir, other_file)
|
||||
if not dst_file or not os.path.exists(dst_file):
|
||||
FreeCAD.Console.PrintMessage(f"X\n")
|
||||
continue
|
||||
try:
|
||||
os.remove(dst_file)
|
||||
remove_directory_if_empty(os.path.dirname(dst_file))
|
||||
FreeCAD.Console.PrintMessage("✓\n")
|
||||
except Exception:
|
||||
FreeCAD.Console.PrintWarning(
|
||||
translate(
|
||||
"AddonsInstaller",
|
||||
"Failed to remove macro file '{}': it might not exist, or its permissions changed",
|
||||
).format(dst_file)
|
||||
+ "\n"
|
||||
)
|
||||
FreeCAD.Console.PrintMessage(f"?\n")
|
||||
if os.path.isabs(self.icon):
|
||||
dst_file = os.path.normpath(
|
||||
os.path.join(macro_dir, os.path.basename(self.icon))
|
||||
)
|
||||
if os.path.exists(dst_file):
|
||||
try:
|
||||
FreeCAD.Console.PrintMessage(f"{os.path.basename(self.icon)}...")
|
||||
os.remove(dst_file)
|
||||
FreeCAD.Console.PrintMessage("✓\n")
|
||||
except Exception:
|
||||
FreeCAD.Console.PrintMessage(f"?\n")
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -528,7 +528,7 @@ class UpdateChecker:
|
||||
macro_wrapper.macro.fill_details_from_file(
|
||||
macro_wrapper.macro.src_filename
|
||||
)
|
||||
if not macro_wrapper.macro.parsed and macro_wrapper.macro.on_wiki:
|
||||
elif not macro_wrapper.macro.parsed and macro_wrapper.macro.on_wiki:
|
||||
mac = macro_wrapper.macro.name.replace(" ", "_")
|
||||
mac = mac.replace("&", "%26")
|
||||
mac = mac.replace("+", "%2B")
|
||||
@@ -647,7 +647,7 @@ class FillMacroListWorker(QtCore.QThread):
|
||||
except Exception as e:
|
||||
FreeCAD.Console.PrintWarning(
|
||||
translate(
|
||||
"AddonsInstaller", "An error occurred fetching macros from GitHub"
|
||||
"AddonsInstaller", "An error occurred updating macros from GitHub"
|
||||
)
|
||||
+ f":\n{e}\n"
|
||||
)
|
||||
|
||||
278
src/Mod/AddonManager/install_to_toolbar.py
Normal file
278
src/Mod/AddonManager/install_to_toolbar.py
Normal file
@@ -0,0 +1,278 @@
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2022 FreeCAD Project Association *
|
||||
# * *
|
||||
# * This program 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. *
|
||||
# * *
|
||||
# * This program 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 this library; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
|
||||
# * 02110-1301 USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
from PySide2 import QtCore, QtWidgets
|
||||
import AddonManagerRepo
|
||||
|
||||
import os
|
||||
|
||||
translate = FreeCAD.Qt.translate
|
||||
|
||||
|
||||
def ask_to_install_toolbar_button(repo: AddonManagerRepo) -> None:
|
||||
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
|
||||
do_not_show_dialog = pref.GetBool("dontShowAddMacroButtonDialog", False)
|
||||
if not do_not_show_dialog:
|
||||
add_toolbar_button_dialog = FreeCADGui.PySideUic.loadUi(
|
||||
os.path.join(os.path.dirname(__file__), "add_toolbar_button_dialog.ui")
|
||||
)
|
||||
add_toolbar_button_dialog.buttonYes.clicked.connect(
|
||||
lambda: install_toolbar_button(repo)
|
||||
)
|
||||
add_toolbar_button_dialog.buttonNever.clicked.connect(
|
||||
lambda: pref.SetBool("dontShowAddMacroButtonDialog", True)
|
||||
)
|
||||
add_toolbar_button_dialog.exec()
|
||||
|
||||
|
||||
def ask_for_toolbar(
|
||||
repo: AddonManagerRepo, custom_toolbars
|
||||
) -> object: # Returns the pref group for the new toolbar
|
||||
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
|
||||
|
||||
# In this one spot, default True: if this is the first time we got to
|
||||
# this chunk of code, we are always going to ask.
|
||||
ask = pref.GetBool("alwaysAskForToolbar", True)
|
||||
|
||||
if ask:
|
||||
select_toolbar_dialog = FreeCADGui.PySideUic.loadUi(
|
||||
os.path.join(os.path.dirname(__file__), "select_toolbar_dialog.ui")
|
||||
)
|
||||
|
||||
select_toolbar_dialog.comboBox.clear()
|
||||
|
||||
for group in custom_toolbars:
|
||||
ref = FreeCAD.ParamGet(
|
||||
"User parameter:BaseApp/Workbench/Global/Toolbar/" + group
|
||||
)
|
||||
name = ref.GetString("Name", "")
|
||||
if name:
|
||||
select_toolbar_dialog.comboBox.addItem(name)
|
||||
else:
|
||||
FreeCAD.Console.PrintWarning(
|
||||
f"Custom toolbar {group} does not have a Name element\n"
|
||||
)
|
||||
new_menubar_option_text = translate("AddonsInstaller", "Create new toolbar")
|
||||
select_toolbar_dialog.comboBox.addItem(new_menubar_option_text)
|
||||
|
||||
result = select_toolbar_dialog.exec()
|
||||
if result == QtWidgets.QDialog.Accepted:
|
||||
selection = select_toolbar_dialog.comboBox.currentText()
|
||||
if select_toolbar_dialog.checkBox.checkState() == QtCore.Qt.Unchecked:
|
||||
pref.SetBool("alwaysAskForToolbar", False)
|
||||
else:
|
||||
pref.SetBool("alwaysAskForToolbar", True)
|
||||
if selection == new_menubar_option_text:
|
||||
return create_new_custom_toolbar()
|
||||
else:
|
||||
return get_toolbar_with_name(selection)
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
custom_toolbar_name = pref.GetString(
|
||||
"CustomToolbarName", "Auto-Created Macro Toolbar"
|
||||
)
|
||||
toolbar = get_toolbar_with_name(custom_toolbar_name)
|
||||
if not toolbar:
|
||||
# They told us not to ask, but then the toolbar got deleted... ask anyway!
|
||||
ask = pref.RemBool("alwaysAskForToolbar")
|
||||
return ask_for_toolbar(repo, custom_toolbars)
|
||||
else:
|
||||
return toolbar
|
||||
|
||||
|
||||
def get_toolbar_with_name(name: str) -> object:
|
||||
top_group = FreeCAD.ParamGet("User parameter:BaseApp/Workbench/Global/Toolbar")
|
||||
custom_toolbars = top_group.GetGroups()
|
||||
for toolbar in custom_toolbars:
|
||||
group = FreeCAD.ParamGet(
|
||||
"User parameter:BaseApp/Workbench/Global/Toolbar/" + toolbar
|
||||
)
|
||||
group_name = group.GetString("Name", "")
|
||||
if group_name == name:
|
||||
return group
|
||||
return None
|
||||
|
||||
|
||||
def create_new_custom_toolbar() -> object:
|
||||
|
||||
# We need two names: the name of the auto-created toolbar, as it will be displayed to the
|
||||
# user in various menus, and the underlying name of the toolbar group. Both must be
|
||||
# unique.
|
||||
|
||||
# First, the displayed name
|
||||
custom_toolbar_name = "Auto-Created Macro Toolbar"
|
||||
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
|
||||
top_group = FreeCAD.ParamGet("User parameter:BaseApp/Workbench/Global/Toolbar")
|
||||
custom_toolbars = top_group.GetGroups()
|
||||
name_taken = check_for_toolbar(custom_toolbars, custom_toolbar_name)
|
||||
if name_taken:
|
||||
i = 2 # Don't use (1), start at (2)
|
||||
while True:
|
||||
test_name = custom_toolbar_name + f" ({i})"
|
||||
if not check_for_toolbar(custom_toolbars, test_name):
|
||||
custom_toolbar_name = test_name
|
||||
i = i + 1
|
||||
|
||||
# Second, the toolbar preference group name
|
||||
i = 1
|
||||
while True:
|
||||
new_group_name = "Custom_" + str(i)
|
||||
if new_group_name not in custom_toolbars:
|
||||
break
|
||||
i = i + 1
|
||||
|
||||
custom_toolbar = FreeCAD.ParamGet(
|
||||
"User parameter:BaseApp/Workbench/Global/Toolbar/" + new_group_name
|
||||
)
|
||||
custom_toolbar.SetString("Name", custom_toolbar_name)
|
||||
custom_toolbar.SetBool("Active", True)
|
||||
return custom_toolbar
|
||||
|
||||
|
||||
def check_for_toolbar(custom_toolbars, toolbar_name: str) -> bool:
|
||||
tb = get_toolbar_with_name(toolbar_name)
|
||||
if tb:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def install_toolbar_button(repo: AddonManagerRepo) -> None:
|
||||
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
|
||||
custom_toolbar_name = pref.GetString(
|
||||
"CustomToolbarName", "Auto-Created Macro Toolbar"
|
||||
)
|
||||
|
||||
# Default to false here: if the variable hasn't been set, we don't assume
|
||||
# that we have to ask, because the simplest is to just create a new toolbar
|
||||
# and never ask at all.
|
||||
ask = pref.GetBool("alwaysAskForToolbar", False)
|
||||
|
||||
# See if there is already a custom toolbar for macros:
|
||||
top_group = FreeCAD.ParamGet("User parameter:BaseApp/Workbench/Global/Toolbar")
|
||||
custom_toolbars = top_group.GetGroups()
|
||||
if custom_toolbars:
|
||||
# If there are already custom toolbars, see if one of them is the one we used last time
|
||||
found_toolbar = False
|
||||
for toolbar_name in custom_toolbars:
|
||||
test_toolbar = FreeCAD.ParamGet(
|
||||
"User parameter:BaseApp/Workbench/Global/Toolbar/" + toolbar_name
|
||||
)
|
||||
name = test_toolbar.GetString("Name", "")
|
||||
if name == custom_toolbar_name:
|
||||
custom_toolbar = test_toolbar
|
||||
found_toolbar = True
|
||||
break
|
||||
if ask or not found_toolbar:
|
||||
# We have to ask the user what to do...
|
||||
custom_toolbar = ask_for_toolbar(repo, custom_toolbars)
|
||||
if custom_toolbar:
|
||||
custom_toolbar_name = custom_toolbar.GetString("Name")
|
||||
pref.SetString("CustomToolbarName", custom_toolbar_name)
|
||||
else:
|
||||
# Create a custom toolbar
|
||||
custom_toolbar = FreeCAD.ParamGet(
|
||||
"User parameter:BaseApp/Workbench/Global/Toolbar/Custom_1"
|
||||
)
|
||||
custom_toolbar.SetString("Name", custom_toolbar_name)
|
||||
custom_toolbar.SetBool("Active", True)
|
||||
|
||||
if custom_toolbar:
|
||||
install_macro_to_toolbar(repo, custom_toolbar)
|
||||
else:
|
||||
FreeCAD.Console.PrintMessage(
|
||||
f"In the end, no custom toolbar was set, bailing out\n"
|
||||
)
|
||||
|
||||
|
||||
def install_macro_to_toolbar(repo: AddonManagerRepo, toolbar: object) -> None:
|
||||
menuText = repo.display_name
|
||||
tooltipText = f"<b>{repo.display_name}</b>"
|
||||
if repo.macro.comment:
|
||||
tooltipText += f"<br/><p>{repo.macro.comment}</p>"
|
||||
whatsThisText = repo.macro.comment
|
||||
else:
|
||||
whatsThisText = translate(
|
||||
"AddonsInstaller", "A macro installed with the FreeCAD Addon Manager"
|
||||
)
|
||||
statustipText = (
|
||||
translate("AddonsInstaller", "Run", "Indicates a macro that can be 'run'")
|
||||
+ " "
|
||||
+ repo.display_name
|
||||
)
|
||||
if repo.macro.icon:
|
||||
if os.path.isabs(repo.macro.icon):
|
||||
pixmapText = os.path.normpath(repo.macro.icon)
|
||||
else:
|
||||
macro_repo_dir = FreeCAD.getUserMacroDir(True)
|
||||
pixmapText = os.path.normpath(os.path.join(macro_repo_dir, repo.macro.icon))
|
||||
elif repo.macro.xpm:
|
||||
macro_repo_dir = FreeCAD.getUserMacroDir(True)
|
||||
icon_file = os.path.normpath(
|
||||
os.path.join(macro_repo_dir, repo.macro.name + "_icon.xpm")
|
||||
)
|
||||
with open(icon_file, "w") as f:
|
||||
f.write(repo.macro.xpm)
|
||||
pixmapText = icon_file
|
||||
else:
|
||||
pixmapText = None
|
||||
|
||||
# Add this command to that toolbar
|
||||
command_name = FreeCADGui.Command.createCustomCommand(
|
||||
repo.macro.filename,
|
||||
menuText,
|
||||
tooltipText,
|
||||
whatsThisText,
|
||||
statustipText,
|
||||
pixmapText,
|
||||
)
|
||||
toolbar.SetString(command_name, "FreeCAD")
|
||||
|
||||
# Force the toolbars to be recreated
|
||||
wb = FreeCADGui.activeWorkbench()
|
||||
wb.reloadActive()
|
||||
|
||||
|
||||
def remove_custom_toolbar_button(repo: AddonManagerRepo) -> None:
|
||||
"""If this repo contains a macro, look through the custom commands and
|
||||
see if one is set up for this macro. If so, remove it, including any
|
||||
toolbar entries."""
|
||||
|
||||
command = FreeCADGui.Command.findCustomCommand(repo.macro.filename)
|
||||
if not command:
|
||||
return
|
||||
custom_toolbars = FreeCAD.ParamGet(
|
||||
"User parameter:BaseApp/Workbench/Global/Toolbar"
|
||||
)
|
||||
toolbar_groups = custom_toolbars.GetGroups()
|
||||
for group in toolbar_groups:
|
||||
toolbar = custom_toolbars.GetGroup(group)
|
||||
if toolbar.GetString(command, "*") != "*":
|
||||
toolbar.RemString(command)
|
||||
|
||||
FreeCADGui.Command.removeCustomCommand(command)
|
||||
|
||||
# Force the toolbars to be recreated
|
||||
wb = FreeCADGui.activeWorkbench()
|
||||
wb.reloadActive()
|
||||
@@ -146,14 +146,10 @@ class PackageDetails(QWidget):
|
||||
self.show_package(repo)
|
||||
self.ui.buttonExecute.hide()
|
||||
|
||||
if self.status_update_thread is not None:
|
||||
if not self.status_update_thread.isFinished():
|
||||
self.status_update_thread.requestInterruption()
|
||||
self.status_update_thread.wait()
|
||||
|
||||
if repo.status() == AddonManagerRepo.UpdateStatus.UNCHECKED:
|
||||
self.status_update_thread = QThread()
|
||||
self.status_update_worker = CheckSingleUpdateWorker(repo, self)
|
||||
if not self.status_update_thread:
|
||||
self.status_update_thread = QThread()
|
||||
self.status_update_worker = CheckSingleUpdateWorker(repo)
|
||||
self.status_update_worker.moveToThread(self.status_update_thread)
|
||||
self.status_update_thread.finished.connect(
|
||||
self.status_update_worker.deleteLater
|
||||
|
||||
87
src/Mod/AddonManager/select_toolbar_dialog.ui
Normal file
87
src/Mod/AddonManager/select_toolbar_dialog.ui
Normal file
@@ -0,0 +1,87 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>select_toolbar_dialog</class>
|
||||
<widget class="QDialog" name="select_toolbar_dialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>308</width>
|
||||
<height>109</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Select Toolbar</string>
|
||||
</property>
|
||||
<property name="sizeGripEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="modal">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Select a toolbar to add this macro to:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="comboBox"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox">
|
||||
<property name="text">
|
||||
<string>Ask every time</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>select_toolbar_dialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>select_toolbar_dialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
56
src/Mod/AddonManager/toolbar_button.ui
Normal file
56
src/Mod/AddonManager/toolbar_button.ui
Normal file
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>toolbar_button</class>
|
||||
<widget class="QDialog" name="toolbar_button">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>257</width>
|
||||
<height>62</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Add button?</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Add a toolbar button for this macro?</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_2">
|
||||
<property name="text">
|
||||
<string>Yes</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_3">
|
||||
<property name="text">
|
||||
<string>No</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_4">
|
||||
<property name="text">
|
||||
<string>Never</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
Reference in New Issue
Block a user