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:
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user