diff --git a/src/Gui/Icons/AddonManager.svg b/src/Gui/Icons/AddonManager.svg
index 2c7537fe7741..f630e855ac17 100644
--- a/src/Gui/Icons/AddonManager.svg
+++ b/src/Gui/Icons/AddonManager.svg
@@ -14,8 +14,8 @@
height="64px"
id="svg3057"
version="1.1"
- inkscape:version="0.48.5 r10040"
- sodipodi:docname="addon-manager.svg">
+ inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
+ sodipodi:docname="AddonManager.svg">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Mod/AddonManager/AddonManager.py b/src/Mod/AddonManager/AddonManager.py
index 9ec93742d7c4..a70bb57f2953 100644
--- a/src/Mod/AddonManager/AddonManager.py
+++ b/src/Mod/AddonManager/AddonManager.py
@@ -33,9 +33,10 @@
FreeCAD Addon Manager Module
It will fetch its contents from https://github.com/FreeCAD/FreeCAD-addons
-You need a working internet connection, and the GitPython package
+You need a working internet connection, and optionally the GitPython package
installed.
'''
+
import os
import re
import shutil
@@ -43,258 +44,130 @@
import sys
import tempfile
-from PySide import QtCore, QtGui
-import FreeCAD
-import FreeCADGui
-if sys.version_info.major < 3:
- import urllib2
-else:
- import urllib.request as urllib2
-
-from addonmanager_macro import Macro
from addonmanager_utilities import translate
-from addonmanager_utilities import urlopen
+from addonmanager_utilities import update_macro_details
+from addonmanager_utilities import install_macro
+from addonmanager_utilities import remove_macro
+from addonmanager_utilities import remove_directory_if_empty
+from addonmanager_utilities import restartFreeCAD
+from addonmanager_workers import *
-NOGIT = False # for debugging purposes, set this to True to always use http downloads
+def QT_TRANSLATE_NOOP(ctx,txt):
+ return txt
-MACROS_BLACKLIST = ["BOLTS","WorkFeatures","how to install","PartsLibrary","FCGear"]
-OBSOLETE = ["assembly2","drawing_dimensioning","cura_engine"] # These addons will print an additional message informing the user
-if sys.version_info.major < 3:
- import StringIO as io
- _stringio = io.StringIO
-else:
- import io
- _stringio = io.BytesIO
+class CommandAddonManager:
+ "The Addon Manager command"
-def symlink(source, link_name):
- if os.path.exists(link_name) or os.path.lexists(link_name):
- #print("macro already exists")
- pass
- else:
- os_symlink = getattr(os, "symlink", None)
- if callable(os_symlink):
- os_symlink(source, link_name)
- else:
- import ctypes
- csl = ctypes.windll.kernel32.CreateSymbolicLinkW
- csl.argtypes = (ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint32)
- csl.restype = ctypes.c_ubyte
- flags = 1 if os.path.isdir(source) else 0
- # set the SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE flag
- # (see https://blogs.windows.com/buildingapps/2016/12/02/symlinks-windows-10/#joC5tFKhdXs2gGml.97)
- flags += 2
- if csl(link_name, source, flags) == 0:
- raise ctypes.WinError()
-
-
-def update_macro_details(old_macro, new_macro):
- """Update a macro with information from another one
-
- Update a macro with information from another one, supposedly the same but
- from a different source. The first source is supposed to be git, the second
- one the wiki.
- """
- if old_macro.on_git and new_macro.on_git:
- FreeCAD.Console.PrintWarning('The macro "{}" is present twice in github, please report'.format(old_macro.name))
- # We don't report macros present twice on the wiki because a link to a
- # macro is considered as a macro. For example, 'Perpendicular To Wire'
- # appears twice, as of 2018-05-05).
- old_macro.on_wiki = new_macro.on_wiki
- for attr in ['desc', 'url', 'code']:
- if not hasattr(old_macro, attr):
- setattr(old_macro, attr, getattr(new_macro, attr))
-
-
-def install_macro(macro, macro_repo_dir):
- """Install a macro and all its related files
-
- Returns True if the macro was installed correctly.
-
- Parameters
- ----------
- - macro: an addonmanager_macro.Macro instance
- """
- if not macro.code:
- return False
- macro_dir = FreeCAD.getUserMacroDir(True)
- if not os.path.isdir(macro_dir):
- try:
- os.makedirs(macro_dir)
- except OSError:
- return False
- macro_path = os.path.join(macro_dir, macro.filename)
- if sys.version_info.major < 3:
- # In python2 the code is a bytes object.
- mode = 'wb'
- else:
- mode = 'w'
- try:
- with open(macro_path, mode) as macrofile:
- macrofile.write(macro.code)
- except IOError:
- return False
- # Copy related files, which are supposed to be given relative to
- # macro.src_filename.
- base_dir = os.path.dirname(macro.src_filename)
- for other_file in macro.other_files:
- 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
- src_file = os.path.join(base_dir, other_file)
- dst_file = os.path.join(macro_dir, other_file)
- try:
- shutil.copy(src_file, dst_file)
- except IOError:
- return False
- return True
-
-
-def remove_macro(macro):
- """Remove a macro and all its related files
-
- Returns True if the macro was removed correctly.
-
- Parameters
- ----------
- - macro: an addonmanager_macro.Macro instance
- """
- if not macro.is_installed():
- # Macro not installed, nothing to do.
- return True
- macro_dir = FreeCAD.getUserMacroDir(True)
- macro_path = os.path.join(macro_dir, macro.filename)
- macro_path_with_macro_prefix = os.path.join(macro_dir, 'Macro_' + macro.filename)
- if os.path.exists(macro_path):
- os.remove(macro_path)
- elif os.path.exists(macro_path_with_macro_prefix):
- os.remove(macro_path_with_macro_prefix)
- # Remove related files, which are supposed to be given relative to
- # macro.src_filename.
- for other_file in macro.other_files:
- dst_file = os.path.join(macro_dir, other_file)
- remove_directory_if_empty(os.path.dirname(dst_file))
- os.remove(dst_file)
- return True
+ def GetResources(self):
+ return {'Pixmap': 'AddonManager',
+ 'MenuText': QT_TRANSLATE_NOOP("Std_AddonMgr", '&Addon manager'),
+ 'ToolTip': QT_TRANSLATE_NOOP("Std_AddonMgr", 'Manage external workbenches and macros'),
+ 'Group': 'Tools'}
-def remove_directory_if_empty(dir):
- """Remove the directory if it is empty
+ def Activated(self):
- Directory FreeCAD.getUserMacroDir(True) will not be removed even if empty.
- """
- if dir == FreeCAD.getUserMacroDir(True):
- return
- if not os.listdir(dir):
- os.rmdir(dir)
+ # display first use dialog if needed
+ from PySide import QtGui
+ readWarning = FreeCAD.ParamGet('User parameter:Plugins/addonsRepository').GetBool('readWarning',False)
+ if not readWarning:
+ if QtGui.QMessageBox.warning(None,"FreeCAD",translate("AddonsInstaller", "The addons that can be installed here are not officially part of FreeCAD, and are not reviewed by the FreeCAD team. Make sure you know what you are installing!"), QtGui.QMessageBox.Cancel | QtGui.QMessageBox.Ok) != QtGui.QMessageBox.StandardButton.Cancel:
+ FreeCAD.ParamGet('User parameter:Plugins/addonsRepository').SetBool('readWarning',True)
+ readWarning = True
-def restartFreeCAD():
+ if readWarning:
+ self.launch()
- "Shuts down and restarts FreeCAD"
+ def launch(self):
- args = QtGui.QApplication.arguments()[1:]
- if FreeCADGui.getMainWindow().close():
- QtCore.QProcess.startDetached(QtGui.QApplication.applicationFilePath(),args)
-
-
-class AddonsInstaller(QtGui.QDialog):
+ import FreeCADGui
+ from PySide import QtGui
+
+ # create the dialog
+ self.dialog = FreeCADGui.PySideUic.loadUi(os.path.join(os.path.dirname(__file__),"AddonManager.ui"))
- def __init__(self):
- QtGui.QDialog.__init__(self)
+ # cleanup the leftovers from previous runs
self.repos = []
self.macros = []
self.macro_repo_dir = tempfile.mkdtemp()
-
- self.setObjectName("AddonsInstaller")
- self.resize(626, 404)
- self.verticalLayout = QtGui.QVBoxLayout(self)
- self.tabWidget = QtGui.QTabWidget()
- self.verticalLayout.addWidget(self.tabWidget)
- self.listWorkbenches = QtGui.QListWidget()
- self.listWorkbenches.setIconSize(QtCore.QSize(16,16))
- self.tabWidget.addTab(self.listWorkbenches,"")
- self.listMacros = QtGui.QListWidget()
- self.listMacros.setSortingEnabled(False)
- self.listMacros.setIconSize(QtCore.QSize(16,16))
- self.tabWidget.addTab(self.listMacros,"")
- self.labelDescription = QtGui.QLabel()
- self.labelDescription.setMinimumSize(QtCore.QSize(0, 75))
- self.labelDescription.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop)
- self.labelDescription.setWordWrap(True)
- self.verticalLayout.addWidget(self.labelDescription)
self.doUpdate = []
+ self.addon_removed = False
+ for worker in ["update_worker","check_worker","show_worker","showmacro_worker","macro_worker","install_worker"]:
+ if hasattr(self,worker):
+ thread = getattr(self,worker)
+ if thread:
+ if thread.isFinished():
+ setattr(self,worker,None)
+ self.dialog.tabWidget.setCurrentIndex(0)
+ # these 2 settings to prevent loading an addon description on start (let the user click something first)
+ self.firsttime = True
+ self.firstmacro = True
+
+ # restore window geometry and splitter state from stored state
+ pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
+ w = pref.GetInt("WindowWidth",600)
+ h = pref.GetInt("WindowHeight",480)
+ self.dialog.resize(w,h)
+ sl = pref.GetInt("SplitterLeft",298)
+ sr = pref.GetInt("SplitterRight",274)
+ self.dialog.splitter.setSizes([sl,sr])
+
+ # set nice icons to everything, by theme with fallback to FreeCAD icons
+ self.dialog.setWindowIcon(QtGui.QIcon(":/icons/AddonManager.svg"))
+ self.dialog.buttonExecute.setIcon(QtGui.QIcon.fromTheme("execute",QtGui.QIcon(":/icons/button_valid.svg")))
+ self.dialog.buttonUninstall.setIcon(QtGui.QIcon.fromTheme("cancel",QtGui.QIcon(":/icons/edit_Cancel.svg")))
+ self.dialog.buttonInstall.setIcon(QtGui.QIcon.fromTheme("download",QtGui.QIcon(":/icons/edit_OK.svg")))
+ self.dialog.buttonUpdateAll.setIcon(QtGui.QIcon(":/icons/button_valid.svg"))
+ self.dialog.buttonConfigure.setIcon(QtGui.QIcon(":/icons/preferences-system.svg"))
+ self.dialog.tabWidget.setTabIcon(0,QtGui.QIcon(":/icons/Group.svg"))
+ self.dialog.tabWidget.setTabIcon(1,QtGui.QIcon(":/icons/applications-python.svg"))
+
+
+ # enable/disable stuff
+ self.dialog.buttonExecute.setEnabled(False)
+ self.dialog.buttonUninstall.setEnabled(False)
+ self.dialog.buttonInstall.setEnabled(False)
+ self.dialog.buttonUpdateAll.setEnabled(False)
+
+ # connect slots
+ self.dialog.buttonExecute.clicked.connect(self.executemacro)
+ self.dialog.rejected.connect(self.reject)
+ self.dialog.buttonInstall.clicked.connect(self.install)
+ self.dialog.buttonUninstall.clicked.connect(self.remove)
+ self.dialog.buttonUpdateAll.clicked.connect(self.apply_updates)
+ self.dialog.listWorkbenches.currentRowChanged.connect(self.show)
+ self.dialog.tabWidget.currentChanged.connect(self.switchtab)
+ self.dialog.listMacros.currentRowChanged.connect(self.show_macro)
+ self.dialog.buttonConfigure.clicked.connect(self.show_config)
+
+ # allow to open links in browser
+ self.dialog.description.setOpenLinks(True)
+ self.dialog.description.setOpenExternalLinks(True)
- self.progressBar = QtGui.QProgressBar(self)
- #self.progressBar.setProperty("value", 24)
- self.progressBar.setObjectName("progressBar")
- #self.progressBar.hide()
- self.progressBar.setRange(0,0)
- self.verticalLayout.addWidget(self.progressBar)
-
- self.horizontalLayout = QtGui.QHBoxLayout()
- spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
- self.horizontalLayout.addItem(spacerItem)
-
- self.buttonExecute = QtGui.QPushButton()
- icon = QtGui.QIcon.fromTheme("execute")
- self.buttonExecute.setIcon(icon)
- self.buttonExecute.setEnabled(False)
- self.horizontalLayout.addWidget(self.buttonExecute)
-
- self.buttonCheck = QtGui.QPushButton()
- icon = QtGui.QIcon.fromTheme("reload",QtGui.QIcon(":/icons/view-refresh.svg"))
- self.buttonCheck.setIcon(icon)
- self.horizontalLayout.addWidget(self.buttonCheck)
- self.buttonCheck.hide()
- self.buttonInstall = QtGui.QPushButton()
- icon = QtGui.QIcon.fromTheme("download")
- self.buttonInstall.setIcon(icon)
- self.horizontalLayout.addWidget(self.buttonInstall)
- self.buttonRemove = QtGui.QPushButton()
- icon = QtGui.QIcon.fromTheme("edit-delete")
- self.buttonRemove.setIcon(icon)
- self.horizontalLayout.addWidget(self.buttonRemove)
- self.buttonCancel = QtGui.QPushButton()
- icon = QtGui.QIcon.fromTheme("cancel")
- self.buttonCancel.setIcon(icon)
- self.buttonCancel.setDefault(True)
- self.horizontalLayout.addWidget(self.buttonCancel)
- self.verticalLayout.addLayout(self.horizontalLayout)
-
- self.retranslateUi()
-
- QtCore.QObject.connect(self.buttonExecute, QtCore.SIGNAL("clicked()"), self.executemacro)
- QtCore.QObject.connect(self.buttonCancel, QtCore.SIGNAL("clicked()"), self.reject)
- QtCore.QObject.connect(self.buttonInstall, QtCore.SIGNAL("clicked()"), self.install)
- QtCore.QObject.connect(self.buttonRemove, QtCore.SIGNAL("clicked()"), self.remove)
- QtCore.QObject.connect(self.labelDescription, QtCore.SIGNAL("linkActivated(QString)"), self.showlink)
- QtCore.QObject.connect(self.listWorkbenches, QtCore.SIGNAL("currentRowChanged(int)"), self.show)
- QtCore.QObject.connect(self.tabWidget, QtCore.SIGNAL("currentChanged(int)"), self.switchtab)
- QtCore.QObject.connect(self.listMacros, QtCore.SIGNAL("currentRowChanged(int)"), self.show_macro)
- QtCore.QObject.connect(self.buttonCheck, QtCore.SIGNAL("clicked()"), self.check_updates)
- QtCore.QMetaObject.connectSlotsByName(self)
+ # center the dialog over the FreeCAD window
+ mw = FreeCADGui.getMainWindow()
+ self.dialog.move(mw.frameGeometry().topLeft() + mw.rect().center() - self.dialog.rect().center())
+ # populate the list
self.update()
- if not NOGIT:
- try:
- import git
- except:
- self.buttonCheck.hide()
- else:
- self.buttonCheck.show()
+ # rock 'n roll!!!
+ self.dialog.exec_()
- # center the dialog over the FreeCAD window
- mw = FreeCADGui.getMainWindow()
- self.move(mw.frameGeometry().topLeft() + mw.rect().center() - self.rect().center())
+ def reject(self):
+ "called when the window has been closed"
+
+ # save window geometry and splitter state for next use
+ pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
+ pref.SetInt("WindowWidth",self.dialog.width())
+ pref.SetInt("WindowHeight",self.dialog.height())
+ pref.SetInt("SplitterLeft",self.dialog.splitter.sizes()[0])
+ pref.SetInt("SplitterRight",self.dialog.splitter.sizes()[1])
- def reject(self):
# ensure all threads are finished before closing
oktoclose = True
for worker in ["update_worker","check_worker","show_worker","showmacro_worker",
@@ -304,10 +177,15 @@ def reject(self):
if thread:
if not thread.isFinished():
oktoclose = False
+
+ # all threads have finished
if oktoclose:
- if hasattr(self,"install_worker") or hasattr(self,"addon_removed"):
+ if (hasattr(self,"install_worker") and self.install_worker) or (hasattr(self,"addon_removed") and self.addon_removed):
+ # display restart dialog
+ from PySide import QtGui,QtCore
m = QtGui.QMessageBox()
m.setWindowTitle(translate("AddonsInstaller","Addon manager"))
+ m.setWindowIcon(QtGui.QIcon(":/icons/AddonManager.svg"))
m.setText(translate("AddonsInstaller","You must restart FreeCAD for changes to take effect. Press Ok to restart FreeCAD now, or Cancel to restart later."))
m.setIcon(m.Warning)
m.setStandardButtons(m.Ok | m.Cancel)
@@ -315,116 +193,180 @@ def reject(self):
ret = m.exec_()
if ret == m.Ok:
shutil.rmtree(self.macro_repo_dir,onerror=self.remove_readonly)
- # restart FreeCAD after a delay to give time to close this dialog
+ # restart FreeCAD after a delay to give time to this dialog to close
QtCore.QTimer.singleShot(1000,restartFreeCAD)
try:
shutil.rmtree(self.macro_repo_dir,onerror=self.remove_readonly)
except:
pass
- QtGui.QDialog.reject(self)
-
- def retranslateUi(self):
- self.setWindowTitle(translate("AddonsInstaller","Addon manager"))
- self.labelDescription.setText(translate("AddonsInstaller", "Downloading addon list..."))
- self.buttonExecute.setText(translate("AddonsInstaller", "Execute"))
- self.buttonExecute.setToolTip(translate("AddonsInstaller", "This button runs the selected macro (which must be installed first)"))
- self.buttonCheck.setText(translate("AddonsInstaller", "Check"))
- self.buttonCheck.setToolTip(translate("AddonsInstaller", "Check for available updates"))
- self.buttonCancel.setText(translate("AddonsInstaller", "Close"))
- self.buttonInstall.setText(translate("AddonsInstaller", "Install / update"))
- self.buttonRemove.setText(translate("AddonsInstaller", "Remove"))
- self.tabWidget.setTabText(self.tabWidget.indexOf(self.listWorkbenches), translate("AddonsInstaller", "Workbenches"))
- self.tabWidget.setTabText(self.tabWidget.indexOf(self.listMacros), translate("AddonsInstaller", "Macros"))
+
+ return True
def update(self):
- self.listWorkbenches.clear()
- self.buttonExecute.setEnabled(False)
+
+ "updates the list of workbenches"
+
+ self.dialog.listWorkbenches.clear()
+ self.dialog.buttonExecute.setEnabled(False)
self.repos = []
- self.info_worker = InfoWorker()
- self.info_worker.addon_repos.connect(self.update_repos)
self.update_worker = UpdateWorker()
- self.update_worker.info_label.connect(self.set_information_label)
+ self.update_worker.info_label.connect(self.show_information)
self.update_worker.addon_repo.connect(self.add_addon_repo)
self.update_worker.progressbar_show.connect(self.show_progress_bar)
+ self.update_worker.done.connect(self.check_updates)
self.update_worker.start()
def check_updates(self):
- if self.tabWidget.currentIndex() == 0:
- if not self.doUpdate:
- self.check_worker = CheckWBWorker(self.repos)
- self.check_worker.mark.connect(self.mark)
- self.check_worker.info_label.connect(self.set_information_label)
- self.check_worker.progressbar_show.connect(self.show_progress_bar)
- self.check_worker.change_button.connect(self.change_update_button)
- self.check_worker.start()
- else:
- self.install(self.doUpdate)
+
+ "checks every installed addon for available updates"
+
+ pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
+ if pref.GetBool("AutoCheck",False) and not self.doUpdate:
+ if hasattr(self,"check_worker"):
+ thread = self.check_worker
+ if thread:
+ if not thread.isFinished():
+ return
+ self.dialog.buttonUpdateAll.setText(translate("AddonsInstaller","Checking for updates..."))
+ self.check_worker = CheckWBWorker(self.repos)
+ self.check_worker.mark.connect(self.mark)
+ self.check_worker.enable.connect(self.enable_updates)
+ self.check_worker.start()
+
+ def apply_updates(self):
+
+ "apply all available updates"
+
+ if self.doUpdate:
+ self.install(self.doUpdate)
+ self.dialog.buttonUpdateAll.setEnabled(False)
+
+ def enable_updates(self,num):
+
+ "enables the update button"
+
+ if num:
+ self.dialog.buttonUpdateAll.setText(translate("AddonsInstaller","Apply")+" "+str(num)+" "+translate("AddonsInstaller","update(s)"))
+ self.dialog.buttonUpdateAll.setEnabled(True)
+ else:
+ self.dialog.buttonUpdateAll.setText(translate("AddonsInstaller","No update available"))
+ self.dialog.buttonUpdateAll.setEnabled(False)
def add_addon_repo(self, addon_repo):
+
+ "adds a workbench to the list"
+
+ from PySide import QtGui
self.repos.append(addon_repo)
if addon_repo[2] == 1 :
- self.listWorkbenches.addItem(QtGui.QListWidgetItem(QtGui.QIcon(":/icons/button_valid.svg"),str(addon_repo[0]) + str(" (Installed)")))
+ self.dialog.listWorkbenches.addItem(QtGui.QListWidgetItem(QtGui.QIcon(":/icons/button_valid.svg"),str(addon_repo[0]) + str(" ("+translate("AddonsInstaller","Installed")+")")))
else:
- self.listWorkbenches.addItem(" "+str(addon_repo[0]))
+ self.dialog.listWorkbenches.addItem(QtGui.QListWidgetItem(QtGui.QIcon(":/icons/Group.svg"),str(addon_repo[0])))
+
+ def show_information(self, label):
- def set_information_label(self, label):
- self.labelDescription.setText(label)
- if self.listWorkbenches.isVisible():
- self.listWorkbenches.setFocus()
+ "shows text in the information pane"
+
+ self.dialog.description.setText(label)
+ if self.dialog.listWorkbenches.isVisible():
+ self.dialog.listWorkbenches.setFocus()
else:
- self.listMacros.setFocus()
+ self.dialog.listMacros.setFocus()
def show(self,idx):
+
+ "loads information of a given workbench"
+
+ # this function is triggered also when the list is populated, prevent that here
+ if idx == 0 and self.firsttime:
+ self.dialog.listWorkbenches.setCurrentRow(-1)
+ self.firsttime = False
+ return
+
if self.repos and idx >= 0:
+ if hasattr(self,"show_worker"):
+ # kill existing show worker (might still be busy loading images...)
+ if self.show_worker:
+ self.show_worker.exit()
self.show_worker = ShowWorker(self.repos, idx)
- self.show_worker.info_label.connect(self.set_information_label)
+ self.show_worker.info_label.connect(self.show_information)
self.show_worker.addon_repos.connect(self.update_repos)
self.show_worker.progressbar_show.connect(self.show_progress_bar)
self.show_worker.start()
+ self.dialog.buttonInstall.setEnabled(True)
+ self.dialog.buttonUninstall.setEnabled(True)
def show_macro(self,idx):
+
+ "loads information of a given macro"
+
+ # this function is triggered when the list is populated, prevent that here
+ if idx == 0 and self.firstmacro:
+ self.dialog.listMacros.setCurrentRow(-1)
+ self.firstmacro = False
+ return
+
if self.macros and idx >= 0:
+ if hasattr(self,"showmacro_worker"):
+ if self.showmacro_worker:
+ if not self.showmacro_worker.isFinished():
+ self.showmacro_worker.exit()
+ if not self.showmacro_worker.isFinished():
+ return
self.showmacro_worker = GetMacroDetailsWorker(self.macros[idx])
- self.showmacro_worker.info_label.connect(self.set_information_label)
+ self.showmacro_worker.info_label.connect(self.show_information)
self.showmacro_worker.progressbar_show.connect(self.show_progress_bar)
self.showmacro_worker.start()
+ self.dialog.buttonInstall.setEnabled(True)
+ self.dialog.buttonUninstall.setEnabled(True)
+ if self.macros[idx].is_installed():
+ self.dialog.buttonExecute.setEnabled(True)
+ else:
+ self.dialog.buttonExecute.setEnabled(False)
def switchtab(self,idx):
+
+ "does what needs to be done when switching tabs"
+
if idx == 1:
if not self.macros:
- self.listMacros.clear()
+ self.dialog.listMacros.clear()
self.macros = []
self.macro_worker = FillMacroListWorker(self.macro_repo_dir)
self.macro_worker.add_macro_signal.connect(self.add_macro)
- self.macro_worker.info_label_signal.connect(self.set_information_label)
+ self.macro_worker.info_label_signal.connect(self.show_information)
self.macro_worker.progressbar_show.connect(self.show_progress_bar)
self.macro_worker.start()
- self.buttonCheck.setEnabled(False)
- else:
- self.buttonCheck.setEnabled(True)
+ self.dialog.listMacros.setCurrentRow(0)
def update_repos(self, repos):
+
+ "convenience function to update the internal list of workbenches"
+
self.repos = repos
def add_macro(self, macro):
- if macro in self.macros:
- # The macro is already in the list of macros.
- old_macro = self.macros[self.macros.index(macro)]
- update_macro_details(old_macro, macro)
- else:
- self.macros.append(macro)
- if macro.is_installed():
- self.listMacros.addItem(QtGui.QListWidgetItem(QtGui.QIcon(":/icons/button_valid.svg"), macro.name + str(' (Installed)')))
- else:
- self.listMacros.addItem(" "+macro.name)
- def showlink(self,link):
- """opens a link with the system browser"""
- #print("clicked: ",link)
- QtGui.QDesktopServices.openUrl(QtCore.QUrl(link, QtCore.QUrl.TolerantMode))
+ "adds a macro to the list"
+
+ if macro.name:
+ if macro in self.macros:
+ # The macro is already in the list of macros.
+ old_macro = self.macros[self.macros.index(macro)]
+ update_macro_details(old_macro, macro)
+ else:
+ from PySide import QtGui
+ self.macros.append(macro)
+ if macro.is_installed():
+ self.dialog.listMacros.addItem(QtGui.QListWidgetItem(QtGui.QIcon(":/icons/button_valid.svg"), macro.name + str(' (Installed)')))
+ else:
+ self.dialog.listMacros.addItem(QtGui.QListWidgetItem(QtGui.QIcon(":/icons/applications-python.svg"),macro.name))
def install(self,repos=None):
- if self.tabWidget.currentIndex() == 0:
+
+ "installs a workbench or macro"
+
+ if self.dialog.tabWidget.currentIndex() == 0:
# Tab "Workbenches".
idx = None
if repos:
@@ -434,52 +376,55 @@ def install(self,repos=None):
if r[0] == repo:
idx.append(i)
else:
- idx = self.listWorkbenches.currentRow()
+ idx = self.dialog.listWorkbenches.currentRow()
if idx != None:
- if hasattr(self,"install_worker"):
+ if hasattr(self,"install_worker") and self.install_worker:
if self.install_worker.isRunning():
return
self.install_worker = InstallWorker(self.repos, idx)
- self.install_worker.info_label.connect(self.set_information_label)
+ self.install_worker.info_label.connect(self.show_information)
self.install_worker.progressbar_show.connect(self.show_progress_bar)
self.install_worker.mark_recompute.connect(self.mark_recompute)
self.install_worker.start()
- elif self.tabWidget.currentIndex() == 1:
+
+ elif self.dialog.tabWidget.currentIndex() == 1:
# Tab "Macros".
- macro = self.macros[self.listMacros.currentRow()]
+ macro = self.macros[self.dialog.listMacros.currentRow()]
if install_macro(macro, self.macro_repo_dir):
- self.labelDescription.setText(translate("AddonsInstaller", "Macro successfully installed. The macro is now available from the Macros dialog."))
+ self.dialog.description.setText(translate("AddonsInstaller", "Macro successfully installed. The macro is now available from the Macros dialog."))
else:
- self.labelDescription.setText(translate("AddonsInstaller", "Unable to install"))
+ self.dialog.description.setText(translate("AddonsInstaller", "Unable to install"))
def show_progress_bar(self, state):
+
+ "shows or hides the progress bar"
+
if state == True:
- self.listWorkbenches.setEnabled(False)
- self.listMacros.setEnabled(False)
- self.buttonExecute.setEnabled(False)
- self.buttonInstall.setEnabled(False)
- self.buttonRemove.setEnabled(False)
- self.buttonCheck.setEnabled(False)
- self.progressBar.show()
+ self.dialog.listWorkbenches.setEnabled(False)
+ self.dialog.listMacros.setEnabled(False)
+ self.dialog.buttonInstall.setEnabled(False)
+ self.dialog.buttonUninstall.setEnabled(False)
+ self.dialog.progressBar.show()
else:
- self.progressBar.hide()
- self.listWorkbenches.setEnabled(True)
- self.listMacros.setEnabled(True)
- self.buttonExecute.setEnabled(False)
- self.buttonInstall.setEnabled(True)
- self.buttonRemove.setEnabled(True)
- if self.tabWidget.currentIndex() == 0:
- self.buttonCheck.setEnabled(True)
- if self.listWorkbenches.isVisible():
- self.listWorkbenches.setFocus()
+ self.dialog.progressBar.hide()
+ self.dialog.listWorkbenches.setEnabled(True)
+ self.dialog.listMacros.setEnabled(True)
+ if not (self.firsttime and self.firstmacro):
+ self.dialog.buttonInstall.setEnabled(True)
+ self.dialog.buttonUninstall.setEnabled(True)
+ if self.dialog.listWorkbenches.isVisible():
+ self.dialog.listWorkbenches.setFocus()
else:
- self.listMacros.setFocus()
- self.buttonExecute.setEnabled(True)
+ self.dialog.listMacros.setFocus()
def executemacro(self):
- if self.tabWidget.currentIndex() == 1:
+
+ "executes a selected macro"
+
+ import FreeCADGui
+ if self.dialog.tabWidget.currentIndex() == 1:
# Tab "Macros".
- macro = self.macros[self.listMacros.currentRow()]
+ macro = self.macros[self.dialog.listMacros.currentRow()]
if not macro.is_installed():
# Macro not installed, nothing to do.
return
@@ -491,645 +436,141 @@ def executemacro(self):
self.hide()
FreeCADGui.SendMsgToActiveView("Run")
else:
- self.buttonExecute.setEnabled(False)
+ self.dialog.buttonExecute.setEnabled(False)
def remove_readonly(self, func, path, _):
- "Remove read only file."
+
+ "Remove a read-only file."
+
os.chmod(path, stat.S_IWRITE)
func(path)
def remove(self):
- if self.tabWidget.currentIndex() == 0:
+
+ "uninstalls a macro or workbench"
+
+ if self.dialog.tabWidget.currentIndex() == 0:
# Tab "Workbenches".
- idx = self.listWorkbenches.currentRow()
+ idx = self.dialog.listWorkbenches.currentRow()
basedir = FreeCAD.getUserAppDataDir()
moddir = basedir + os.sep + "Mod"
clonedir = moddir + os.sep + self.repos[idx][0]
if os.path.exists(clonedir):
shutil.rmtree(clonedir, onerror=self.remove_readonly)
- self.labelDescription.setText(translate("AddonsInstaller", "Addon successfully removed. Please restart FreeCAD"))
+ self.dialog.description.setText(translate("AddonsInstaller", "Addon successfully removed. Please restart FreeCAD"))
else:
- self.labelDescription.setText(translate("AddonsInstaller", "Unable to remove this addon"))
+ self.dialog.description.setText(translate("AddonsInstaller", "Unable to remove this addon"))
+
elif self.tabWidget.currentIndex() == 1:
# Tab "Macros".
- macro = self.macros[self.listMacros.currentRow()]
+ macro = self.macros[self.dialog.listMacros.currentRow()]
if remove_macro(macro):
- self.labelDescription.setText(translate('AddonsInstaller', 'Macro successfully removed.'))
+ self.dialog.description.setText(translate('AddonsInstaller', 'Macro successfully removed.'))
else:
- self.labelDescription.setText(translate('AddonsInstaller', 'Macro could not be removed.'))
+ self.dialog.description.setText(translate('AddonsInstaller', 'Macro could not be removed.'))
self.update_status(soft=True)
- self.addon_removed = True # A value to trigger the restart message
+ self.addon_removed = True # A value to trigger the restart message on dialog close
def mark_recompute(self,addon):
"marks an addon in the list as installed but needs recompute"
- for i in range(self.listWorkbenches.count()):
- txt = self.listWorkbenches.item(i).text().strip()
- if txt.endswith(" (Installed)"):
+ for i in range(self.dialog.listWorkbenches.count()):
+ txt = self.dialog.listWorkbenches.item(i).text().strip()
+ if txt.endswith(" ("+translate("AddonsInstaller","Installed")+")"):
txt = txt[:-12]
- elif txt.endswith(" (Update available)"):
+ elif txt.endswith(" ("+translate("AddonsInstaller","Update available")+")"):
txt = txt[:-19]
if txt == addon:
- self.listWorkbenches.item(i).setText(txt+" (Restart required)")
- self.listWorkbenches.item(i).setIcon(QtGui.QIcon(":/icons/edit-undo.svg"))
+ from PySide import QtGui
+ self.dialog.listWorkbenches.item(i).setText(txt+" ("+translate("AddonsInstaller","Restart required")+")")
+ self.dialog.listWorkbenches.item(i).setIcon(QtGui.QIcon(":/icons/edit-undo.svg"))
def update_status(self,soft=False):
"""Updates the list of workbenches/macros.
If soft is true, items are not recreated (and therefore display text isn't triggered)"
"""
+
moddir = FreeCAD.getUserAppDataDir() + os.sep + "Mod"
+ from PySide import QtGui
if soft:
- for i in range(self.listWorkbenches.count()):
- txt = self.listWorkbenches.item(i).text().strip()
+ for i in range(self.dialog.listWorkbenches.count()):
+ txt = self.dialog.listWorkbenches.item(i).text().strip()
ext = ""
- if txt.endswith(" (Installed)"):
+ if txt.endswith(" ("+translate("AddonsInstaller","Installed")+")"):
txt = txt[:-12]
- ext = " (Installed)"
- elif txt.endswith(" (Update available)"):
+ ext = " ("+translate("AddonsInstaller","Installed")+")"
+ elif txt.endswith(" ("+translate("AddonsInstaller","Update available")+")"):
txt = txt[:-19]
- ext = " (Update available)"
- elif txt.endswith(" (Restart required)"):
+ ext = " ("+translate("AddonsInstaller","Update available")+")"
+ elif txt.endswith(" ("+translate("AddonsInstaller","Restart required")+")"):
txt = txt[:-19]
- ext = " (Restart required)"
+ ext = " ("+translate("AddonsInstaller","Restart required")+")"
if os.path.exists(os.path.join(moddir,txt)):
- self.listWorkbenches.item(i).setText(txt+ext)
+ self.dialog.listWorkbenches.item(i).setText(txt+ext)
else:
- self.listWorkbenches.item(i).setText(" "+txt)
- self.listWorkbenches.item(i).setIcon(QtGui.QIcon())
- for i in range(self.listMacros.count()):
- txt = self.listMacros.item(i).text().strip()
- if txt.endswith(" (Installed)"):
+ self.dialog.listWorkbenches.item(i).setText(txt)
+ self.dialog.listWorkbenches.item(i).setIcon(QtGui.QIcon(":/icons/Group.svg"))
+ for i in range(self.dialog.listMacros.count()):
+ txt = self.dialog.listMacros.item(i).text().strip()
+ if txt.endswith(" ("+translate("AddonsInstaller","Installed")+")"):
txt = txt[:-12]
- elif txt.endswith(" (Update available)"):
+ elif txt.endswith(" ("+translate("AddonsInstaller","Update available")+")"):
txt = txt[:-19]
if os.path.exists(os.path.join(moddir,txt)):
- self.listMacros.item(i).setText(txt+ext)
+ self.dialog.listMacros.item(i).setText(txt+ext)
else:
- self.listMacros.item(i).setText(" "+txt)
- self.listMacros.item(i).setIcon(QtGui.QIcon())
+ self.dialog.listMacros.item(i).setText(txt)
+ self.dialog.listMacros.item(i).setIcon(QtGui.QIcon(":/icons/Group.svg"))
else:
- self.listWorkbenches.clear()
- self.listMacros.clear()
+ self.dialog.listWorkbenches.clear()
+ self.dialog.listMacros.clear()
for wb in self.repos:
if os.path.exists(os.path.join(moddir,wb[0])):
- self.listWorkbenches.addItem(QtGui.QListWidgetItem(QtGui.QIcon(":/icons/button_valid.svg"),str(wb[0]) + str(" (Installed)")))
+ self.dialog.listWorkbenches.addItem(QtGui.QListWidgetItem(QtGui.QIcon(":/icons/button_valid.svg"),str(wb[0]) + " ("+translate("AddonsInstaller","Installed")+")"))
wb[2] = 1
else:
- self.listWorkbenches.addItem(" "+str(wb[0]))
+ self.dialog.listWorkbenches.addItem(QtGui.QListWidgetItem(QtGui.QIcon(":/icons/applications-python.svg"),str(wb[0])))
wb[2] = 0
for macro in self.macros:
if macro.is_installed():
- self.listMacros.addItem(QtGui.QListWidgetItem(QtGui.QIcon(":/icons/button_valid.svg"), macro.name + str(' (Installed)')))
+ self.dialog.listMacros.addItem(QtGui.QListWidgetItem(QtGui.QIcon(":/icons/button_valid.svg"), macro.name + " ("+translate("AddonsInstaller","Installed")+")"))
else:
- self.listMacros.addItem(" "+macro.name)
+ self.dialog.listMacros.addItem(QtGui.QListWidgetItem(QtGui.QIcon(":/icons/applications-python.svg"),+macro.name))
def mark(self,repo):
- for i in range(self.listWorkbenches.count()):
- w = self.listWorkbenches.item(i)
+
+ "mark a workbench as updatable"
+
+ from PySide import QtGui
+ for i in range(self.dialog.listWorkbenches.count()):
+ w = self.dialog.listWorkbenches.item(i)
if w.text().startswith(str(repo)):
- w.setText(str(repo) + str(" (Update available)"))
- w.setIcon(QtGui.QIcon(":/icons/view-refresh.svg"))
+ w.setText(str(repo) + str(" ("+translate("AddonsInstaller","Update available")+")"))
+ w.setIcon(QtGui.QIcon(":/icons/debug-marker.svg"))
if not repo in self.doUpdate:
self.doUpdate.append(repo)
- def change_update_button(self):
- self.buttonCheck.setText(translate("AddonsInstaller", "Update all"))
- self.buttonCheck.setToolTip(translate("AddonsInstaller", "Apply all available updates"))
-
-
-
-
-
-class UpdateWorker(QtCore.QThread):
-
- info_label = QtCore.Signal(str)
- addon_repo = QtCore.Signal(object)
- progressbar_show = QtCore.Signal(bool)
-
- def __init__(self):
- QtCore.QThread.__init__(self)
-
- def run(self):
- "populates the list of addons"
- self.progressbar_show.emit(True)
- u = urlopen("https://github.com/FreeCAD/FreeCAD-addons")
- p = u.read()
- if sys.version_info.major >= 3 and isinstance(p, bytes):
- p = p.decode("utf-8")
- u.close()
- p = p.replace("\n"," ")
- p = re.findall("octicon-file-submodule(.*?)message",p)
- basedir = FreeCAD.getUserAppDataDir()
- moddir = basedir + os.sep + "Mod"
- repos = []
- # querying official addons
- for l in p:
- #name = re.findall("data-skip-pjax=\"true\">(.*?)<",l)[0]
- name = re.findall("title=\"(.*?) @",l)[0]
- self.info_label.emit(name)
- #url = re.findall("title=\"(.*?) @",l)[0]
- url = "https://github.com/" + re.findall("href=\"\/(.*?)\/tree",l)[0]
- addondir = moddir + os.sep + name
- #print ("found:",name," at ",url)
- if not os.path.exists(addondir):
- state = 0
- else:
- state = 1
- repos.append([name,url,state])
- # querying custom addons
- customaddons = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons").GetString("CustomRepositories","").split("\n")
- for url in customaddons:
- if url:
- name = url.split("/")[-1]
- if name.lower().endswith(".git"):
- name = name[:-4]
- addondir = moddir + os.sep + name
- if not os.path.exists(addondir):
- state = 0
- else:
- state = 1
- repos.append([name,url,state])
- if not repos:
- self.info_label.emit(translate("AddonsInstaller", "Unable to download addon list."))
- else:
- repos = sorted(repos, key=lambda s: s[0].lower())
- for repo in repos:
- self.addon_repo.emit(repo)
- self.info_label.emit(translate("AddonsInstaller", "Workbenches list was updated."))
- self.progressbar_show.emit(False)
- self.stop = True
-
-
-class InfoWorker(QtCore.QThread):
- addon_repos = QtCore.Signal(object)
-
- def __init__(self):
- QtCore.QThread.__init__(self)
-
- def run(self):
- i = 0
- for repo in self.repos:
- url = repo[1]
- u = urlopen(url)
- p = u.read()
- if sys.version_info.major >= 3 and isinstance(p, bytes):
- p = p.decode("utf-8")
- u.close()
- desc = re.findall("= 3 and isinstance(p, bytes):
- p = p.decode("utf-8")
- macros = re.findall('title="(Macro.*?)"', p)
- macros = [mac for mac in macros if ('translated' not in mac)]
- for mac in macros:
- macname = mac[6:] # Remove "Macro ".
- macname = macname.replace("&","&")
- if (macname not in MACROS_BLACKLIST) and ('recipes' not in macname.lower()):
- macro = Macro(macname)
- macro.on_wiki = True
- self.macros.append(macro)
-
-
-class ShowWorker(QtCore.QThread):
+ def show_config(self):
- info_label = QtCore.Signal(str)
- addon_repos = QtCore.Signal(object)
- progressbar_show = QtCore.Signal(bool)
-
- def __init__(self, repos, idx):
- QtCore.QThread.__init__(self)
- self.repos = repos
- self.idx = idx
-
- def run(self):
- self.progressbar_show.emit(True)
- self.info_label.emit(translate("AddonsInstaller", "Retrieving description..."))
- if len(self.repos[self.idx]) == 4:
- desc = self.repos[self.idx][3]
- else:
- url = self.repos[self.idx][1]
- self.info_label.emit(translate("AddonsInstaller", "Retrieving info from") + ' ' + str(url))
- u = urlopen(url)
- p = u.read()
- if sys.version_info.major >= 3 and isinstance(p, bytes):
- p = p.decode("utf-8")
- u.close()
- desc = re.findall("This add-on is marked as obsolete - This usually means it is no longer maintained, and some more advanced add-on in this list provides the same functionality."
- else:
- desc = "Unable to retrieve addon description"
- self.repos[self.idx].append(desc)
- self.addon_repos.emit(self.repos)
- if self.repos[self.idx][2] == 1:
- upd = False
- # checking for updates
- if not NOGIT:
- try:
- import git
- except:
- pass
- else:
- repo = self.repos[self.idx]
- clonedir = FreeCAD.getUserAppDataDir() + os.sep + "Mod" + os.sep + repo[0]
- if os.path.exists(clonedir):
- if not os.path.exists(clonedir + os.sep + '.git'):
- # Repair addon installed with raw download
- bare_repo = git.Repo.clone_from(repo[1], clonedir + os.sep + '.git', bare=True)
- try:
- with bare_repo.config_writer() as cw:
- cw.set('core', 'bare', False)
- except AttributeError:
- FreeCAD.Console.PrintWarning(translate("AddonsInstaller", "Outdated GitPython detected, consider upgrading with pip.")+"\n")
- cw = bare_repo.config_writer()
- cw.set('core', 'bare', False)
- del cw
- repo = git.Repo(clonedir)
- repo.head.reset('--hard')
- gitrepo = git.Git(clonedir)
- gitrepo.fetch()
- if "git pull" in gitrepo.status():
- upd = True
- if upd:
- message = "" + translate("AddonsInstaller", "An update is available for this addon.") + " " + desc + ' - ' + self.repos[self.idx][1] + ''
- else:
- message = "" + translate("AddonsInstaller", "This addon is already installed.") + " " + desc + ' - ' + self.repos[self.idx][1] + ''
- else:
- message = desc + ' - ' + self.repos[self.idx][1] + ''
- self.info_label.emit( message )
- self.progressbar_show.emit(False)
- self.stop = True
-
-
-class GetMacroDetailsWorker(QtCore.QThread):
- """Retrieve the macro details for a macro"""
-
- info_label = QtCore.Signal(str)
- progressbar_show = QtCore.Signal(bool)
-
- def __init__(self, macro):
- QtCore.QThread.__init__(self)
- self.macro = macro
-
- def run(self):
- self.progressbar_show.emit(True)
- self.info_label.emit(translate("AddonsInstaller", "Retrieving description..."))
- if not self.macro.parsed and self.macro.on_git:
- self.info_label.emit(translate('AddonsInstaller', 'Retrieving info from git'))
- self.macro.fill_details_from_file(self.macro.src_filename)
- if not self.macro.parsed and self.macro.on_wiki:
- self.info_label.emit(translate('AddonsInstaller', 'Retrieving info from wiki'))
- mac = self.macro.name.replace(' ', '_')
- mac = mac.replace('&', '%26')
- mac = mac.replace('+', '%2B')
- url = 'https://www.freecadweb.org/wiki/Macro_' + mac
- self.macro.fill_details_from_wiki(url)
- if self.macro.is_installed():
- already_installed_msg = (''
- + translate("AddonsInstaller", "This addon is already installed.")
- + ' ')
- else:
- already_installed_msg = ''
- message = (already_installed_msg
- + self.macro.desc
- + ' - '
- + self.macro.url
- + '')
- self.info_label.emit(message)
- self.progressbar_show.emit(False)
- self.stop = True
-
-
-class InstallWorker(QtCore.QThread):
-
- info_label = QtCore.Signal(str)
- progressbar_show = QtCore.Signal(bool)
- mark_recompute = QtCore.Signal(str)
-
- def __init__(self, repos, idx):
- QtCore.QThread.__init__(self)
- self.idx = idx
- self.repos = repos
+ "shows the configuration dialog"
- def run(self):
- "installs or updates the selected addon"
- git = None
- try:
- import git
- except Exception as e:
- self.info_label.emit("GitPython not found.")
- print(e)
- FreeCAD.Console.PrintWarning(translate("AddonsInstaller","GitPython not found. Using standard download instead.")+"\n")
- try:
- import zipfile
- except:
- self.info_label.emit("no zip support.")
- FreeCAD.Console.PrintError(translate("AddonsInstaller","Your version of python doesn't appear to support ZIP files. Unable to proceed.")+"\n")
- return
- try:
- import StringIO as io
- except ImportError: # StringIO is not available with python3
- import io
- if not isinstance(self.idx,list):
- self.idx = [self.idx]
- for idx in self.idx:
- if idx < 0:
- return
- if not self.repos:
- return
- if NOGIT:
- git = None
- basedir = FreeCAD.getUserAppDataDir()
- moddir = basedir + os.sep + "Mod"
- if not os.path.exists(moddir):
- os.makedirs(moddir)
- clonedir = moddir + os.sep + self.repos[idx][0]
- self.progressbar_show.emit(True)
- if os.path.exists(clonedir):
- self.info_label.emit("Updating module...")
- if git:
- if not os.path.exists(clonedir + os.sep + '.git'):
- # Repair addon installed with raw download
- bare_repo = git.Repo.clone_from(self.repos[idx][1], clonedir + os.sep + '.git', bare=True)
- try:
- with bare_repo.config_writer() as cw:
- cw.set('core', 'bare', False)
- except AttributeError:
- FreeCAD.Console.PrintWarning(translate("AddonsInstaller", "Outdated GitPython detected, consider upgrading with pip.")+"\n")
- cw = bare_repo.config_writer()
- cw.set('core', 'bare', False)
- del cw
- repo = git.Repo(clonedir)
- repo.head.reset('--hard')
- repo = git.Git(clonedir)
- answer = repo.pull()
-
- # Update the submodules for this repository
- repo_sms = git.Repo(clonedir)
- for submodule in repo_sms.submodules:
- submodule.update(init=True, recursive=True)
- else:
- answer = self.download(self.repos[idx][1],clonedir)
- else:
- self.info_label.emit("Checking module dependencies...")
- depsok,answer = self.checkDependencies(self.repos[idx][1])
- if depsok:
- if git:
- self.info_label.emit("Cloning module...")
- repo = git.Repo.clone_from(self.repos[idx][1], clonedir, branch='master')
-
- # Make sure to clone all the submodules as well
- if repo.submodules:
- repo.submodule_update(recursive=True)
- else:
- self.info_label.emit("Downloading module...")
- self.download(self.repos[idx][1],clonedir)
- answer = translate("AddonsInstaller", "Workbench successfully installed. Please restart FreeCAD to apply the changes.")
- # symlink any macro contained in the module to the macros folder
- macro_dir = FreeCAD.getUserMacroDir(True)
- if not os.path.exists(macro_dir):
- os.makedirs(macro_dir)
- if os.path.exists(clonedir):
- for f in os.listdir(clonedir):
- if f.lower().endswith(".fcmacro"):
- print("copying macro:",f)
- symlink(os.path.join(clonedir, f), os.path.join(macro_dir, f))
- FreeCAD.ParamGet('User parameter:Plugins/'+self.repos[idx][0]).SetString("destination",clonedir)
- answer += translate("AddonsInstaller", "A macro has been installed and is available the Macros menu") + ": "
- answer += f + ""
- self.progressbar_show.emit(False)
- self.info_label.emit(answer)
- self.mark_recompute.emit(self.repos[idx][0])
- self.stop = True
-
- def checkDependencies(self,baseurl):
- "checks if the repo contains a metadata.txt and check its contents"
import FreeCADGui
- ok = True
- message = ""
- depsurl = baseurl.replace("github.com","raw.githubusercontent.com")
- if not depsurl.endswith("/"):
- depsurl += "/"
- depsurl += "master/metadata.txt"
- try:
- mu = urlopen(depsurl)
- except urllib2.HTTPError:
- # no metadata.txt, we just continue without deps checking
- pass
- else:
- # metadata.txt found
- depsfile = mu.read()
- mu.close()
-
- # urllib2 gives us a bytelike object instead of a string. Have to consider that
- try:
- depsfile = depsfile.decode('utf-8')
- except AttributeError:
- pass
-
- deps = depsfile.split("\n")
- for l in deps:
- if l.startswith("workbenches="):
- depswb = l.split("=")[1].split(",")
- for wb in depswb:
- if wb.strip():
- if not wb.strip() in FreeCADGui.listWorkbenches().keys():
- if not wb.strip()+"Workbench" in FreeCADGui.listWorkbenches().keys():
- ok = False
- message += translate("AddonsInstaller","Missing workbench") + ": " + wb + ", "
- elif l.startswith("pylibs="):
- depspy = l.split("=")[1].split(",")
- for pl in depspy:
- if pl.strip():
- try:
- __import__(pl.strip())
- except:
- ok = False
- message += translate("AddonsInstaller","Missing python module") +": " + pl + ", "
- elif l.startswith("optionalpylibs="):
- opspy = l.split("=")[1].split(",")
- for pl in opspy:
- if pl.strip():
- try:
- __import__(pl.strip())
- except:
- message += translate("AddonsInstaller","Missing optional python module (doesn't prevent installing)") +": " + pl + ", "
- if message and (not ok):
- message = translate("AddonsInstaller", "Some errors were found that prevent to install this workbench") + ": " + message + ". "
- message += translate("AddonsInstaller","Please install the missing components first.")
- return ok, message
-
- def download(self,giturl,clonedir):
- "downloads and unzip from github"
- import zipfile
- bakdir = None
- if os.path.exists(clonedir):
- bakdir = clonedir+".bak"
- if os.path.exists(bakdir):
- shutil.rmtree(bakdir)
- os.rename(clonedir,bakdir)
- os.makedirs(clonedir)
- zipurl = giturl+"/archive/master.zip"
- try:
- print("Downloading "+zipurl)
- u = urlopen(zipurl)
- except:
- return translate("AddonsInstaller", "Error: Unable to download") + " " + zipurl
- zfile = _stringio()
- zfile.write(u.read())
- zfile = zipfile.ZipFile(zfile)
- master = zfile.namelist()[0] # github will put everything in a subfolder
- zfile.extractall(clonedir)
- u.close()
- zfile.close()
- for filename in os.listdir(clonedir+os.sep+master):
- shutil.move(clonedir+os.sep+master+os.sep+filename, clonedir+os.sep+filename)
- os.rmdir(clonedir+os.sep+master)
- if bakdir:
- shutil.rmtree(bakdir)
- return translate("AddonsInstaller", "Successfully installed") + " " + zipurl
-
-
-def launchAddonMgr():
- # first use dialog
- readWarning = FreeCAD.ParamGet('User parameter:Plugins/addonsRepository').GetBool('readWarning',False)
- if not readWarning:
- if QtGui.QMessageBox.warning(None,"FreeCAD",translate("AddonsInstaller", "The addons that can be installed here are not officially part of FreeCAD, and are not reviewed by the FreeCAD team. Make sure you know what you are installing!"), QtGui.QMessageBox.Cancel | QtGui.QMessageBox.Ok) != QtGui.QMessageBox.StandardButton.Cancel:
- FreeCAD.ParamGet('User parameter:Plugins/addonsRepository').SetBool('readWarning',True)
- readWarning = True
-
- if readWarning:
- dialog = AddonsInstaller()
- dialog.exec_()
+ from PySide import QtGui
+ self.config = FreeCADGui.PySideUic.loadUi(os.path.join(os.path.dirname(__file__),"AddonManagerOptions.ui"))
+
+ # restore stored values
+ pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
+ self.config.checkUpdates.setChecked(pref.GetBool("AutoCheck",False))
+ self.config.customRepositories.setPlainText(pref.GetString("CustomRepositories",""))
+
+ # center the dialog over the Addon Manager
+ self.config.move(self.dialog.frameGeometry().topLeft() + self.dialog.rect().center() - self.config.rect().center())
+
+ ret = self.config.exec_()
+
+ if ret:
+ # OK button has been pressed
+ pref.SetBool("AutoCheck",self.config.checkUpdates.isChecked())
+ pref.SetString("CustomRepositories",self.config.customRepositories.toPlainText())
diff --git a/src/Mod/AddonManager/AddonManager.ui b/src/Mod/AddonManager/AddonManager.ui
new file mode 100644
index 000000000000..a982168d6229
--- /dev/null
+++ b/src/Mod/AddonManager/AddonManager.ui
@@ -0,0 +1,160 @@
+
+
+ Dialog
+
+
+
+ 0
+ 0
+ 599
+ 480
+
+
+
+ Addons manager
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+ Qt::Horizontal
+
+
+
+
+
+
+ 0
+
+
+
+ Workbenches
+
+
+
+
+
+
+
+
+
+ Macros
+
+
+
+
+
+
+
+
+ Executes the selected macro, if installed
+
+
+ Execute
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+ Sets configuration options for the Addon Manager
+
+
+ Configure...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 24
+
+
+ Downloading info...
+
+
+
+
+
+
+
+
+ QLayout::SetDefaultConstraint
+
+
+
+
+ Uninstalls a selected macro or workbench
+
+
+ Uninstall selected
+
+
+
+
+
+
+ Installs or updates the selected macro or workbench
+
+
+ Install/update selected
+
+
+
+
+
+
+ Download and apply all available updates
+
+
+ Update all
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Mod/AddonManager/AddonManagerGui.py b/src/Mod/AddonManager/AddonManagerGui.py
deleted file mode 100644
index 9bf49d61fe20..000000000000
--- a/src/Mod/AddonManager/AddonManagerGui.py
+++ /dev/null
@@ -1,17 +0,0 @@
-# -*- coding: utf-8 -*-
-# FreeCAD tools of the AddonManager workbench
-# (c) 2001 Juergen Riegel
-# License LGPL
-
-import FreeCAD, FreeCADGui
-
-class CmdAddonMgr:
- def Activated(self):
- import AddonManager
- AddonManager.launchAddonMgr()
- def IsActive(self):
- return True
- def GetResources(self):
- return {'Pixmap': 'AddonManager', 'MenuText': '&Addon manager',
- 'ToolTip': 'Manage FreeCAD workbenches and macros',
- 'Group': 'Tools'}
diff --git a/src/Mod/AddonManager/AddonManagerOptions.ui b/src/Mod/AddonManager/AddonManagerOptions.ui
new file mode 100644
index 000000000000..efebdf3aa1c3
--- /dev/null
+++ b/src/Mod/AddonManager/AddonManagerOptions.ui
@@ -0,0 +1,88 @@
+
+
+ Dialog
+
+
+
+ 0
+ 0
+ 390
+ 183
+
+
+
+ Addon manager options
+
+
+
+
+
+ If this option is checked, when launching the Addon Manager, installed addons will be checked for available updates (requires the python-git package installed on your system)
+
+
+ Automatically check for updates at start (requires python-git)
+
+
+
+
+
+
+ Custom repositories (one per line):
+
+
+
+
+
+
+ You can use this window to specify additional addon repositories to be scanned for available addons
+
+
+
+
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ Dialog
+ accept()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+ buttonBox
+ rejected()
+ Dialog
+ reject()
+
+
+ 316
+ 260
+
+
+ 286
+ 274
+
+
+
+
+
diff --git a/src/Mod/AddonManager/CMakeLists.txt b/src/Mod/AddonManager/CMakeLists.txt
index 488eacf47263..b155ae408730 100644
--- a/src/Mod/AddonManager/CMakeLists.txt
+++ b/src/Mod/AddonManager/CMakeLists.txt
@@ -2,9 +2,11 @@ SET(AddonManager_SRCS
Init.py
InitGui.py
AddonManager.py
- AddonManagerGui.py
addonmanager_macro.py
addonmanager_utilities.py
+ addonmanager_workers.py
+ AddonManager.ui
+ AddonManagerOptions.ui
)
SOURCE_GROUP("" FILES ${AddonManager_SRCS})
diff --git a/src/Mod/AddonManager/InitGui.py b/src/Mod/AddonManager/InitGui.py
index 1cc8d70f57b2..7f1dd9e70dcd 100644
--- a/src/Mod/AddonManager/InitGui.py
+++ b/src/Mod/AddonManager/InitGui.py
@@ -2,6 +2,6 @@
# AddonManager gui init module
# (c) 2001 Juergen Riegel
# License LGPL
-from AddonManagerGui import CmdAddonMgr
-FreeCADGui.addCommand('Std_AddonMgr', CmdAddonMgr())
+import AddonManager
+FreeCADGui.addCommand('Std_AddonMgr', AddonManager.CommandAddonManager())
diff --git a/src/Mod/AddonManager/Resources/translations/AddonManager.ts b/src/Mod/AddonManager/Resources/translations/AddonManager.ts
new file mode 100644
index 000000000000..bc719b1e6f35
--- /dev/null
+++ b/src/Mod/AddonManager/Resources/translations/AddonManager.ts
@@ -0,0 +1,337 @@
+
+
+
+ AddonsInstaller
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Dialog
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Std_AddonMgr
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Mod/AddonManager/addonmanager_macro.py b/src/Mod/AddonManager/addonmanager_macro.py
index 4e3b474b564e..732cdd7300fc 100644
--- a/src/Mod/AddonManager/addonmanager_macro.py
+++ b/src/Mod/AddonManager/addonmanager_macro.py
@@ -27,7 +27,6 @@
import FreeCAD
from addonmanager_utilities import translate
-from addonmanager_utilities import urllib2
from addonmanager_utilities import urlopen
@@ -95,7 +94,8 @@ def fill_details_from_wiki(self, url):
code = ""
try:
u = urlopen(url)
- except urllib2.HTTPError:
+ except:
+ print("AddonManager: Debug: unable to open URL",url)
return
p = u.read()
if sys.version_info.major >= 3 and isinstance(p, bytes):
@@ -108,7 +108,8 @@ def fill_details_from_wiki(self, url):
rawcodeurl = rawcodeurl[0]
try:
u2 = urlopen(rawcodeurl)
- except urllib2.HTTPError:
+ except:
+ print("AddonManager: Debug: unable to open URL",rawcodeurl)
return
# code = u2.read()
# github is slow to respond... We need to use this trick below
@@ -146,7 +147,7 @@ def fill_details_from_wiki(self, url):
code = HTMLParser().unescape(code)
code = code.replace(b'\xc2\xa0'.decode("utf-8"), ' ')
except:
- FreeCAD.Console.PrintWarning(translate("AddonsInstaller", "Unable to clean macro code: ") + code + '\n')
+ FreeCAD.Console.PrintWarning(translate("AddonsInstaller", "Unable to clean macro code") + ": "+ code + '\n')
if sys.version_info.major < 3:
code = code.encode('utf8')
desc = re.findall("
(.*?)<\/td>", p.replace('\n', ' '))
diff --git a/src/Mod/AddonManager/addonmanager_utilities.py b/src/Mod/AddonManager/addonmanager_utilities.py
index c3f76a7b560d..2d742f9f329d 100644
--- a/src/Mod/AddonManager/addonmanager_utilities.py
+++ b/src/Mod/AddonManager/addonmanager_utilities.py
@@ -1,22 +1,30 @@
# -*- coding: utf-8 -*-
+#***************************************************************************
+#* *
+#* Copyright (c) 2018 Gaël Écorchard *
+#* *
+#* This program is free software; you can redistribute it and/or modify *
+#* it under the terms of the GNU Lesser General Public License (LGPL) *
+#* as published by the Free Software Foundation; either version 2 of *
+#* the License, or (at your option) any later version. *
+#* for detail see the LICENCE text file. *
+#* *
+#* 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 Library General Public License for more details. *
+#* *
+#* You should have received a copy of the GNU Library General Public *
+#* License along with this program; if not, write to the Free Software *
+#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
+#* USA *
+#* *
+#***************************************************************************
import os
import sys
-if sys.version_info.major < 3:
- import urllib2
-else:
- import urllib.request as urllib2
-from PySide import QtGui
-
-# Qt translation handling
-try:
- _encoding = QtGui.QApplication.UnicodeUTF8
- def translate(context, text, disambig=None):
- return QtGui.QApplication.translate(context, text, disambig, _encoding)
-except AttributeError:
- def translate(context, text, disambig=None):
- return QtGui.QApplication.translate(context, text, disambig)
+# check for SSL support
ssl_ctx = None
try:
@@ -30,10 +38,183 @@ def translate(context, text, disambig=None):
pass
+
+def translate(context, text, disambig=None):
+
+ "Main translation function"
+
+ from PySide import QtGui
+ try:
+ _encoding = QtGui.QApplication.UnicodeUTF8
+ except AttributeError:
+ return QtGui.QApplication.translate(context, text, disambig)
+ else:
+ return QtGui.QApplication.translate(context, text, disambig, _encoding)
+
+
+def symlink(source, link_name):
+
+ "creates a symlink of a file, if possible"
+
+ if os.path.exists(link_name) or os.path.lexists(link_name):
+ #print("macro already exists")
+ pass
+ else:
+ os_symlink = getattr(os, "symlink", None)
+ if callable(os_symlink):
+ os_symlink(source, link_name)
+ else:
+ import ctypes
+ csl = ctypes.windll.kernel32.CreateSymbolicLinkW
+ csl.argtypes = (ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint32)
+ csl.restype = ctypes.c_ubyte
+ flags = 1 if os.path.isdir(source) else 0
+ # set the SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE flag
+ # (see https://blogs.windows.com/buildingapps/2016/12/02/symlinks-windows-10/#joC5tFKhdXs2gGml.97)
+ flags += 2
+ if csl(link_name, source, flags) == 0:
+ raise ctypes.WinError()
+
+
def urlopen(url):
+
"""Opens an url with urllib2"""
- if ssl_ctx:
- u = urllib2.urlopen(url, context=ssl_ctx)
+
+ if sys.version_info.major < 3:
+ import urllib2
else:
- u = urllib2.urlopen(url)
- return u
+ import urllib.request as urllib2
+
+ try:
+ if ssl_ctx:
+ u = urllib2.urlopen(url, context=ssl_ctx)
+ else:
+ u = urllib2.urlopen(url)
+ except:
+ return None
+ else:
+ return u
+
+
+def update_macro_details(old_macro, new_macro):
+
+ """Update a macro with information from another one
+
+ Update a macro with information from another one, supposedly the same but
+ from a different source. The first source is supposed to be git, the second
+ one the wiki.
+ """
+
+ if old_macro.on_git and new_macro.on_git:
+ FreeCAD.Console.PrintWarning('The macro "{}" is present twice in github, please report'.format(old_macro.name))
+ # We don't report macros present twice on the wiki because a link to a
+ # macro is considered as a macro. For example, 'Perpendicular To Wire'
+ # appears twice, as of 2018-05-05).
+ old_macro.on_wiki = new_macro.on_wiki
+ for attr in ['desc', 'url', 'code']:
+ if not hasattr(old_macro, attr):
+ setattr(old_macro, attr, getattr(new_macro, attr))
+
+
+def install_macro(macro, macro_repo_dir):
+
+ """Install a macro and all its related files
+
+ Returns True if the macro was installed correctly.
+
+ Parameters
+ ----------
+ - macro: an addonmanager_macro.Macro instance
+ """
+
+ if not macro.code:
+ return False
+ macro_dir = FreeCAD.getUserMacroDir(True)
+ if not os.path.isdir(macro_dir):
+ try:
+ os.makedirs(macro_dir)
+ except OSError:
+ return False
+ macro_path = os.path.join(macro_dir, macro.filename)
+ if sys.version_info.major < 3:
+ # In python2 the code is a bytes object.
+ mode = 'wb'
+ else:
+ mode = 'w'
+ try:
+ with open(macro_path, mode) as macrofile:
+ macrofile.write(macro.code)
+ except IOError:
+ return False
+ # Copy related files, which are supposed to be given relative to
+ # macro.src_filename.
+ base_dir = os.path.dirname(macro.src_filename)
+ for other_file in macro.other_files:
+ 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
+ src_file = os.path.join(base_dir, other_file)
+ dst_file = os.path.join(macro_dir, other_file)
+ try:
+ shutil.copy(src_file, dst_file)
+ except IOError:
+ return False
+ return True
+
+
+def remove_macro(macro):
+
+ """Remove a macro and all its related files
+
+ Returns True if the macro was removed correctly.
+
+ Parameters
+ ----------
+ - macro: an addonmanager_macro.Macro instance
+ """
+
+ if not macro.is_installed():
+ # Macro not installed, nothing to do.
+ return True
+ macro_dir = FreeCAD.getUserMacroDir(True)
+ macro_path = os.path.join(macro_dir, macro.filename)
+ macro_path_with_macro_prefix = os.path.join(macro_dir, 'Macro_' + macro.filename)
+ if os.path.exists(macro_path):
+ os.remove(macro_path)
+ elif os.path.exists(macro_path_with_macro_prefix):
+ os.remove(macro_path_with_macro_prefix)
+ # Remove related files, which are supposed to be given relative to
+ # macro.src_filename.
+ for other_file in macro.other_files:
+ dst_file = os.path.join(macro_dir, other_file)
+ remove_directory_if_empty(os.path.dirname(dst_file))
+ os.remove(dst_file)
+ return True
+
+
+def remove_directory_if_empty(dir):
+
+ """Remove the directory if it is empty
+
+ Directory FreeCAD.getUserMacroDir(True) will not be removed even if empty.
+ """
+
+ if dir == FreeCAD.getUserMacroDir(True):
+ return
+ if not os.listdir(dir):
+ os.rmdir(dir)
+
+
+def restartFreeCAD():
+
+ "Shuts down and restarts FreeCAD"
+
+ import FreeCADGui
+ from PySide import QtGui,QtCore
+ args = QtGui.QApplication.arguments()[1:]
+ if FreeCADGui.getMainWindow().close():
+ QtCore.QProcess.startDetached(QtGui.QApplication.applicationFilePath(),args)
+
diff --git a/src/Mod/AddonManager/addonmanager_workers.py b/src/Mod/AddonManager/addonmanager_workers.py
new file mode 100644
index 000000000000..b419bf67e945
--- /dev/null
+++ b/src/Mod/AddonManager/addonmanager_workers.py
@@ -0,0 +1,688 @@
+# -*- coding: utf-8 -*-
+#***************************************************************************
+#* *
+#* Copyright (c) 2019 Yorik van Havre *
+#* *
+#* This program is free software; you can redistribute it and/or modify *
+#* it under the terms of the GNU Lesser General Public License (LGPL) *
+#* as published by the Free Software Foundation; either version 2 of *
+#* the License, or (at your option) any later version. *
+#* for detail see the LICENCE text file. *
+#* *
+#* 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 Library General Public License for more details. *
+#* *
+#* You should have received a copy of the GNU Library General Public *
+#* License along with this program; if not, write to the Free Software *
+#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
+#* USA *
+#* *
+#***************************************************************************
+
+import os
+import re
+import shutil
+import stat
+import sys
+import tempfile
+import FreeCAD
+
+from PySide import QtCore
+
+from addonmanager_macro import Macro
+from addonmanager_utilities import urlopen
+from addonmanager_utilities import translate
+from addonmanager_utilities import symlink
+
+MACROS_BLACKLIST = ["BOLTS","WorkFeatures","how to install","PartsLibrary","FCGear"]
+OBSOLETE = ["assembly2","drawing_dimensioning","cura_engine"] # These addons will print an additional message informing the user
+NOGIT = False # for debugging purposes, set this to True to always use http downloads
+
+
+
+"Multithread workers for the Addon Manager"
+
+
+
+class UpdateWorker(QtCore.QThread):
+
+ "This worker updates the list of available workbenches"
+
+ info_label = QtCore.Signal(str)
+ addon_repo = QtCore.Signal(object)
+ progressbar_show = QtCore.Signal(bool)
+ done = QtCore.Signal()
+
+ def __init__(self):
+
+ QtCore.QThread.__init__(self)
+
+ def run(self):
+
+ "populates the list of addons"
+
+ self.progressbar_show.emit(True)
+ u = urlopen("https://github.com/FreeCAD/FreeCAD-addons")
+ if not u:
+ self.progressbar_show.emit(False)
+ self.done.emit()
+ self.stop = True
+ return
+ p = u.read()
+ if sys.version_info.major >= 3 and isinstance(p, bytes):
+ p = p.decode("utf-8")
+ u.close()
+ p = p.replace("\n"," ")
+ p = re.findall("octicon-file-submodule(.*?)message",p)
+ basedir = FreeCAD.getUserAppDataDir()
+ moddir = basedir + os.sep + "Mod"
+ repos = []
+ # querying official addons
+ for l in p:
+ #name = re.findall("data-skip-pjax=\"true\">(.*?)<",l)[0]
+ name = re.findall("title=\"(.*?) @",l)[0]
+ self.info_label.emit(name)
+ #url = re.findall("title=\"(.*?) @",l)[0]
+ url = "https://github.com/" + re.findall("href=\"\/(.*?)\/tree",l)[0]
+ addondir = moddir + os.sep + name
+ #print ("found:",name," at ",url)
+ if not os.path.exists(addondir):
+ state = 0
+ else:
+ state = 1
+ repos.append([name,url,state])
+ # querying custom addons
+ customaddons = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons").GetString("CustomRepositories","").split("\n")
+ for url in customaddons:
+ if url:
+ name = url.split("/")[-1]
+ if name.lower().endswith(".git"):
+ name = name[:-4]
+ addondir = moddir + os.sep + name
+ if not os.path.exists(addondir):
+ state = 0
+ else:
+ state = 1
+ repos.append([name,url,state])
+ if not repos:
+ self.info_label.emit(translate("AddonsInstaller", "Unable to download addon list."))
+ else:
+ repos = sorted(repos, key=lambda s: s[0].lower())
+ for repo in repos:
+ self.addon_repo.emit(repo)
+ self.info_label.emit(translate("AddonsInstaller", "Workbenches list was updated."))
+ self.progressbar_show.emit(False)
+ self.done.emit()
+ self.stop = True
+
+
+class InfoWorker(QtCore.QThread):
+
+ "This worker retrieves the description text of a workbench"
+
+ addon_repos = QtCore.Signal(object)
+
+ def __init__(self):
+
+ QtCore.QThread.__init__(self)
+
+ def run(self):
+
+ i = 0
+ for repo in self.repos:
+ url = repo[1]
+ u = urlopen(url)
+ if not u:
+ self.stop = True
+ return
+ p = u.read()
+ if sys.version_info.major >= 3 and isinstance(p, bytes):
+ p = p.decode("utf-8")
+ u.close()
+ desc = re.findall("= 3 and isinstance(p, bytes):
+ p = p.decode("utf-8")
+ macros = re.findall('title="(Macro.*?)"', p)
+ macros = [mac for mac in macros if ('translated' not in mac)]
+ for mac in macros:
+ macname = mac[6:] # Remove "Macro ".
+ macname = macname.replace("&","&")
+ if (macname not in MACROS_BLACKLIST) and ('recipes' not in macname.lower()):
+ macro = Macro(macname)
+ macro.on_wiki = True
+ self.macros.append(macro)
+
+
+class ShowWorker(QtCore.QThread):
+
+ "This worker retrieves info of a given workbench"
+
+ info_label = QtCore.Signal(str)
+ addon_repos = QtCore.Signal(object)
+ progressbar_show = QtCore.Signal(bool)
+
+ def __init__(self, repos, idx):
+
+ QtCore.QThread.__init__(self)
+ self.repos = repos
+ self.idx = idx
+
+ def run(self):
+
+ self.progressbar_show.emit(True)
+ self.info_label.emit(translate("AddonsInstaller", "Retrieving description..."))
+ if len(self.repos[self.idx]) == 4:
+ desc = self.repos[self.idx][3]
+ else:
+ u = None
+ url = self.repos[self.idx][1]
+ self.info_label.emit(translate("AddonsInstaller", "Retrieving info from") + ' ' + str(url))
+ desc = ""
+ # get the README if possible
+ u = urlopen(url+"/blob/master/README.md")
+ if u:
+ p = u.read()
+ if sys.version_info.major >= 3 and isinstance(p, bytes):
+ p = p.decode("utf-8")
+ u.close()
+ readme = re.findall("(.*?)",p,flags=re.MULTILINE|re.DOTALL)
+ if readme:
+ desc += readme[0]
+ else:
+ # fall back to the description text
+ u = urlopen(url)
+ if not u:
+ self.progressbar_show.emit(False)
+ self.stop = True
+ return
+ p = u.read()
+ if sys.version_info.major >= 3 and isinstance(p, bytes):
+ p = p.decode("utf-8")
+ u.close()
+ desc = re.findall("" + translate("AddonsInstaller", "An update is available for this addon.") + " " + desc + '
Addon repository: ' + self.repos[self.idx][1] + ''
+
+ if self.repos[self.idx][0] in OBSOLETE:
+ message = " "+translate("AddonsInstaller","This add-on is marked as obsolete")+"
"+translate("AddonsInstaller","This usually means it is no longer maintained, and some more advanced add-on in this list provides the same functionality.")+"
" + message
+
+ self.info_label.emit( message )
+ self.progressbar_show.emit(False)
+ l = self.loadImages( message )
+ if l:
+ self.info_label.emit( l )
+ self.stop = True
+
+ def loadImages(self,message):
+
+ "checks if the given page contains images and downloads them"
+
+ # QTextBrowser cannot display online images. So we download them
+ # here, and replace the image link in the html code with the
+ # downloaded version
+
+ imagepaths = re.findall(" 300) or (img.height() > 300):
+ pix = QtGui.QPixmap()
+ pix = pix.fromImage(img.scaled(300,300,QtCore.Qt.KeepAspectRatio,QtCore.Qt.FastTransformation))
+ pix.save(storename, "jpeg",100)
+
+ message = message.replace(path,"file://"+storename.replace("\\","/"))
+ return message
+ return None
+
+
+
+class GetMacroDetailsWorker(QtCore.QThread):
+
+ """Retrieve the macro details for a macro"""
+
+ info_label = QtCore.Signal(str)
+ progressbar_show = QtCore.Signal(bool)
+
+ def __init__(self, macro):
+
+ QtCore.QThread.__init__(self)
+ self.macro = macro
+
+ def run(self):
+
+ self.progressbar_show.emit(True)
+ self.info_label.emit(translate("AddonsInstaller", "Retrieving description..."))
+ if not self.macro.parsed and self.macro.on_git:
+ self.info_label.emit(translate('AddonsInstaller', 'Retrieving info from git'))
+ self.macro.fill_details_from_file(self.macro.src_filename)
+ if not self.macro.parsed and self.macro.on_wiki:
+ self.info_label.emit(translate('AddonsInstaller', 'Retrieving info from wiki'))
+ mac = self.macro.name.replace(' ', '_')
+ mac = mac.replace('&', '%26')
+ mac = mac.replace('+', '%2B')
+ url = 'https://www.freecadweb.org/wiki/Macro_' + mac
+ self.macro.fill_details_from_wiki(url)
+ if self.macro.is_installed():
+ already_installed_msg = (''
+ + translate("AddonsInstaller", "This macro is already installed.")
+ + ' ')
+ else:
+ already_installed_msg = ''
+ message = (already_installed_msg
+ + "
"+self.macro.name+"
"
+ + self.macro.desc
+ + '
Macro location: '
+ + self.macro.url
+ + '')
+ self.info_label.emit(message)
+ self.progressbar_show.emit(False)
+ self.stop = True
+
+
+class InstallWorker(QtCore.QThread):
+
+ "This worker installs a workbench"
+
+ info_label = QtCore.Signal(str)
+ progressbar_show = QtCore.Signal(bool)
+ mark_recompute = QtCore.Signal(str)
+
+ def __init__(self, repos, idx):
+
+ QtCore.QThread.__init__(self)
+ self.idx = idx
+ self.repos = repos
+
+ def run(self):
+
+ "installs or updates the selected addon"
+
+ git = None
+ try:
+ import git
+ except Exception as e:
+ self.info_label.emit("GitPython not found.")
+ print(e)
+ FreeCAD.Console.PrintWarning(translate("AddonsInstaller","GitPython not found. Using standard download instead.")+"\n")
+ try:
+ import zipfile
+ except:
+ self.info_label.emit("no zip support.")
+ FreeCAD.Console.PrintError(translate("AddonsInstaller","Your version of python doesn't appear to support ZIP files. Unable to proceed.")+"\n")
+ return
+ try:
+ import StringIO as io
+ except ImportError: # StringIO is not available with python3
+ import io
+ if not isinstance(self.idx,list):
+ self.idx = [self.idx]
+ for idx in self.idx:
+ if idx < 0:
+ return
+ if not self.repos:
+ return
+ if NOGIT:
+ git = None
+ basedir = FreeCAD.getUserAppDataDir()
+ moddir = basedir + os.sep + "Mod"
+ if not os.path.exists(moddir):
+ os.makedirs(moddir)
+ clonedir = moddir + os.sep + self.repos[idx][0]
+ self.progressbar_show.emit(True)
+ if os.path.exists(clonedir):
+ self.info_label.emit("Updating module...")
+ if git:
+ if not os.path.exists(clonedir + os.sep + '.git'):
+ # Repair addon installed with raw download
+ bare_repo = git.Repo.clone_from(self.repos[idx][1], clonedir + os.sep + '.git', bare=True)
+ try:
+ with bare_repo.config_writer() as cw:
+ cw.set('core', 'bare', False)
+ except AttributeError:
+ FreeCAD.Console.PrintWarning(translate("AddonsInstaller", "Outdated GitPython detected, consider upgrading with pip.")+"\n")
+ cw = bare_repo.config_writer()
+ cw.set('core', 'bare', False)
+ del cw
+ repo = git.Repo(clonedir)
+ repo.head.reset('--hard')
+ repo = git.Git(clonedir)
+ try:
+ answer = repo.pull()
+ except:
+ print("Error updating module",repos[idx][1]," - Please fix manually")
+ answer = repo.status()
+ print(answer)
+ else:
+ # Update the submodules for this repository
+ repo_sms = git.Repo(clonedir)
+ for submodule in repo_sms.submodules:
+ submodule.update(init=True, recursive=True)
+ else:
+ answer = self.download(self.repos[idx][1],clonedir)
+ else:
+ self.info_label.emit("Checking module dependencies...")
+ depsok,answer = self.checkDependencies(self.repos[idx][1])
+ if depsok:
+ if git:
+ self.info_label.emit("Cloning module...")
+ repo = git.Repo.clone_from(self.repos[idx][1], clonedir, branch='master')
+
+ # Make sure to clone all the submodules as well
+ if repo.submodules:
+ repo.submodule_update(recursive=True)
+ else:
+ self.info_label.emit("Downloading module...")
+ self.download(self.repos[idx][1],clonedir)
+ answer = translate("AddonsInstaller", "Workbench successfully installed. Please restart FreeCAD to apply the changes.")
+ # symlink any macro contained in the module to the macros folder
+ macro_dir = FreeCAD.getUserMacroDir(True)
+ if not os.path.exists(macro_dir):
+ os.makedirs(macro_dir)
+ if os.path.exists(clonedir):
+ for f in os.listdir(clonedir):
+ if f.lower().endswith(".fcmacro"):
+ print("copying macro:",f)
+ symlink(os.path.join(clonedir, f), os.path.join(macro_dir, f))
+ FreeCAD.ParamGet('User parameter:Plugins/'+self.repos[idx][0]).SetString("destination",clonedir)
+ answer += "\n\n"+translate("AddonsInstaller", "A macro has been installed and is available the Macros menu") + ": "
+ answer += f + ""
+ self.progressbar_show.emit(False)
+ self.info_label.emit(answer)
+ self.mark_recompute.emit(self.repos[idx][0])
+ self.stop = True
+
+ def checkDependencies(self,baseurl):
+
+ "checks if the repo contains a metadata.txt and check its contents"
+
+ import FreeCADGui
+ ok = True
+ message = ""
+ depsurl = baseurl.replace("github.com","raw.githubusercontent.com")
+ if not depsurl.endswith("/"):
+ depsurl += "/"
+ depsurl += "master/metadata.txt"
+ try:
+ mu = urlopen(depsurl)
+ except:
+ # no metadata.txt, we just continue without deps checking
+ pass
+ else:
+ # metadata.txt found
+ depsfile = mu.read()
+ mu.close()
+
+ # urllib2 gives us a bytelike object instead of a string. Have to consider that
+ try:
+ depsfile = depsfile.decode('utf-8')
+ except AttributeError:
+ pass
+
+ deps = depsfile.split("\n")
+ for l in deps:
+ if l.startswith("workbenches="):
+ depswb = l.split("=")[1].split(",")
+ for wb in depswb:
+ if wb.strip():
+ if not wb.strip() in FreeCADGui.listWorkbenches().keys():
+ if not wb.strip()+"Workbench" in FreeCADGui.listWorkbenches().keys():
+ ok = False
+ message += translate("AddonsInstaller","Missing workbench") + ": " + wb + ", "
+ elif l.startswith("pylibs="):
+ depspy = l.split("=")[1].split(",")
+ for pl in depspy:
+ if pl.strip():
+ try:
+ __import__(pl.strip())
+ except:
+ ok = False
+ message += translate("AddonsInstaller","Missing python module") +": " + pl + ", "
+ elif l.startswith("optionalpylibs="):
+ opspy = l.split("=")[1].split(",")
+ for pl in opspy:
+ if pl.strip():
+ try:
+ __import__(pl.strip())
+ except:
+ message += translate("AddonsInstaller","Missing optional python module (doesn't prevent installing)") +": " + pl + ", "
+ if message and (not ok):
+ message = translate("AddonsInstaller", "Some errors were found that prevent to install this workbench") + ": " + message + ". "
+ message += translate("AddonsInstaller","Please install the missing components first.")
+ return ok, message
+
+ def download(self,giturl,clonedir):
+
+ "downloads and unzip from github"
+
+ import zipfile
+ bakdir = None
+ if os.path.exists(clonedir):
+ bakdir = clonedir+".bak"
+ if os.path.exists(bakdir):
+ shutil.rmtree(bakdir)
+ os.rename(clonedir,bakdir)
+ os.makedirs(clonedir)
+ zipurl = giturl+"/archive/master.zip"
+ try:
+ print("Downloading "+zipurl)
+ u = urlopen(zipurl)
+ except:
+ return translate("AddonsInstaller", "Error: Unable to download") + " " + zipurl
+ if sys.version_info.major < 3:
+ import StringIO as io
+ _stringio = io.StringIO
+ else:
+ import io
+ _stringio = io.BytesIO
+ zfile = _stringio()
+ zfile.write(u.read())
+ zfile = zipfile.ZipFile(zfile)
+ master = zfile.namelist()[0] # github will put everything in a subfolder
+ zfile.extractall(clonedir)
+ u.close()
+ zfile.close()
+ for filename in os.listdir(clonedir+os.sep+master):
+ shutil.move(clonedir+os.sep+master+os.sep+filename, clonedir+os.sep+filename)
+ os.rmdir(clonedir+os.sep+master)
+ if bakdir:
+ shutil.rmtree(bakdir)
+ return translate("AddonsInstaller", "Successfully installed") + " " + zipurl
+