AddonManager: New UI
This commit is contained in:
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 19 KiB |
File diff suppressed because it is too large
Load Diff
160
src/Mod/AddonManager/AddonManager.ui
Normal file
160
src/Mod/AddonManager/AddonManager.ui
Normal file
@@ -0,0 +1,160 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Dialog</class>
|
||||
<widget class="QDialog" name="Dialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>599</width>
|
||||
<height>480</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Addons manager</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<widget class="QSplitter" name="splitter">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<widget class="QWidget" name="verticalLayoutWidget">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QTabWidget" name="tabWidget">
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="tab">
|
||||
<attribute name="title">
|
||||
<string>Workbenches</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||
<item>
|
||||
<widget class="QListWidget" name="listWorkbenches"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_2">
|
||||
<attribute name="title">
|
||||
<string>Macros</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_6">
|
||||
<item>
|
||||
<widget class="QListWidget" name="listMacros"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonExecute">
|
||||
<property name="toolTip">
|
||||
<string>Executes the selected macro, if installed</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Execute</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="verticalLayoutWidget_2">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonConfigure">
|
||||
<property name="toolTip">
|
||||
<string>Sets configuration options for the Addon Manager</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Configure...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTextBrowser" name="description"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QProgressBar" name="progressBar">
|
||||
<property name="value">
|
||||
<number>24</number>
|
||||
</property>
|
||||
<property name="format">
|
||||
<string>Downloading info...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetDefaultConstraint</enum>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonUninstall">
|
||||
<property name="toolTip">
|
||||
<string>Uninstalls a selected macro or workbench</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Uninstall selected</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonInstall">
|
||||
<property name="toolTip">
|
||||
<string>Installs or updates the selected macro or workbench</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Install/update selected</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonUpdateAll">
|
||||
<property name="toolTip">
|
||||
<string>Download and apply all available updates</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Update all</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -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'}
|
||||
88
src/Mod/AddonManager/AddonManagerOptions.ui
Normal file
88
src/Mod/AddonManager/AddonManagerOptions.ui
Normal file
@@ -0,0 +1,88 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Dialog</class>
|
||||
<widget class="QDialog" name="Dialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>390</width>
|
||||
<height>183</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Addon manager options</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkUpdates">
|
||||
<property name="toolTip">
|
||||
<string>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)</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Automatically check for updates at start (requires python-git)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Custom repositories (one per line):</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTextEdit" name="customRepositories">
|
||||
<property name="toolTip">
|
||||
<string>You can use this window to specify additional addon repositories to be scanned for available addons</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>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>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>
|
||||
@@ -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})
|
||||
|
||||
@@ -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())
|
||||
|
||||
337
src/Mod/AddonManager/Resources/translations/AddonManager.ts
Normal file
337
src/Mod/AddonManager/Resources/translations/AddonManager.ts
Normal file
@@ -0,0 +1,337 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE TS><TS version="2.0">
|
||||
<context>
|
||||
<name>AddonsInstaller</name>
|
||||
<message>
|
||||
<location filename="../../addonmanager_macro.py" line="144"/>
|
||||
<source>Unable to fetch the code of this macro.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../addonmanager_macro.py" line="163"/>
|
||||
<source>Unable to retrieve a description for this macro.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../AddonManager.py" line="234"/>
|
||||
<source>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!</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../AddonManager.py" line="329"/>
|
||||
<source>Addon manager</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../AddonManager.py" line="331"/>
|
||||
<source>You must restart FreeCAD for changes to take effect. Press Ok to restart FreeCAD now, or Cancel to restart later.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../AddonManager.py" line="379"/>
|
||||
<source>Checking for updates...</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../AddonManager.py" line="398"/>
|
||||
<source>Apply</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../AddonManager.py" line="398"/>
|
||||
<source>update(s)</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../AddonManager.py" line="401"/>
|
||||
<source>No update available</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../AddonManager.py" line="542"/>
|
||||
<source>Macro successfully installed. The macro is now available from the Macros dialog.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../AddonManager.py" line="544"/>
|
||||
<source>Unable to install</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../AddonManager.py" line="608"/>
|
||||
<source>Addon successfully removed. Please restart FreeCAD</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../AddonManager.py" line="610"/>
|
||||
<source>Unable to remove this addon</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../AddonManager.py" line="615"/>
|
||||
<source>Macro successfully removed.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../AddonManager.py" line="617"/>
|
||||
<source>Macro could not be removed.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../addonmanager_workers.py" line="113"/>
|
||||
<source>Unable to download addon list.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../addonmanager_workers.py" line="118"/>
|
||||
<source>Workbenches list was updated.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../addonmanager_workers.py" line="545"/>
|
||||
<source>Outdated GitPython detected, consider upgrading with pip.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../addonmanager_workers.py" line="235"/>
|
||||
<source>List of macros successfully retrieved.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../addonmanager_workers.py" line="454"/>
|
||||
<source>Retrieving description...</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../addonmanager_workers.py" line="317"/>
|
||||
<source>Retrieving info from</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../addonmanager_workers.py" line="378"/>
|
||||
<source>An update is available for this addon.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../addonmanager_workers.py" line="380"/>
|
||||
<source>This addon is already installed.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../addonmanager_workers.py" line="385"/>
|
||||
<source>This add-on is marked as obsolete</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../addonmanager_workers.py" line="385"/>
|
||||
<source>This usually means it is no longer maintained, and some more advanced add-on in this list provides the same functionality.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../addonmanager_workers.py" line="456"/>
|
||||
<source>Retrieving info from git</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../addonmanager_workers.py" line="459"/>
|
||||
<source>Retrieving info from wiki</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../addonmanager_workers.py" line="509"/>
|
||||
<source>GitPython not found. Using standard download instead.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../addonmanager_workers.py" line="514"/>
|
||||
<source>Your version of python doesn't appear to support ZIP files. Unable to proceed.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../addonmanager_workers.py" line="579"/>
|
||||
<source>Workbench successfully installed. Please restart FreeCAD to apply the changes.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../addonmanager_workers.py" line="590"/>
|
||||
<source>A macro has been installed and is available the Macros menu</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../addonmanager_workers.py" line="634"/>
|
||||
<source>Missing workbench</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../addonmanager_workers.py" line="643"/>
|
||||
<source>Missing python module</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../addonmanager_workers.py" line="651"/>
|
||||
<source>Missing optional python module (doesn't prevent installing)</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../addonmanager_workers.py" line="653"/>
|
||||
<source>Some errors were found that prevent to install this workbench</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../addonmanager_workers.py" line="654"/>
|
||||
<source>Please install the missing components first.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../addonmanager_workers.py" line="674"/>
|
||||
<source>Error: Unable to download</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../addonmanager_workers.py" line="687"/>
|
||||
<source>Successfully installed</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../addonmanager_workers.py" line="251"/>
|
||||
<source>GitPython not installed! Cannot retrieve macros from git</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../addonmanager_macro.py" line="156"/>
|
||||
<source>Unable to clean macro code</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../AddonManager.py" line="687"/>
|
||||
<source>Installed</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../AddonManager.py" line="699"/>
|
||||
<source>Update available</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../AddonManager.py" line="658"/>
|
||||
<source>Restart required</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../addonmanager_workers.py" line="466"/>
|
||||
<source>This macro is already installed.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>Dialog</name>
|
||||
<message>
|
||||
<location filename="../../AddonManager.ui" line="14"/>
|
||||
<source>Addons manager</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../AddonManager.ui" line="37"/>
|
||||
<source>Workbenches</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../AddonManager.ui" line="47"/>
|
||||
<source>Macros</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../AddonManager.ui" line="59"/>
|
||||
<source>Execute</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../AddonManager.ui" line="113"/>
|
||||
<source>Downloading info...</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../AddonManager.ui" line="150"/>
|
||||
<source>Update all</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../AddonManager.ui" line="56"/>
|
||||
<source>Executes the selected macro, if installed</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../AddonManager.ui" line="127"/>
|
||||
<source>Uninstalls a selected macro or workbench</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../AddonManager.ui" line="137"/>
|
||||
<source>Installs or updates the selected macro or workbench</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../AddonManager.ui" line="147"/>
|
||||
<source>Download and apply all available updates</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../AddonManagerOptions.ui" line="20"/>
|
||||
<source>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)</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../AddonManagerOptions.ui" line="23"/>
|
||||
<source>Automatically check for updates at start (requires python-git)</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../AddonManagerOptions.ui" line="30"/>
|
||||
<source>Custom repositories (one per line):</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../AddonManagerOptions.ui" line="37"/>
|
||||
<source>You can use this window to specify additional addon repositories to be scanned for available addons</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../AddonManager.ui" line="89"/>
|
||||
<source>Sets configuration options for the Addon Manager</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../AddonManager.ui" line="92"/>
|
||||
<source>Configure...</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../AddonManagerOptions.ui" line="14"/>
|
||||
<source>Addon manager options</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../AddonManager.ui" line="130"/>
|
||||
<source>Uninstall selected</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../AddonManager.ui" line="140"/>
|
||||
<source>Install/update selected</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>Std_AddonMgr</name>
|
||||
<message>
|
||||
<location filename="../../AddonManager.py" line="223"/>
|
||||
<source>&Addon manager</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../AddonManager.py" line="224"/>
|
||||
<source>Manage external workbenches and macros</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
@@ -27,7 +27,6 @@ import sys
|
||||
import FreeCAD
|
||||
|
||||
from addonmanager_utilities import translate
|
||||
from addonmanager_utilities import urllib2
|
||||
from addonmanager_utilities import urlopen
|
||||
|
||||
|
||||
@@ -95,7 +94,8 @@ class Macro(object):
|
||||
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 @@ class Macro(object):
|
||||
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 @@ class Macro(object):
|
||||
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 class=\"ctEven left macro-description\">(.*?)<\/td>", p.replace('\n', ' '))
|
||||
|
||||
@@ -1,22 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#***************************************************************************
|
||||
#* *
|
||||
#* Copyright (c) 2018 Gaël Écorchard <galou_breizh@yahoo.fr> *
|
||||
#* *
|
||||
#* 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 @@ else:
|
||||
pass
|
||||
|
||||
|
||||
def urlopen(url):
|
||||
"""Opens an url with urllib2"""
|
||||
if ssl_ctx:
|
||||
u = urllib2.urlopen(url, context=ssl_ctx)
|
||||
|
||||
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:
|
||||
u = urllib2.urlopen(url)
|
||||
return u
|
||||
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 sys.version_info.major < 3:
|
||||
import urllib2
|
||||
else:
|
||||
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)
|
||||
|
||||
|
||||
688
src/Mod/AddonManager/addonmanager_workers.py
Normal file
688
src/Mod/AddonManager/addonmanager_workers.py
Normal file
@@ -0,0 +1,688 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#***************************************************************************
|
||||
#* *
|
||||
#* Copyright (c) 2019 Yorik van Havre <yorik@uncreated.net> *
|
||||
#* *
|
||||
#* 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("<meta property=\"og:description\" content=\"(.*?)\"",p)
|
||||
if desc:
|
||||
desc = desc[0]
|
||||
else:
|
||||
desc = "Unable to retrieve addon description"
|
||||
self.repos[i].append(desc)
|
||||
i += 1
|
||||
self.addon_repos.emit(self.repos)
|
||||
self.stop = True
|
||||
|
||||
|
||||
class CheckWBWorker(QtCore.QThread):
|
||||
|
||||
"This worker checks for available updates for all workbenches"
|
||||
|
||||
enable = QtCore.Signal(int)
|
||||
mark = QtCore.Signal(str)
|
||||
|
||||
def __init__(self,repos):
|
||||
|
||||
QtCore.QThread.__init__(self)
|
||||
self.repos = repos
|
||||
|
||||
def run(self):
|
||||
|
||||
if NOGIT:
|
||||
self.stop = True
|
||||
return
|
||||
try:
|
||||
import git
|
||||
except:
|
||||
self.stop = True
|
||||
return
|
||||
basedir = FreeCAD.getUserAppDataDir()
|
||||
moddir = basedir + os.sep + "Mod"
|
||||
upds = []
|
||||
gitpython_warning = False
|
||||
for repo in self.repos:
|
||||
if repo[2] == 1: #installed
|
||||
#print("Checking for updates for",repo[0])
|
||||
clonedir = moddir + 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:
|
||||
if not gitpython_warning:
|
||||
FreeCAD.Console.PrintWarning(translate("AddonsInstaller", "Outdated GitPython detected, consider upgrading with pip.")+"\n")
|
||||
gitpython_warning = True
|
||||
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():
|
||||
self.mark.emit(repo[0])
|
||||
upds.append(repo[0])
|
||||
self.enable.emit(len(upds))
|
||||
self.stop = True
|
||||
|
||||
|
||||
class FillMacroListWorker(QtCore.QThread):
|
||||
|
||||
"This worker opulates the list of macros"
|
||||
|
||||
add_macro_signal = QtCore.Signal(Macro)
|
||||
info_label_signal = QtCore.Signal(str)
|
||||
progressbar_show = QtCore.Signal(bool)
|
||||
|
||||
def __init__(self, repo_dir):
|
||||
|
||||
QtCore.QThread.__init__(self)
|
||||
self.repo_dir = repo_dir
|
||||
self.macros = []
|
||||
|
||||
def run(self):
|
||||
|
||||
"""Populates the list of macros"""
|
||||
|
||||
self.retrieve_macros_from_git()
|
||||
self.retrieve_macros_from_wiki()
|
||||
[self.add_macro_signal.emit(m) for m in sorted(self.macros, key=lambda m: m.name.lower())]
|
||||
if self.macros:
|
||||
self.info_label_signal.emit(translate('AddonsInstaller', 'List of macros successfully retrieved.'))
|
||||
self.progressbar_show.emit(False)
|
||||
self.stop = True
|
||||
|
||||
def retrieve_macros_from_git(self):
|
||||
|
||||
"""Retrieve macros from FreeCAD-macros.git
|
||||
|
||||
Emits a signal for each macro in
|
||||
https://github.com/FreeCAD/FreeCAD-macros.git
|
||||
"""
|
||||
|
||||
try:
|
||||
import git
|
||||
except ImportError:
|
||||
self.info_label_signal.emit("GitPython not installed! Cannot retrieve macros from Git")
|
||||
FreeCAD.Console.PrintWarning(translate('AddonsInstaller', 'GitPython not installed! Cannot retrieve macros from git')+"\n")
|
||||
return
|
||||
|
||||
self.info_label_signal.emit('Downloading list of macros from git...')
|
||||
git.Repo.clone_from('https://github.com/FreeCAD/FreeCAD-macros.git', self.repo_dir)
|
||||
for dirpath, _, filenames in os.walk(self.repo_dir):
|
||||
if '.git' in dirpath:
|
||||
continue
|
||||
for filename in filenames:
|
||||
if filename.lower().endswith('.fcmacro'):
|
||||
macro = Macro(filename[:-8]) # Remove ".FCMacro".
|
||||
macro.on_git = True
|
||||
macro.src_filename = os.path.join(dirpath, filename)
|
||||
self.macros.append(macro)
|
||||
|
||||
def retrieve_macros_from_wiki(self):
|
||||
|
||||
"""Retrieve macros from the wiki
|
||||
|
||||
Read the wiki and emit a signal for each found macro.
|
||||
Reads only the page https://www.freecadweb.org/wiki/Macros_recipes.
|
||||
"""
|
||||
|
||||
self.info_label_signal.emit("Downloading list of macros from the FreeCAD wiki...")
|
||||
self.progressbar_show.emit(True)
|
||||
u = urlopen("https://www.freecadweb.org/wiki/Macros_recipes")
|
||||
if not u:
|
||||
return
|
||||
p = u.read()
|
||||
u.close()
|
||||
if sys.version_info.major >= 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("<article.*?>(.*?)</article>",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("<meta property=\"og:description\" content=\"(.*?)\"",p)
|
||||
if desc:
|
||||
desc = desc[0]
|
||||
if not desc:
|
||||
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 = "<strong style=\"background: #B65A00;\">" + translate("AddonsInstaller", "An update is available for this addon.") + "</strong><br>" + desc + '<br/><br/>Addon repository: <a href="' + self.repos[self.idx][1] + '">' + self.repos[self.idx][1] + '</a>'
|
||||
else:
|
||||
message = "<strong style=\"background: #00B629;\">" + translate("AddonsInstaller", "This addon is already installed.") + "</strong><br>" + desc + '<br/><br/>Addon repository: <a href="' + self.repos[self.idx][1] + '">' + self.repos[self.idx][1] + '</a>'
|
||||
else:
|
||||
message = desc + '<br/><br/>Addon repository: <a href="' + self.repos[self.idx][1] + '">' + self.repos[self.idx][1] + '</a>'
|
||||
|
||||
if self.repos[self.idx][0] in OBSOLETE:
|
||||
message = " <strong style=\"background: #FF0000;\">"+translate("AddonsInstaller","This add-on is marked as obsolete")+"</strong><br/><br/>"+translate("AddonsInstaller","This usually means it is no longer maintained, and some more advanced add-on in this list provides the same functionality.")+"<br/><br/>" + 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("<img.*?src=\"(.*?)\"",message)
|
||||
if imagepaths:
|
||||
storedimages = []
|
||||
store = os.path.join(FreeCAD.getUserAppDataDir(),"AddonManager","Images")
|
||||
if not os.path.exists(store):
|
||||
os.makedirs(store)
|
||||
for path in imagepaths:
|
||||
name = path.split("/")[-1]
|
||||
if name and path.startswith("http"):
|
||||
storename = os.path.join(store,name)
|
||||
if not os.path.exists(storename):
|
||||
try:
|
||||
u = urlopen(path)
|
||||
imagedata = u.read()
|
||||
u.close()
|
||||
except:
|
||||
print("AddonManager: Debug: Error retrieving image from",path)
|
||||
return None
|
||||
else:
|
||||
f = open(storename,"wb")
|
||||
f.write(imagedata)
|
||||
f.close()
|
||||
|
||||
# resize the image to 300x300px if needed
|
||||
from PySide import QtCore,QtGui
|
||||
img = QtGui.QImage(storename)
|
||||
if (img.width() > 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 = ('<strong style=\"background: #00B629;\">'
|
||||
+ translate("AddonsInstaller", "This macro is already installed.")
|
||||
+ '</strong><br>')
|
||||
else:
|
||||
already_installed_msg = ''
|
||||
message = (already_installed_msg
|
||||
+ "<h1>"+self.macro.name+"</h1>"
|
||||
+ self.macro.desc
|
||||
+ '<br/><br/>Macro location: <a href="'
|
||||
+ self.macro.url
|
||||
+ '">'
|
||||
+ self.macro.url
|
||||
+ '</a>')
|
||||
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") + ": <b>"
|
||||
answer += f + "</b>"
|
||||
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") + ": <b>" + message + "</b>. "
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user