Merge branch 'master' into feature-backup-manager

This commit is contained in:
ChrisTerBeke 2018-05-08 17:27:54 +02:00
commit c90a958eaf
10 changed files with 1295 additions and 103 deletions

View File

@ -15,12 +15,10 @@ from UM.Logger import Logger
from UM.Resources import Resources from UM.Resources import Resources
from UM.Version import Version from UM.Version import Version
class CuraPackageManager(QObject): class CuraPackageManager(QObject):
Version = 1 Version = 1
# The prefix that's added to all files for an installed package to avoid naming conflicts with user created # The prefix that's added to all files for an installed package to avoid naming conflicts with user created files.
# files.
PREFIX_PLACE_HOLDER = "-CP;" PREFIX_PLACE_HOLDER = "-CP;"
def __init__(self, parent = None): def __init__(self, parent = None):
@ -31,13 +29,23 @@ class CuraPackageManager(QObject):
self._plugin_registry = self._application.getPluginRegistry() self._plugin_registry = self._application.getPluginRegistry()
# JSON file that keeps track of all installed packages. # JSON file that keeps track of all installed packages.
self._package_management_file_path = os.path.join(os.path.abspath(Resources.getDataStoragePath()), self._bundled_package_management_file_path = os.path.join(
"packages.json") os.path.dirname(os.path.abspath(__file__)),
self._installed_package_dict = {} # a dict of all installed packages "..",
self._to_remove_package_set = set() # a set of packages that need to be removed at the next start "resources",
self._to_install_package_dict = {} # a dict of packages that need to be installed at the next start "packages.json"
)
self._user_package_management_file_path = os.path.join(
os.path.abspath(Resources.getDataStoragePath()),
"packages.json"
)
installedPackagesChanged = pyqtSignal() # Emitted whenever the installed packages collection have been changed. self._bundled_package_dict = {} # A dict of all bundled packages
self._installed_package_dict = {} # A dict of all installed packages
self._to_remove_package_set = set() # A set of packages that need to be removed at the next start
self._to_install_package_dict = {} # A dict of packages that need to be installed at the next start
installedPackagesChanged = pyqtSignal() # Emitted whenever the installed packages collection have been changed.
def initialize(self): def initialize(self):
self._loadManagementData() self._loadManagementData()
@ -46,34 +54,42 @@ class CuraPackageManager(QObject):
# (for initialize) Loads the package management file if exists # (for initialize) Loads the package management file if exists
def _loadManagementData(self) -> None: def _loadManagementData(self) -> None:
if not os.path.exists(self._package_management_file_path): if not os.path.exists(self._bundled_package_management_file_path):
Logger.log("i", "Package management file %s doesn't exist, do nothing", self._package_management_file_path) Logger.log("w", "Bundled package management file could not be found!")
return
if not os.path.exists(self._user_package_management_file_path):
Logger.log("i", "User package management file %s doesn't exist, do nothing", self._user_package_management_file_path)
return return
# Need to use the file lock here to prevent concurrent I/O from other processes/threads # Need to use the file lock here to prevent concurrent I/O from other processes/threads
container_registry = self._application.getContainerRegistry() container_registry = self._application.getContainerRegistry()
with container_registry.lockFile(): with container_registry.lockFile():
with open(self._package_management_file_path, "r", encoding = "utf-8") as f:
management_dict = json.load(f, encoding = "utf-8")
# Load the bundled packages:
with open(self._bundled_package_management_file_path, "r", encoding = "utf-8") as f:
self._bundled_package_dict = json.load(f, encoding = "utf-8")
Logger.log("i", "Loaded bundled packages data from %s", self._bundled_package_management_file_path)
# Load the user packages:
with open(self._user_package_management_file_path, "r", encoding="utf-8") as f:
management_dict = json.load(f, encoding="utf-8")
self._installed_package_dict = management_dict.get("installed", {}) self._installed_package_dict = management_dict.get("installed", {})
self._to_remove_package_set = set(management_dict.get("to_remove", [])) self._to_remove_package_set = set(management_dict.get("to_remove", []))
self._to_install_package_dict = management_dict.get("to_install", {}) self._to_install_package_dict = management_dict.get("to_install", {})
Logger.log("i", "Loaded user packages management file from %s", self._user_package_management_file_path)
Logger.log("i", "Package management file %s is loaded", self._package_management_file_path)
def _saveManagementData(self) -> None: def _saveManagementData(self) -> None:
# Need to use the file lock here to prevent concurrent I/O from other processes/threads # Need to use the file lock here to prevent concurrent I/O from other processes/threads
container_registry = self._application.getContainerRegistry() container_registry = self._application.getContainerRegistry()
with container_registry.lockFile(): with container_registry.lockFile():
with open(self._package_management_file_path, "w", encoding = "utf-8") as f: with open(self._user_package_management_file_path, "w", encoding = "utf-8") as f:
data_dict = {"version": CuraPackageManager.Version, data_dict = {"version": CuraPackageManager.Version,
"installed": self._installed_package_dict, "installed": self._installed_package_dict,
"to_remove": list(self._to_remove_package_set), "to_remove": list(self._to_remove_package_set),
"to_install": self._to_install_package_dict} "to_install": self._to_install_package_dict}
data_dict["to_remove"] = list(data_dict["to_remove"]) data_dict["to_remove"] = list(data_dict["to_remove"])
json.dump(data_dict, f) json.dump(data_dict, f, sort_keys = True, indent = 4)
Logger.log("i", "Package management file %s is saved", self._package_management_file_path) Logger.log("i", "Package management file %s was saved", self._user_package_management_file_path)
# (for initialize) Removes all packages that have been scheduled to be removed. # (for initialize) Removes all packages that have been scheduled to be removed.
def _removeAllScheduledPackages(self) -> None: def _removeAllScheduledPackages(self) -> None:
@ -84,10 +100,13 @@ class CuraPackageManager(QObject):
# (for initialize) Installs all packages that have been scheduled to be installed. # (for initialize) Installs all packages that have been scheduled to be installed.
def _installAllScheduledPackages(self) -> None: def _installAllScheduledPackages(self) -> None:
for package_id, installation_package_data in self._to_install_package_dict.items():
self._installPackage(installation_package_data) while self._to_install_package_dict:
self._to_install_package_dict.clear() package_id, package_info = list(self._to_install_package_dict.items())[0]
self._saveManagementData() self._installPackage(package_info)
self._installed_package_dict[package_id] = self._to_install_package_dict[package_id]
del self._to_install_package_dict[package_id]
self._saveManagementData()
# Checks the given package is installed. If so, return a dictionary that contains the package's information. # Checks the given package is installed. If so, return a dictionary that contains the package's information.
def getInstalledPackageInfo(self, package_id: str) -> Optional[dict]: def getInstalledPackageInfo(self, package_id: str) -> Optional[dict]:
@ -99,64 +118,66 @@ class CuraPackageManager(QObject):
return package_info return package_info
if package_id in self._installed_package_dict: if package_id in self._installed_package_dict:
package_info = self._installed_package_dict.get(package_id) package_info = self._installed_package_dict[package_id]["package_info"]
return package_info return package_info
for section, packages in self.getAllInstalledPackagesInfo().items(): if package_id in self._bundled_package_dict:
for package in packages: package_info = self._bundled_package_dict[package_id]["package_info"]
if package["package_id"] == package_id: return package_info
return package
return None return None
def getAllInstalledPackagesInfo(self) -> dict: def getAllInstalledPackagesInfo(self) -> dict:
installed_package_id_set = set(self._installed_package_dict.keys()) | set(self._to_install_package_dict.keys())
installed_package_id_set = installed_package_id_set.difference(self._to_remove_package_set)
managed_package_id_set = installed_package_id_set | self._to_remove_package_set # Add bundled, installed, and to-install packages to the set of installed package IDs
all_installed_ids = set()
# TODO: For absolutely no reason, this function seems to run in a loop if self._bundled_package_dict.keys():
# even though no loop is ever called with it. all_installed_ids = all_installed_ids.union(set(self._bundled_package_dict.keys()))
if self._installed_package_dict.keys():
all_installed_ids = all_installed_ids.union(set(self._installed_package_dict.keys()))
if self._to_install_package_dict.keys():
all_installed_ids = all_installed_ids.union(set(self._to_install_package_dict.keys()))
all_installed_ids = all_installed_ids.difference(self._to_remove_package_set)
# map of <package_type> -> <package_id> -> <package_info> # map of <package_type> -> <package_id> -> <package_info>
installed_packages_dict = {} installed_packages_dict = {}
for package_id in installed_package_id_set: for package_id in all_installed_ids:
# Skip required plugins as they should not be tampered with
if package_id in Application.getInstance().getRequiredPlugins(): if package_id in Application.getInstance().getRequiredPlugins():
continue continue
# Add bundled plugins
if package_id in self._bundled_package_dict:
package_info = self._bundled_package_dict[package_id]["package_info"]
# Add installed plugins
if package_id in self._installed_package_dict:
package_info = self._installed_package_dict[package_id]["package_info"]
# Add to install plugins
if package_id in self._to_install_package_dict: if package_id in self._to_install_package_dict:
package_info = self._to_install_package_dict[package_id]["package_info"] package_info = self._to_install_package_dict[package_id]["package_info"]
else:
package_info = self._installed_package_dict[package_id]
package_info["is_bundled"] = False
package_type = package_info["package_type"]
if package_type not in installed_packages_dict:
installed_packages_dict[package_type] = []
installed_packages_dict[package_type].append( package_info )
# We also need to get information from the plugin registry such as if a plugin is active # We also need to get information from the plugin registry such as if a plugin is active
package_info["is_active"] = self._plugin_registry.isActivePlugin(package_id) if package_info["package_type"] == "plugin":
package_info["is_active"] = self._plugin_registry.isActivePlugin(package_id)
else:
package_info["is_active"] = self._plugin_registry.isActivePlugin(package_id)
# Also get all bundled plugins # If the package ID is in bundled, label it as such
all_metadata = self._plugin_registry.getAllMetaData() if package_info["package_id"] in self._bundled_package_dict.keys():
for item in all_metadata: package_info["is_bundled"] = True
if item == {}: else:
continue package_info["is_bundled"] = False
plugin_package_info = self.__convertPluginMetadataToPackageMetadata(item) # If there is not a section in the dict for this type, add it
# Only gather the bundled plugins here. if package_info["package_type"] not in installed_packages_dict:
package_id = plugin_package_info["package_id"] installed_packages_dict[package_info["package_type"]] = []
if package_id in managed_package_id_set:
continue # Finally, add the data
if package_id in Application.getInstance().getRequiredPlugins(): installed_packages_dict[package_info["package_type"]].append( package_info )
continue
plugin_package_info["is_bundled"] = True if plugin_package_info["author"]["display_name"] == "Ultimaker B.V." else False
plugin_package_info["is_active"] = self._plugin_registry.isActivePlugin(package_id)
package_type = "plugin"
if package_type not in installed_packages_dict:
installed_packages_dict[package_type] = []
installed_packages_dict[package_type].append( plugin_package_info )
return installed_packages_dict return installed_packages_dict
@ -176,7 +197,7 @@ class CuraPackageManager(QObject):
"email": "", "email": "",
"website": "", "website": "",
}, },
"tags": ["plugin"], "tags": ["plugin"]
} }
return package_metadata return package_metadata

View File

@ -2,7 +2,7 @@
"name": "Machine Settings action", "name": "Machine Settings action",
"author": "fieldOfView", "author": "fieldOfView",
"version": "1.0.0", "version": "1.0.0",
"description": "Provides a way to change machine settings (such as build volume, nozzle size, etc)", "description": "Provides a way to change machine settings (such as build volume, nozzle size, etc.).",
"api": 4, "api": 4,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -0,0 +1,29 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
ButtonStyle
{
background: Rectangle
{
implicitWidth: UM.Theme.getSize("toolbox_action_button").width
implicitHeight: UM.Theme.getSize("toolbox_action_button").height
color: "transparent"
border
{
width: UM.Theme.getSize("default_lining").width
color: UM.Theme.getColor("lining")
}
}
label: Label
{
text: control.text
color: UM.Theme.getColor("text")
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
}

View File

@ -26,7 +26,7 @@ Item
Column Column
{ {
id: pluginInfo id: pluginInfo
property var color: isEnabled ? UM.Theme.getColor("text") : UM.Theme.getColor("lining") property var color: model.package_type === "plugin" && !isEnabled ? UM.Theme.getColor("lining") : UM.Theme.getColor("text")
height: parent.height height: parent.height
anchors anchors
{ {
@ -49,12 +49,11 @@ Item
Text Text
{ {
text: model.description text: model.description
maximumLineCount: 3
elide: Text.ElideRight
width: parent.width width: parent.width
height: UM.Theme.getSize("toolbox_property_label").height
clip: true
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
color: pluginInfo.color color: pluginInfo.color
elide: Text.ElideRight
} }
} }
Column Column
@ -104,19 +103,11 @@ Item
right: parent.right right: parent.right
topMargin: UM.Theme.getSize("default_margin").height topMargin: UM.Theme.getSize("default_margin").height
} }
Button { Button
id: removeButton {
text: id: disableButton
{ text: isEnabled ? catalog.i18nc("@action:button", "Disable") : catalog.i18nc("@action:button", "Enable")
if (model.is_bundled) visible: model.type == "plugin"
{
return isEnabled ? catalog.i18nc("@action:button", "Disable") : catalog.i18nc("@action:button", "Enable")
}
else
{
return catalog.i18nc("@action:button", "Uninstall")
}
}
enabled: !toolbox.isDownloading enabled: !toolbox.isDownloading
style: ButtonStyle style: ButtonStyle
{ {
@ -139,24 +130,36 @@ Item
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
} }
} }
onClicked: onClicked: toolbox.isEnabled(model.id) ? toolbox.disable(model.id) : toolbox.enable(model.id)
}
Button
{
id: removeButton
text: catalog.i18nc("@action:button", "Uninstall")
visible: !model.is_bundled
enabled: !toolbox.isDownloading
style: ButtonStyle
{ {
if (model.is_bundled) background: Rectangle
{ {
if (toolbox.isEnabled(model.id)) implicitWidth: UM.Theme.getSize("toolbox_action_button").width
implicitHeight: UM.Theme.getSize("toolbox_action_button").height
color: "transparent"
border
{ {
toolbox.disable(model.id) width: UM.Theme.getSize("default_lining").width
} color: UM.Theme.getColor("lining")
else
{
toolbox.enable(model.id)
} }
} }
else label: Label
{ {
toolbox.uninstall(model.id) text: control.text
color: UM.Theme.getColor("text")
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
} }
} }
onClicked: toolbox.uninstall(model.id)
} }
Button Button
{ {
@ -180,10 +183,7 @@ Item
font: UM.Theme.getFont("default_bold") font: UM.Theme.getFont("default_bold")
} }
} }
onClicked: onClicked: toolbox.update(model.id)
{
toolbox.update(model.id);
}
} }
ProgressBar ProgressBar
{ {

View File

@ -28,8 +28,9 @@ class PackagesModel(ListModel):
self.addRoleName(Qt.UserRole + 11, "download_url") self.addRoleName(Qt.UserRole + 11, "download_url")
self.addRoleName(Qt.UserRole + 12, "last_updated") self.addRoleName(Qt.UserRole + 12, "last_updated")
self.addRoleName(Qt.UserRole + 13, "is_bundled") self.addRoleName(Qt.UserRole + 13, "is_bundled")
self.addRoleName(Qt.UserRole + 14, "has_configs") self.addRoleName(Qt.UserRole + 14, "is_enabled")
self.addRoleName(Qt.UserRole + 15, "supported_configs") self.addRoleName(Qt.UserRole + 15, "has_configs")
self.addRoleName(Qt.UserRole + 16, "supported_configs")
# List of filters for queries. The result is the union of the each list of results. # List of filters for queries. The result is the union of the each list of results.
self._filter = {} # type: Dict[str, str] self._filter = {} # type: Dict[str, str]
@ -52,20 +53,26 @@ class PackagesModel(ListModel):
configs_model = ConfigsModel() configs_model = ConfigsModel()
configs_model.setConfigs(package["data"]["supported_configs"]) configs_model.setConfigs(package["data"]["supported_configs"])
if "author_id" not in package["author"] or "display_name" not in package["author"]:
package["author"]["author_id"] = ""
package["author"]["display_name"] = ""
# raise Exception("Detected a package with malformed author data.")
items.append({ items.append({
"id": package["package_id"], "id": package["package_id"],
"type": package["package_type"], "type": package["package_type"],
"name": package["display_name"], "name": package["display_name"],
"version": package["package_version"], "version": package["package_version"],
"author_id": package["author"]["author_id"] if "author_id" in package["author"] else package["author"]["name"], "author_id": package["author"]["author_id"],
"author_name": package["author"]["display_name"] if "display_name" in package["author"] else package["author"]["name"], "author_name": package["author"]["display_name"],
"author_email": package["author"]["email"] if "email" in package["author"] else None, "author_email": package["author"]["email"] if "email" in package["author"] else None,
"description": package["description"], "description": package["description"] if "description" in package else None,
"icon_url": package["icon_url"] if "icon_url" in package else None, "icon_url": package["icon_url"] if "icon_url" in package else None,
"image_urls": package["image_urls"] if "image_urls" in package else None, "image_urls": package["image_urls"] if "image_urls" in package else None,
"download_url": package["download_url"] if "download_url" in package else None, "download_url": package["download_url"] if "download_url" in package else None,
"last_updated": package["last_updated"] if "last_updated" in package else None, "last_updated": package["last_updated"] if "last_updated" in package else None,
"is_bundled": package["is_bundled"] if "is_bundled" in package else False, "is_bundled": package["is_bundled"] if "is_bundled" in package else False,
"is_enabled": package["is_enabled"] if "is_enabled" in package else False,
"has_configs": has_configs, "has_configs": has_configs,
"supported_configs": configs_model "supported_configs": configs_model
}) })

View File

@ -18,6 +18,7 @@ from UM.Extension import Extension
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.Version import Version from UM.Version import Version
import cura.CuraVersion
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from .AuthorsModel import AuthorsModel from .AuthorsModel import AuthorsModel
from .PackagesModel import PackagesModel from .PackagesModel import PackagesModel
@ -34,7 +35,7 @@ class Toolbox(QObject, Extension):
self._application = Application.getInstance() self._application = Application.getInstance()
self._package_manager = None self._package_manager = None
self._plugin_registry = Application.getInstance().getPluginRegistry() self._plugin_registry = Application.getInstance().getPluginRegistry()
self._packages_version = self._plugin_registry.APIVersion self._packages_version = cura.CuraVersion.CuraPackagesVersion if hasattr(cura.CuraVersion, "CuraPackagesVersion") else self._plugin_registry.APIVersion # type:ignore
self._api_version = 1 self._api_version = 1
self._api_url = "https://api-staging.ultimaker.com/cura-packages/v{api_version}/cura/v{package_version}".format( api_version = self._api_version, package_version = self._packages_version) self._api_url = "https://api-staging.ultimaker.com/cura-packages/v{api_version}/cura/v{package_version}".format( api_version = self._api_version, package_version = self._packages_version)

View File

@ -1,7 +1,7 @@
{ {
"name": "UM3 Network Connection", "name": "UM3 Network Connection",
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"description": "Manages network connections to Ultimaker 3 printers", "description": "Manages network connections to Ultimaker 3 printers.",
"version": "1.0.0", "version": "1.0.0",
"api": 4, "api": 4,
"i18n-catalog": "cura" "i18n-catalog": "cura"

View File

@ -2,7 +2,7 @@
"name": "Ultimaker machine actions", "name": "Ultimaker machine actions",
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Provides machine actions for Ultimaker machines (such as bed leveling wizard, selecting upgrades, etc)", "description": "Provides machine actions for Ultimaker machines (such as bed leveling wizard, selecting upgrades, etc.).",
"api": 4, "api": 4,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -2,7 +2,7 @@
"name": "UserAgreement", "name": "UserAgreement",
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Ask the user once if he/she agrees with our license", "description": "Ask the user once if he/she agrees with our license.",
"api": 4, "api": 4,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

1134
resources/packages.json Normal file

File diff suppressed because it is too large Load Diff