From 30fd2ce5b8cb529d184944b94588c70c0da7960f Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Mon, 19 Apr 2021 12:19:23 -0500 Subject: [PATCH 01/16] [App] Add metadata handling class --- src/App/CMakeLists.txt | 5 + src/App/Metadata.cpp | 750 ++++++++++++++++++++++++++++++++++++++ src/App/Metadata.h | 299 +++++++++++++++ src/App/MetadataPy.xml | 156 ++++++++ src/App/MetadataPyImp.cpp | 285 +++++++++++++++ 5 files changed, 1495 insertions(+) create mode 100644 src/App/Metadata.cpp create mode 100644 src/App/Metadata.h create mode 100644 src/App/MetadataPy.xml create mode 100644 src/App/MetadataPyImp.cpp diff --git a/src/App/CMakeLists.txt b/src/App/CMakeLists.txt index d97f8cd8d7..be1699a541 100644 --- a/src/App/CMakeLists.txt +++ b/src/App/CMakeLists.txt @@ -92,6 +92,7 @@ generate_from_xml(LinkBaseExtensionPy) generate_from_xml(DocumentObjectGroupPy) generate_from_xml(GeoFeaturePy) generate_from_xml(GeoFeatureGroupExtensionPy) +generate_from_xml(MetadataPy) generate_from_xml(OriginGroupExtensionPy) generate_from_xml(PartPy) @@ -108,6 +109,7 @@ SET(FreeCADApp_XML_SRCS DocumentObjectExtensionPy.xml GroupExtensionPy.xml LinkBaseExtensionPy.xml + MetadataPy.xml DocumentObjectGroupPy.xml DocumentObjectPy.xml GeoFeaturePy.xml @@ -261,6 +263,8 @@ SET(FreeCADApp_CPP_SRCS Enumeration.cpp Material.cpp MaterialPyImp.cpp + Metadata.cpp + MetadataPyImp.cpp ) SET(FreeCADApp_HPP_SRCS @@ -273,6 +277,7 @@ SET(FreeCADApp_HPP_SRCS ComplexGeoData.h Enumeration.h Material.h + Metadata.h ) SET(FreeCADApp_SRCS diff --git a/src/App/Metadata.cpp b/src/App/Metadata.cpp new file mode 100644 index 0000000000..31a8853c5a --- /dev/null +++ b/src/App/Metadata.cpp @@ -0,0 +1,750 @@ +/*************************************************************************** + * Copyright (c) 2021 Chris Hennes * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library 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 library; see the file LICENSE.html. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + +#include "PreCompiled.h" + +#ifndef _PreComp_ +# include +#endif + +#include "Metadata.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +#include "Base/XMLTools.h" +#include "App/Expression.h" +#include "App/Application.h" + +/* +*** From GCC: *** +In the GNU C Library, "major" and "minor" are defined +by . For historical compatibility, it is +currently defined by as well, but we plan to +remove this soon. To use "major", include +directly. If you did not intend to use a system-defined macro +"major", you should undefine it after including . +*/ +#ifdef major +#undef major +#endif +#ifdef minor +#undef minor +#endif + +using namespace App; +namespace fs = boost::filesystem; +XERCES_CPP_NAMESPACE_USE + + +Metadata::Metadata(const fs::path& metadataFile) +{ + // Any exception thrown by the XML code propagates out and prevents object creation + XMLPlatformUtils::Initialize(); + + _parser = std::make_shared(); + _parser->setValidationScheme(XercesDOMParser::Val_Never); + _parser->setDoNamespaces(true); + + auto errHandler = std::make_unique(); + _parser->setErrorHandler(errHandler.get()); + + _parser->parse(metadataFile.string().c_str()); + + auto doc = _parser->getDocument(); + _dom = doc->getDocumentElement(); + + auto rootTagName = StrXUTF8(_dom->getTagName()).str; + if (rootTagName != "package") + throw Base::XMLBaseException("Malformed package.xml document: Root group not found"); + + auto formatVersion = XMLString::parseInt(_dom->getAttribute(XUTF8Str("format").unicodeForm())); + switch (formatVersion) { + case 1: + parseVersion1(_dom); + break; + default: + throw Base::XMLBaseException("package.xml format version is not supported by this version of FreeCAD"); + } +} + +Metadata::Metadata() : _dom(nullptr) +{ +} + +Metadata::Metadata(const DOMNode* domNode, int format) : _dom(nullptr) +{ + auto element = dynamic_cast(domNode); + if (element) { + switch (format) { + case 1: + parseVersion1(element); + break; + default: + throw Base::XMLBaseException("package.xml format version is not supported by this version of FreeCAD"); + } + } +} + +Metadata::~Metadata() +{ +} + +std::string Metadata::name() const +{ + return _name; +} + +Meta::Version Metadata::version() const +{ + return _version; +} + +std::string Metadata::description() const +{ + return _description; +} + +std::vector Metadata::maintainer() const +{ + return _maintainer; +} + +std::vector Metadata::license() const +{ + return _license; +} + +std::vector Metadata::url() const +{ + return _url; +} + +std::vector Metadata::author() const +{ + return _author; +} + +std::vector Metadata::depend() const +{ + return _depend; +} + +std::vector Metadata::conflict() const +{ + return _conflict; +} + +std::vector Metadata::replace() const +{ + return _replace; +} + +std::vector Metadata::tag() const +{ + return _tag; +} + +fs::path Metadata::icon() const +{ + return _icon; +} + +std::string Metadata::classname() const +{ + return _classname; +} + +std::vector Metadata::file() const +{ + return _file; +} + +Meta::Version App::Metadata::freecadmin() const +{ + return _freecadmin; +} + +Meta::Version App::Metadata::freecadmax() const +{ + return _freecadmax; +} + +std::multimap Metadata::content() const +{ + return _content; +} + +std::vector Metadata::operator[](const std::string& tag) const +{ + std::vector returnValue; + auto range = _genericMetadata.equal_range(tag); + for (auto item = range.first; item != range.second; ++item) + returnValue.push_back(item->second); + return returnValue; +} + +XERCES_CPP_NAMESPACE::DOMElement* Metadata::dom() const +{ + return _dom; +} + +void Metadata::setName(const std::string& name) +{ + std::string invalidCharacters = "/\\?%*:|\"<>"; // Should cover all OSes + if (_name.find_first_of(invalidCharacters) != std::string::npos) + throw Base::RuntimeError("Name cannot contain any of: " + invalidCharacters); + + _name = name; +} + +void Metadata::setVersion(const Meta::Version& version) +{ + _version = version; +} + +void Metadata::setDescription(const std::string& description) +{ + _description = description; +} + +void Metadata::addMaintainer(const Meta::Contact& maintainer) +{ + _maintainer.push_back(maintainer); +} + +void Metadata::addLicense(const Meta::License& license) +{ + _license.push_back(license); +} + +void Metadata::addUrl(const Meta::Url& url) +{ + _url.push_back(url); +} + +void Metadata::addAuthor(const Meta::Contact& author) +{ + _author.push_back(author); +} + +void Metadata::addDepend(const Meta::Dependency& dep) +{ + _depend.push_back(dep); +} + +void Metadata::addConflict(const Meta::Dependency& dep) +{ + _conflict.push_back(dep); +} + +void Metadata::addReplace(const Meta::Dependency& dep) +{ + _replace.push_back(dep); +} + +void Metadata::addTag(const std::string& tag) +{ + _tag.push_back(tag); +} + +void Metadata::setIcon(const fs::path& path) +{ + _icon = path; +} + +void Metadata::setClassname(const std::string& name) +{ + _classname = name; +} + +void Metadata::addFile(const fs::path& path) +{ + _file.push_back(path); +} + +void Metadata::addContentItem(const std::string& tag, const Metadata& item) +{ + _content.insert(std::make_pair(tag, item)); +} + +void App::Metadata::setFreeCADMin(const Meta::Version& version) +{ + _freecadmin = version; +} + +void App::Metadata::setFreeCADMax(const Meta::Version& version) +{ + _freecadmax = version; +} + +void App::Metadata::addGenericMetadata(const std::string& tag, const Meta::GenericMetadata& genericMetadata) +{ + _genericMetadata.insert(std::make_pair(tag, genericMetadata)); +} + + +DOMElement* appendSimpleXMLNode(DOMElement* baseNode, const std::string& nodeName, const std::string& nodeContents) +{ + // For convenience (and brevity of final output) don't create nodes that don't have contents + if (nodeContents.empty()) + return nullptr; + + auto doc = baseNode->getOwnerDocument(); + DOMElement* namedElement = doc->createElement(XUTF8Str(nodeName.c_str()).unicodeForm()); + baseNode->appendChild(namedElement); + DOMText* namedNode = doc->createTextNode(XUTF8Str(nodeContents.c_str()).unicodeForm()); + namedElement->appendChild(namedNode); + return namedElement; +} + +void addAttribute(DOMElement* node, const std::string& key, const std::string& value) +{ + if (value.empty()) + return; + + node->setAttribute(XUTF8Str(key.c_str()).unicodeForm(), XUTF8Str(value.c_str()).unicodeForm()); +} + +void addDependencyNode(DOMElement* root, const std::string& name, const Meta::Dependency& depend) +{ + auto element = appendSimpleXMLNode(root, name, depend.package); + if (element) { + addAttribute(element, "version_lt", depend.version_lt); + addAttribute(element, "version_lte", depend.version_lte); + addAttribute(element, "version_eq", depend.version_eq); + addAttribute(element, "version_gte", depend.version_gte); + addAttribute(element, "version_gt", depend.version_gt); + addAttribute(element, "condition", depend.condition); + } +} + +void Metadata::write(const fs::path& file) const +{ + DOMImplementation* impl = DOMImplementationRegistry::getDOMImplementation(XUTF8Str("Core LS").unicodeForm()); + + DOMDocument* doc = impl->createDocument(nullptr, XUTF8Str("package").unicodeForm(), nullptr); + DOMElement* root = doc->getDocumentElement(); + root->setAttribute(XUTF8Str("format").unicodeForm(), XUTF8Str("1").unicodeForm()); + + appendToElement(root); + + DOMLSSerializer* theSerializer = ((DOMImplementationLS*)impl)->createLSSerializer(); + DOMConfiguration* config = theSerializer->getDomConfig(); + if (config->canSetParameter(XMLUni::fgDOMWRTFormatPrettyPrint, true)) + config->setParameter(XMLUni::fgDOMWRTFormatPrettyPrint, true); + + // set feature if the serializer supports the feature/mode + if (config->canSetParameter(XMLUni::fgDOMWRTSplitCdataSections, true)) + config->setParameter(XMLUni::fgDOMWRTSplitCdataSections, true); + + if (config->canSetParameter(XMLUni::fgDOMWRTDiscardDefaultContent, true)) + config->setParameter(XMLUni::fgDOMWRTDiscardDefaultContent, true); + + try { + XMLFormatTarget* myFormTarget = new LocalFileFormatTarget(file.string().c_str()); + DOMLSOutput* theOutput = ((DOMImplementationLS*)impl)->createLSOutput(); + + theOutput->setByteStream(myFormTarget); + theSerializer->write(doc, theOutput); + + theOutput->release(); + theSerializer->release(); + delete myFormTarget; + } + catch (const XMLException& toCatch) { + char* message = XMLString::transcode(toCatch.getMessage()); + std::string what = message; + XMLString::release(&message); + throw Base::XMLBaseException(what); + } + catch (const DOMException& toCatch) { + char* message = XMLString::transcode(toCatch.getMessage()); + std::string what = message; + XMLString::release(&message); + throw Base::XMLBaseException(what); + } + + doc->release(); +} + +bool Metadata::satisfies(const Meta::Dependency& dep) +{ + if (dep.package != _name) + return false; + + // The "condition" attribute allows an expression to enable or disable this dependency check: it must contain a valid + // FreeCAD Expression. If it evaluates to false, this dependency is bypassed (e.g. this function returns false). + if (!dep.condition.empty()) { + auto injectedString = dep.condition; + std::map replacements; + std::map& config = App::Application::Config(); + replacements.insert(std::make_pair("$BuildVersionMajor", config["BuildVersionMajor"])); + replacements.insert(std::make_pair("$BuildVersionMinor", config["BuildVersionMinor"])); + replacements.insert(std::make_pair("$BuildRevision", config["BuildRevision"])); + for (const auto& replacement : replacements) { + auto pos = injectedString.find(replacement.first); + while (pos != std::string::npos) { + injectedString.replace(pos, replacement.first.length(), replacement.second); + pos = injectedString.find(replacement.first); + } + } + auto parsedExpression = App::Expression::parse(nullptr, dep.condition); + auto result = parsedExpression->eval(); + if (boost::any_cast (result->getValueAsAny()) == false) + return false; + } + + if (!dep.version_eq.empty()) + return _version == Meta::Version(dep.version_eq); + + // Any of the others might be specified in pairs, so only return the "false" case + + if (!dep.version_lt.empty()) + if (!(_version < Meta::Version(dep.version_lt))) + return false; + + if (!dep.version_lte.empty()) + if (!(_version <= Meta::Version(dep.version_lt))) + return false; + + if (!dep.version_gt.empty()) + if (!(_version > Meta::Version(dep.version_lt))) + return false; + + if (!dep.version_gte.empty()) + if (!(_version >= Meta::Version(dep.version_lt))) + return false; + + return true; +} + +void Metadata::appendToElement(DOMElement* root) const +{ + appendSimpleXMLNode(root, "name", _name); + appendSimpleXMLNode(root, "description", _description); + appendSimpleXMLNode(root, "version", _version.str()); + + for (const auto& maintainer : _maintainer) { + auto element = appendSimpleXMLNode(root, "maintainer", maintainer.name); + if (element) + addAttribute(element, "email", maintainer.email); + } + + for (const auto& license : _license) { + auto element = appendSimpleXMLNode(root, "license", license.name); + if (element) + addAttribute(element, "file", license.file.string()); + } + + if (_freecadmin != Meta::Version()) + appendSimpleXMLNode(root, "freecadmin", _freecadmin.str()); + + if (_freecadmax != Meta::Version()) + appendSimpleXMLNode(root, "freecadmax", _freecadmin.str()); + + for (const auto& url : _url) { + auto element = appendSimpleXMLNode(root, "url", url.location); + if (element) { + std::string typeAsString("website"); + switch (url.type) { + case Meta::UrlType::website: typeAsString = "website"; break; + case Meta::UrlType::repository: typeAsString = "repository"; break; + case Meta::UrlType::bugtracker: typeAsString = "bugtracker"; break; + case Meta::UrlType::readme: typeAsString = "readme"; break; + case Meta::UrlType::documentation: typeAsString = "documentation"; break; + } + addAttribute(element, "type", typeAsString); + } + } + + for (const auto& author : _author) { + auto element = appendSimpleXMLNode(root, "author", author.name); + if (element) + addAttribute(element, "email", author.email); + } + + for (const auto& depend : _depend) + addDependencyNode(root, "depend", depend); + + for (const auto& conflict : _conflict) + addDependencyNode(root, "conflict", conflict); + + for (const auto& replace : _replace) + addDependencyNode(root, "replace", replace); + + for (const auto& tag : _tag) + appendSimpleXMLNode(root, "tag", tag); + + appendSimpleXMLNode(root, "icon", _icon.string()); + + appendSimpleXMLNode(root, "classname", _classname); + + for (const auto& file : _file) + appendSimpleXMLNode(root, "file", file.string()); + + for (const auto& md : _genericMetadata) { + auto element = appendSimpleXMLNode(root, md.first, md.second.contents); + for (const auto& attr : md.second.attributes) + addAttribute(element, attr.first, attr.second); + } + + if (!_content.empty()) { + auto doc = root->getOwnerDocument(); + DOMElement* contentRootElement = doc->createElement(XUTF8Str("content").unicodeForm()); + root->appendChild(contentRootElement); + for (const auto& content : _content) { + DOMElement* contentElement = doc->createElement(XUTF8Str(content.first.c_str()).unicodeForm()); + contentRootElement->appendChild(contentElement); + content.second.appendToElement(contentElement); + } + } +} + + +void Metadata::parseVersion1(const DOMNode* startNode) +{ + auto children = startNode->getChildNodes(); + + for (XMLSize_t i = 0; i < children->getLength(); ++i) { + auto child = children->item(i); + auto element = dynamic_cast(child); + if (!element) + continue; + + auto tag = element->getNodeName(); + auto tagString = StrXUTF8(tag).str; + + if (tagString == "name") + _name = StrXUTF8(element->getTextContent()).str; + else if (tagString == "version") + _version = Meta::Version(StrXUTF8(element->getTextContent()).str); + else if (tagString == "description") + _description = StrXUTF8(element->getTextContent()).str; + else if (tagString == "maintainer") + _maintainer.emplace_back(element); + else if (tagString == "license") + _license.emplace_back(element); + else if (tagString == "freecadmin") + _freecadmin = Meta::Version(StrXUTF8(element->getTextContent()).str); + else if (tagString == "freecadmax") + _freecadmax = Meta::Version(StrXUTF8(element->getTextContent()).str); + else if (tagString == "url") + _url.emplace_back(element); + else if (tagString == "author") + _author.emplace_back(element); + else if (tagString == "depend") + _depend.emplace_back(element); + else if (tagString == "conflict") + _conflict.emplace_back(element); + else if (tagString == "replace") + _replace.emplace_back(element); + else if (tagString == "tag") + _tag.emplace_back(StrXUTF8(element->getTextContent()).str); + else if (tagString == "file") + _file.emplace_back(StrXUTF8(element->getTextContent()).str); + else if (tagString == "classname") + _classname = StrXUTF8(element->getTextContent()).str; + else if (tagString == "icon") + _icon = fs::path(StrXUTF8(element->getTextContent()).str); + else if (tagString == "content") + parseContentNodeVersion1(element); // Recursive call + else { + // If none of this node's children have children of their own, it is a simple element and we + // can handle it as a GenericMetadata object + auto children = element->getChildNodes(); + bool hasGrandchildren = false; + for (XMLSize_t i = 0; i < children->getLength() && !hasGrandchildren; ++i) + if (children->item(i)->getChildNodes()->getLength() > 0) + hasGrandchildren = true; + if (!hasGrandchildren) + _genericMetadata.insert(std::make_pair(tagString, Meta::GenericMetadata(element))); + } + } +} + +void Metadata::parseContentNodeVersion1(const DOMElement* contentNode) +{ + auto children = contentNode->getChildNodes(); + for (XMLSize_t i = 0; i < children->getLength(); ++i) { + auto child = dynamic_cast(children->item(i)); + if (child) { + auto tag = StrXUTF8(child->getTagName()).str; + _content.insert(std::make_pair(tag, Metadata(child, 1))); + } + } +} + +Meta::Contact::Contact(const std::string& name, const std::string& email) : + name(name), + email(email) +{ + // This has to be provided manually since we have another constructor +} + +Meta::Contact::Contact(const XERCES_CPP_NAMESPACE::DOMElement* e) +{ + auto emailAttribute = e->getAttribute(XUTF8Str("email").unicodeForm()); + name = StrXUTF8(e->getTextContent()).str; + email = StrXUTF8(emailAttribute).str; +} + +Meta::License::License(const std::string& name, fs::path file) : + name(name), + file(file) +{ + // This has to be provided manually since we have another constructor +} + +Meta::License::License(const XERCES_CPP_NAMESPACE::DOMElement* e) +{ + auto fileAttribute = e->getAttribute(XUTF8Str("file").unicodeForm()); + if (XMLString::stringLen(fileAttribute) > 0) { + file = fs::path(StrXUTF8(fileAttribute).str); + } + name = StrXUTF8(e->getTextContent()).str; +} + +Meta::Url::Url(const std::string& location, UrlType type) : + location(location), + type(type) +{ + // This has to be provided manually since we have another constructor +} + +Meta::Url::Url(const XERCES_CPP_NAMESPACE::DOMElement* e) +{ + auto typeAttribute = StrXUTF8(e->getAttribute(XUTF8Str("type").unicodeForm())).str; + if (typeAttribute.empty() || typeAttribute == "website") + type = UrlType::website; + else if (typeAttribute == "bugtracker") + type = UrlType::bugtracker; + else if (typeAttribute == "repository") + type = UrlType::repository; + else if (typeAttribute == "readme") + type = UrlType::readme; + else if (typeAttribute == "documentation") + type = UrlType::documentation; + location = StrXUTF8(e->getTextContent()).str; +} + +Meta::Dependency::Dependency(const XERCES_CPP_NAMESPACE::DOMElement* e) +{ + version_lt = StrXUTF8(e->getAttribute(XUTF8Str("version_lt").unicodeForm())).str; + version_lte = StrXUTF8(e->getAttribute(XUTF8Str("version_lte").unicodeForm())).str; + version_eq = StrXUTF8(e->getAttribute(XUTF8Str("version_eq").unicodeForm())).str; + version_gte = StrXUTF8(e->getAttribute(XUTF8Str("version_gte").unicodeForm())).str; + version_gt = StrXUTF8(e->getAttribute(XUTF8Str("version_gt").unicodeForm())).str; + condition = StrXUTF8(e->getAttribute(XUTF8Str("condition").unicodeForm())).str; + + package = StrXUTF8(e->getTextContent()).str; +} + +Meta::Version::Version() : + major(0), + minor(0), + patch(0) +{ +} + +Meta::Version::Version(int major, int minor, int patch, const std::string& suffix) : + major(major), + minor(minor), + patch(patch), + suffix(suffix) +{ + +} + +Meta::Version::Version(const std::string& versionString) : + minor(0), + patch(0) +{ + std::istringstream stream(versionString); + char separator; + stream >> major; + if (stream) stream >> separator; + if (stream) stream >> minor; + if (stream) stream >> separator; + if (stream) stream >> patch; + if (stream) stream >> suffix; +} + +std::string Meta::Version::str() const +{ + std::ostringstream stream; + stream << major << "." << minor << "." << patch << suffix; + return stream.str(); +} + +bool Meta::Version::operator<(const Version& rhs) const +{ + return std::tie(major, minor, patch, suffix) < std::tie(rhs.major, rhs.minor, rhs.patch, rhs.suffix); +} + +bool Meta::Version::operator>(const Version& rhs) const +{ + return std::tie(major, minor, patch, suffix) > std::tie(rhs.major, rhs.minor, rhs.patch, rhs.suffix); +} + +bool Meta::Version::operator<=(const Version& rhs) const +{ + return std::tie(major, minor, patch, suffix) <= std::tie(rhs.major, rhs.minor, rhs.patch, rhs.suffix); +} + +bool Meta::Version::operator>=(const Version& rhs) const +{ + return std::tie(major, minor, patch, suffix) >= std::tie(rhs.major, rhs.minor, rhs.patch, rhs.suffix); +} + +bool Meta::Version::operator==(const Version& rhs) const +{ + return std::tie(major, minor, patch, suffix) == std::tie(rhs.major, rhs.minor, rhs.patch, rhs.suffix); +} + +bool Meta::Version::operator!=(const Version& rhs) const +{ + return std::tie(major, minor, patch, suffix) != std::tie(rhs.major, rhs.minor, rhs.patch, rhs.suffix); +} + +Meta::GenericMetadata::GenericMetadata(const XERCES_CPP_NAMESPACE::DOMElement* e) +{ + contents = StrXUTF8(e->getTextContent()).str; + for (XMLSize_t i = 0; i < e->getAttributes()->getLength(); ++i) { + auto a = e->getAttributes()->item(i); + attributes.insert(std::make_pair(StrXUTF8(a->getNodeName()).str, + StrXUTF8(a->getTextContent()).str)); + } +} + +App::Meta::GenericMetadata::GenericMetadata(const std::string& contents) : + contents(contents) +{ +} diff --git a/src/App/Metadata.h b/src/App/Metadata.h new file mode 100644 index 0000000000..de8be3ea8e --- /dev/null +++ b/src/App/Metadata.h @@ -0,0 +1,299 @@ +/*************************************************************************** + * Copyright (c) 2021 Chris Hennes * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library 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 library; see the file LICENSE.html. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + +#ifndef BASE_METADATAREADER_H +#define BASE_METADATAREADER_H + +#include "FCConfig.h" + +#include + +#include +#include +#include + +#include +#include + + +namespace App { + + namespace Meta { + + /** + * \struct Contact + * \brief A person or company representing a point of contact for the package (either author or maintainer). + */ + struct AppExport Contact { + Contact() = default; + Contact(const std::string& name, const std::string& email); + explicit Contact(const XERCES_CPP_NAMESPACE::DOMElement* e); + std::string name; //< Contact name - required + std::string email; //< Contact email - may be optional + }; + + /** + * \struct License + * \brief A license that covers some or all of this package. + * + * Many licenses also require the inclusion of the complete license text, specified in this struct + * using the "file" member. + */ + struct AppExport License { + License() = default; + License(const std::string& name, boost::filesystem::path file); + explicit License(const XERCES_CPP_NAMESPACE::DOMElement* e); + std::string name; //< Short name of license, e.g. "LGPL2", "MIT", "Mozilla Public License", etc. + boost::filesystem::path file; //< Optional path to the license file, relative to the XML file's location + }; + + enum class UrlType { + website, + repository, + bugtracker, + readme, + documentation + }; + + /** + * \struct Url + * \brief A URL, including type information (e.g. website, repository, or bugtracker, in package.xml v3) + */ + struct AppExport Url { + Url() = default; + Url(const std::string& location, UrlType type); + explicit Url(const XERCES_CPP_NAMESPACE::DOMElement* e); + std::string location; //< The actual URL, including protocol + UrlType type; //< What kind of URL this is + }; + + /** + * \struct Version + * A semantic version structure providing comparison operators and conversion to and from std::string + */ + struct AppExport Version { + Version(); + Version(int major, int minor = 0, int patch = 0, const std::string& suffix = std::string()); + explicit Version(const std::string& semanticString); + + int major; + int minor; + int patch; + std::string suffix; + + std::string str() const; + + bool operator<(const Version&) const; + bool operator>(const Version&) const; + bool operator<=(const Version&) const; + bool operator>=(const Version&) const; + bool operator==(const Version&) const; + bool operator!=(const Version&) const; + }; + + /** + * \struct Dependency + * \brief Another package that this package depends on, conflicts with, or replaces + */ + struct AppExport Dependency { + Dependency() = default; + explicit Dependency(const XERCES_CPP_NAMESPACE::DOMElement* e); + std::string package; //< Required: must exactly match the contents of the "name" element in the referenced package's package.xml file. + std::string version_lt; //< Optional: The dependency to the package is restricted to versions less than the stated version number. + std::string version_lte; //< Optional: The dependency to the package is restricted to versions less or equal than the stated version number. + std::string version_eq; //< Optional: The dependency to the package is restricted to a version equal than the stated version number. + std::string version_gte; //< Optional: The dependency to the package is restricted to versions greater or equal than the stated version number. + std::string version_gt; //< Optional: The dependency to the package is restricted to versions greater than the stated version number. + std::string condition; //< Optional: Conditional expression as documented in REP149. + }; + + /** + * \struct GenericMetadata + * A structure to hold unrecognized single-level metadata. + * + * Most unrecognized metadata is simple: when parsing the XML, if the parser finds a tag it + * does not recognize, and that tag has no children, it is parsed into this data structure + * for convenient access by client code. + */ + struct AppExport GenericMetadata { + GenericMetadata() = default; + explicit GenericMetadata(const XERCES_CPP_NAMESPACE::DOMElement* e); + explicit GenericMetadata(const std::string &contents); + std::string contents; //< The contents of the tag + std::map attributes; //< The XML attributes of the tag + }; + + } + + /** + * \class Metadata + * \brief Reads data from a metadata file. + * + * The metadata format is based on https://ros.org/reps/rep-0149.html, modified for FreeCAD + * use. Full format documentation is available at the FreeCAD Wiki: + * https://wiki.freecadweb.org/Package_Metadata + */ + class AppExport Metadata { + public: + + Metadata(); + + /** + * Read the data from a file on disk + * + * This constructor takes a path to an XML file and loads the XML from that file as + * metadata. + */ + explicit Metadata(const boost::filesystem::path& metadataFile); + + /** + * Construct a Metadata object from a DOM node. + * + * This node may have any tag name: it is only accessed via its children, which are + * expected to follow the standard Metadata format for the contents of the element. + */ + Metadata(const XERCES_CPP_NAMESPACE::DOMNode* domNode, int format); + + ~Metadata(); + + + ////////////////////////////////////////////////////////////// + // Recognized Metadata + ////////////////////////////////////////////////////////////// + + std::string name() const; //< A short name for this package, often used as a menu entry. + Meta::Version version() const; //< Version string in symantic triplet format, e.g. "1.2.3". + std::string description() const; //< Text-only description of the package. No markup. + std::vector maintainer() const; //< Must be at least one, and must specify an email address. + std::vector license() const; //< Must be at least one, and most licenses require including a license file. + std::vector url() const; //< Any number of URLs may be specified, but at least one repository URL must be included at the package level. + std::vector author() const; //< Any number of authors may be specified, and email addresses are optional. + std::vector depend() const; //< Zero or more packages this package requires prior to use. + std::vector conflict() const; //< Zero of more packages this package conflicts with. + std::vector replace() const; //< Zero or more packages this package is intended to replace. + std::vector tag() const; //< Zero or more text tags related to this package. + boost::filesystem::path icon() const; //< Path to an icon file. + std::string classname() const; //< Recognized for convenience -- generally only used by Workbenches. + std::vector file() const; //< Arbitrary files associated with this package or content item. + Meta::Version freecadmin() const; //< The minimum FreeCAD version. + Meta::Version freecadmax() const; //< The maximum FreeCAD version. + + /** + * Access the metadata for the content elements of this package + * + * In addition to the overall package metadata, this class reads in metadata contained in a + * element. Each entry in the content element is an element representing some + * type of package content (e.g. add-on, macro, theme, etc.). This class places no restriction + * on the types, it is up to client code to place requirements on the metadata included + * here. + * + * For example, themes might be specified: + * + * + * High Contrast + * + * + */ + std::multimap content() const; + + /** + * Convenience accessor for unrecognized simple metadata. + * + * If the XML parser encounters tags that it does not recognize, and those tags have + * no children, a GenericMetadata object is created. Those objects can be accessed using + * operator[], which returns a (potentially empty) vector containing all instances of the + * given tag. It cannot be used to *create* a new tag, however. See addGenericMetadata(). + */ + std::vector operator[] (const std::string& tag) const; + + /** + * Directly access the DOM tree to support unrecognized multi-level metadata + */ + XERCES_CPP_NAMESPACE::DOMElement* dom() const; + + + // Setters + void setName(const std::string& name); + void setVersion(const Meta::Version& version); + void setDescription(const std::string& description); + void addMaintainer(const Meta::Contact& maintainer); + void addLicense(const Meta::License& license); + void addUrl(const Meta::Url& url); + void addAuthor(const Meta::Contact& author); + void addDepend(const Meta::Dependency& dep); + void addConflict(const Meta::Dependency& dep); + void addReplace(const Meta::Dependency& dep); + void addTag(const std::string& tag); + void setIcon(const boost::filesystem::path& path); + void setClassname(const std::string& name); + void addFile(const boost::filesystem::path& path); + void addContentItem(const std::string& tag, const Metadata& item); + void setFreeCADMin(const Meta::Version& version); + void setFreeCADMax(const Meta::Version& version); + void addGenericMetadata(const std::string& tag, const Meta::GenericMetadata& genericMetadata); + + /** + * Write the metadata to an XML file + */ + void write(const boost::filesystem::path& file) const; + + /** + * Determine whether this package satisfies the given dependency + */ + bool satisfies(const Meta::Dependency&); + + private: + + std::string _name; + Meta::Version _version; + std::string _description; + std::vector _maintainer; + std::vector _license; + std::vector _url; + std::vector _author; + std::vector _depend; + std::vector _conflict; + std::vector _replace; + std::vector _tag; + boost::filesystem::path _icon; + std::string _classname; + std::vector _file; + Meta::Version _freecadmin; + Meta::Version _freecadmax; + + std::multimap _content; + + std::multimap _genericMetadata; + + XERCES_CPP_NAMESPACE::DOMElement* _dom; + std::shared_ptr _parser; + + void parseVersion1(const XERCES_CPP_NAMESPACE::DOMNode* startNode); + void parseContentNodeVersion1(const XERCES_CPP_NAMESPACE::DOMElement* contentNode); + + void appendToElement(XERCES_CPP_NAMESPACE::DOMElement* root) const; + + }; + +} + +#endif \ No newline at end of file diff --git a/src/App/MetadataPy.xml b/src/App/MetadataPy.xml new file mode 100644 index 0000000000..8b211c59a0 --- /dev/null +++ b/src/App/MetadataPy.xml @@ -0,0 +1,156 @@ + + + + + + + + Metadata + A Metadata object reads an XML-formatted package metadata file and provides read-only access to its contents. + + A single constructor is supported: + Metadata(file) -- Reads the XML file and provides access to the metadata it specifies. + + Metadata + + + + + String: the name of this item + + + + + + String: the version of this item in semantic triplet format + + + + + + String: the description of this item (text only, no markup allowed) + + + + + + List of maintainer objects with 'name' and 'email' string attributes + + + + + + List of applicable licenses as objects with 'name' and 'file' string attributes + + + + + + + List of URLs as objects with 'location' and 'urltype' string attributes, where urltype is one of: + * website + * repository + * bugtracker + * readme + * documentation + + + + + + + List of author objects, each with a 'name' and a (potentially empty) 'email' string attribute + + + + + + + List of dependencies, as objects with the following attributes: + * package -- Required: must exactly match the contents of the 'name' element in the referenced package's package.xml file + * version_lt -- Optional: The dependency to the package is restricted to versions less than the stated version number + * version_lte -- Optional: The dependency to the package is restricted to versions less or equal than the stated version number + * version_eq -- Optional: The dependency to the package is restricted to a version equal than the stated version number + * version_gte -- Optional: The dependency to the package is restricted to versions greater or equal than the stated version number + * version_gt -- Optional: The dependency to the package is restricted to versions greater than the stated version number + * condition -- Optional: Conditional expression as documented in REP149 + + + + + + + List of conflicts, format identical to dependencies + + + + + + List of things this item is considered by its author to replace: format identical to dependencies + + + + + + List of strings + + + + + + Relative path to an icon file + + + + + + String: the name of the main Python class this item creates/represents + + + + + + A list of files associated with this item -- the meaning of each file is implementation-defined + + + + + + A dictionary of lists of content items: defined recursively, each item is itself a Metadata object -- see package.xml file format documentation for details + + + + + + + + + getGenericMetadata(name) + Get the list of GenericMetadata objects with key 'name'. Generic metadata objects are Python objects with + a string 'contents' and a dictionary of strings, 'attributes'. They represent unrecognized simple XML tags + in the metadata file. + + + + + + public: + MetadataPy(const Metadata & pla, PyTypeObject *T = &Type) + :PyObjectBase(new Metadata(pla),T){} + Metadata value() const + { return *(getMetadataPtr()); } + + + diff --git a/src/App/MetadataPyImp.cpp b/src/App/MetadataPyImp.cpp new file mode 100644 index 0000000000..32a83a26a7 --- /dev/null +++ b/src/App/MetadataPyImp.cpp @@ -0,0 +1,285 @@ +/*************************************************************************** + * Copyright (c) 2021 Chris Hennes * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library 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 library; see the file LICENSE.html. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + +#include "PreCompiled.h" + +#include "Metadata.h" + +// inclusion of the generated files (generated out of MetadataPy.xml) +#include "MetadataPy.h" +#include "MetadataPy.cpp" + +using namespace Base; + +// Returns a string which represents the object e.g. when printed in Python +std::string MetadataPy::representation(void) const +{ + MetadataPy::PointerType ptr = reinterpret_cast(_pcTwinPointer); + std::stringstream str; + str << "Metadata [Name=("; + str << ptr->name(); + str << "), Description=("; + str << ptr->description(); + str << "), Maintainer=("; + str << ptr->maintainer().front().name << ")]"; + + return str.str(); +} + +PyObject* MetadataPy::PyMake(struct _typeobject*, PyObject* args, PyObject*) // Python wrapper +{ + // create a new instance of MetadataPy and the Twin object + const char* filename; + if (!PyArg_ParseTuple(args, "s!", &filename)) + return nullptr; + + return new MetadataPy(new Metadata(filename)); +} + +// constructor method +int MetadataPy::PyInit(PyObject* args, PyObject* /*kwd*/) +{ + if (PyArg_ParseTuple(args, "")) { + return 0; + } + + // Main class constructor -- takes a file path, loads the metadata from it + PyErr_Clear(); + const char* file; + if (PyArg_ParseTuple(args, "s!", &file)) { + App::Metadata* a = new Metadata(file); + *(getMetadataPtr()) = *a; + return 0; + } + + // Copy constructor + PyErr_Clear(); + PyObject* o; + if (PyArg_ParseTuple(args, "O!", &(App::MetadataPy::Type), &o)) { + App::Metadata* a = static_cast(o)->getMetadataPtr(); + *(getMetadataPtr()) = *a; + return 0; + } + + PyErr_SetString(Base::BaseExceptionFreeCADError, "path to metadata file expected"); + return -1; +} + +Py::Object MetadataPy::getName(void) const +{ + return Py::String(getMetadataPtr()->name()); +} + +Py::Object MetadataPy::getVersion(void) const +{ + return Py::String(getMetadataPtr()->version().str()); +} + +Py::Object MetadataPy::getDescription(void) const +{ + return Py::String(getMetadataPtr()->description()); +} + +Py::Object MetadataPy::getMaintainer(void) const +{ + auto maintainers = getMetadataPtr()->maintainer(); + Py::List pyMaintainers; + for (const auto& m : maintainers) { + Py::Object pyMaintainer; + pyMaintainer.setAttr("name", Py::String(m.name)); + pyMaintainer.setAttr("email", Py::String(m.email)); + pyMaintainers.append(pyMaintainer); + } + return pyMaintainers; +} + +Py::Object MetadataPy::getAuthor(void) const +{ + auto authors = getMetadataPtr()->author(); + Py::List pyAuthors; + for (const auto& a : authors) { + Py::Object pyAuthor; + pyAuthor.setAttr("name", Py::String(a.name)); + pyAuthor.setAttr("email", Py::String(a.email)); + pyAuthors.append(pyAuthor); + } + return pyAuthors; +} + +Py::Object MetadataPy::getLicense(void) const +{ + auto licenses = getMetadataPtr()->license(); + Py::List pyLicenses; + for (const auto& lic : licenses) { + Py::Object pyLicense; + pyLicense.setAttr("name", Py::String(lic.name)); + pyLicense.setAttr("file", Py::String(lic.file.string())); + pyLicenses.append(pyLicense); + } + return pyLicenses; +} + +Py::Object MetadataPy::getUrl(void) const +{ + auto urls = getMetadataPtr()->url (); + Py::List pyUrls; + for (const auto& url : urls) { + Py::Object pyUrl; + pyUrl.setAttr("location", Py::String(url.location)); + switch (url.type) { + case Meta::UrlType::website: pyUrl.setAttr("type", Py::String("website")); break; + case Meta::UrlType::repository: pyUrl.setAttr("type", Py::String("repository")); break; + case Meta::UrlType::bugtracker: pyUrl.setAttr("type", Py::String("bugtracker")); break; + case Meta::UrlType::readme: pyUrl.setAttr("type", Py::String("readme")); break; + case Meta::UrlType::documentation: pyUrl.setAttr("type", Py::String("documentation")); break; + } + pyUrls.append(pyUrl); + } + return pyUrls; +} + +Py::Object dependencyToPyObject(const Meta::Dependency& d) +{ + Py::Object pyDependency; + pyDependency.setAttr("package",Py::String(d.package)); + pyDependency.setAttr("version_lt", Py::String(d.version_lt)); + pyDependency.setAttr("version_lte", Py::String(d.version_lte)); + pyDependency.setAttr("version_eq", Py::String(d.version_eq)); + pyDependency.setAttr("version_gt", Py::String(d.version_gt)); + pyDependency.setAttr("version_gte", Py::String(d.version_gte)); + pyDependency.setAttr("condition", Py::String(d.condition)); + return pyDependency; +} + +Py::Object MetadataPy::getDepend(void) const +{ + auto dependencies = getMetadataPtr()->depend(); + Py::List pyDependencies; + for (const auto& d : dependencies) { + pyDependencies.append(dependencyToPyObject(d)); + } + return pyDependencies; +} + +Py::Object MetadataPy::getConflict(void) const +{ + auto dependencies = getMetadataPtr()->conflict(); + Py::List pyDependencies; + for (const auto& d : dependencies) { + pyDependencies.append(dependencyToPyObject(d)); + } + return pyDependencies; +} + +Py::Object MetadataPy::getReplace(void) const +{ + auto dependencies = getMetadataPtr()->replace(); + Py::List pyDependencies; + for (const auto& d : dependencies) { + pyDependencies.append(dependencyToPyObject(d)); + } + return pyDependencies; +} + +// Tag, icon, classname, file + +Py::Object MetadataPy::getTag(void) const +{ + auto tags = getMetadataPtr()->tag(); + Py::List pyTags; + for (const auto& t : tags) { + pyTags.append(Py::String(t)); + } + return pyTags; +} + +Py::Object MetadataPy::getIcon(void) const +{ + return Py::String(getMetadataPtr()->icon().string()); +} + +Py::Object MetadataPy::getClassname(void) const +{ + return Py::String(getMetadataPtr()->classname()); +} + +Py::Object MetadataPy::getFile(void) const +{ + auto files = getMetadataPtr()->file(); + Py::List pyFiles; + for (const auto& f : files) { + pyFiles.append(Py::String(f.string())); + } + return pyFiles; +} + +Py::Object MetadataPy::getContent(void) const +{ + auto content = getMetadataPtr()->content(); + std::set keys; + for (const auto& item : content) { + keys.insert(item.first); + } + + // For the Python, we'll use a dictionary of lists to store the content components: + Py::Dict pyContent; + for (const auto& key : keys) { + Py::List pyContentForKey; + auto elements = content.equal_range(key); + for (auto element = elements.first; element != elements.second; ++element) { + auto contentMetadataItem = new MetadataPy(new Metadata(element->second)); + pyContentForKey.append(Py::asObject(contentMetadataItem)); + } + pyContent[key] = pyContentForKey; + } + return pyContent; +} + +PyObject* MetadataPy::getGenericMetadata(PyObject* args) +{ + const char* name; + if (!PyArg_ParseTuple(args, "s!", &name)) + return NULL; + auto gm = (*getMetadataPtr())[name]; + auto pyGenericMetadata = new Py::List; + for (const auto& item : gm) { + Py::Object pyItem; + pyItem.setAttr("contents", Py::String(item.contents)); + Py::Dict pyAttributes; + for (const auto& attribute : item.attributes) { + pyAttributes[attribute.first] = Py::String(attribute.second); + } + pyItem.setAttr("attributes", pyAttributes); + pyGenericMetadata->append(pyItem); + } + return pyGenericMetadata->ptr(); +} + +PyObject* MetadataPy::getCustomAttributes(const char* /*attr*/) const +{ + return 0; +} + +int MetadataPy::setCustomAttributes(const char* /*attr*/, PyObject* /*obj*/) +{ + return 0; +} \ No newline at end of file From 858e88afe13d0ddf22e1990f412be4cf69ce85a8 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Mon, 19 Apr 2021 12:19:50 -0500 Subject: [PATCH 02/16] [GUI] Add PreferencePack support Preference Packs are collections of preferences that can be applied en mass to the user's current setup. Any preference that can be stored in user.cfg can be stored in a preference pack, and they are designed to be easy to distribute. Support is also added for saving a subset of current preferences into a new preference pack in order to facilitate easy creation of new "themes", etc. --- src/Gui/Application.cpp | 12 + src/Gui/Application.h | 3 + src/Gui/CMakeLists.txt | 8 + src/Gui/DlgCreateNewPreferencePack.ui | 107 +++++ src/Gui/DlgCreateNewPreferencePackImp.cpp | 153 ++++++ src/Gui/DlgCreateNewPreferencePackImp.h | 78 +++ src/Gui/DlgGeneral.ui | 292 +++++++++-- src/Gui/DlgGeneralImp.cpp | 100 ++++ src/Gui/DlgGeneralImp.h | 9 + src/Gui/DlgPreferencesImp.cpp | 13 + src/Gui/DlgPreferencesImp.h | 4 +- src/Gui/PreferencePackManager.cpp | 454 ++++++++++++++++++ src/Gui/PreferencePackManager.h | 190 ++++++++ .../Appearance/Arch_Colors.cfg | 21 + .../Appearance/CMakeLists.txt | 26 + .../Appearance/Console_Colors.cfg | 15 + .../Appearance/Draft_Colors.cfg | 17 + .../Appearance/Editor_Colors.cfg | 26 + .../Appearance/Path_Colors.cfg | 21 + .../Appearance/Sketcher_Colors.cfg | 32 ++ .../Appearance/Start_Colors.cfg | 20 + .../Appearance/TechDraw_Colors.cfg | 31 ++ .../Appearance/Window_Colors.cfg | 31 ++ .../Behavior/CMakeLists.txt | 18 + .../Behavior/Main_window_layout.cfg | 26 + .../PreferencePackTemplates/CMakeLists.txt | 2 + src/Gui/PreferencePacks/CMakeLists.txt | 28 ++ .../FreeCAD Classic Colors.cfg | 134 ++++++ src/Gui/PreferencePacks/package.xml | 19 + 29 files changed, 1858 insertions(+), 32 deletions(-) create mode 100644 src/Gui/DlgCreateNewPreferencePack.ui create mode 100644 src/Gui/DlgCreateNewPreferencePackImp.cpp create mode 100644 src/Gui/DlgCreateNewPreferencePackImp.h create mode 100644 src/Gui/PreferencePackManager.cpp create mode 100644 src/Gui/PreferencePackManager.h create mode 100644 src/Gui/PreferencePackTemplates/Appearance/Arch_Colors.cfg create mode 100644 src/Gui/PreferencePackTemplates/Appearance/CMakeLists.txt create mode 100644 src/Gui/PreferencePackTemplates/Appearance/Console_Colors.cfg create mode 100644 src/Gui/PreferencePackTemplates/Appearance/Draft_Colors.cfg create mode 100644 src/Gui/PreferencePackTemplates/Appearance/Editor_Colors.cfg create mode 100644 src/Gui/PreferencePackTemplates/Appearance/Path_Colors.cfg create mode 100644 src/Gui/PreferencePackTemplates/Appearance/Sketcher_Colors.cfg create mode 100644 src/Gui/PreferencePackTemplates/Appearance/Start_Colors.cfg create mode 100644 src/Gui/PreferencePackTemplates/Appearance/TechDraw_Colors.cfg create mode 100644 src/Gui/PreferencePackTemplates/Appearance/Window_Colors.cfg create mode 100644 src/Gui/PreferencePackTemplates/Behavior/CMakeLists.txt create mode 100644 src/Gui/PreferencePackTemplates/Behavior/Main_window_layout.cfg create mode 100644 src/Gui/PreferencePackTemplates/CMakeLists.txt create mode 100644 src/Gui/PreferencePacks/CMakeLists.txt create mode 100644 src/Gui/PreferencePacks/FreeCAD Classic Colors/FreeCAD Classic Colors.cfg create mode 100644 src/Gui/PreferencePacks/package.xml diff --git a/src/Gui/Application.cpp b/src/Gui/Application.cpp index d59831672b..e159c30a36 100644 --- a/src/Gui/Application.cpp +++ b/src/Gui/Application.cpp @@ -44,6 +44,7 @@ #endif #include +#include #include #if defined(HAVE_QT5_OPENGL) #include @@ -72,6 +73,7 @@ #include "WidgetFactory.h" #include "Command.h" #include "Macro.h" +#include "PreferencePackManager.h" #include "ProgressBar.h" #include "Workbench.h" #include "WorkbenchManager.h" @@ -159,6 +161,9 @@ struct ApplicationP macroMngr = new MacroManager(); else macroMngr = nullptr; + + // Create the Theme Manager + prefPackManager = new PreferencePackManager(); } ~ApplicationP() @@ -172,6 +177,7 @@ struct ApplicationP Gui::Document* activeDocument; Gui::Document* editDocument; MacroManager* macroMngr; + PreferencePackManager* prefPackManager; /// List of all registered views std::list passive; bool isClosing; @@ -1616,6 +1622,12 @@ CommandManager &Application::commandManager(void) return d->commandManager; } +Gui::PreferencePackManager* Application::prefPackManager(void) +{ + return d->prefPackManager; +} + + //************************************************************************** // Init, Destruct and singleton diff --git a/src/Gui/Application.h b/src/Gui/Application.h index 0f596d3276..05b976bfdc 100644 --- a/src/Gui/Application.h +++ b/src/Gui/Application.h @@ -43,6 +43,7 @@ class MacroManager; class MDIView; class MainWindow; class MenuItem; +class PreferencePackManager; class ViewProvider; class ViewProviderDocumentObject; @@ -216,6 +217,8 @@ public: void createStandardOperations(); //@} + Gui::PreferencePackManager* prefPackManager(void); + /** @name Init, Destruct an Access methods */ //@{ /// some kind of singelton diff --git a/src/Gui/CMakeLists.txt b/src/Gui/CMakeLists.txt index e682cc825b..ac49841c8a 100644 --- a/src/Gui/CMakeLists.txt +++ b/src/Gui/CMakeLists.txt @@ -1,5 +1,7 @@ #add_subdirectory(Icons) add_subdirectory(Stylesheets) +add_subdirectory(PreferencePacks) +add_subdirectory(PreferencePackTemplates) if(WIN32) add_definitions(-DFCGui -DQIIS_MAKEDLL -DQSINT_MAKEDLL -DOVR_OS_WIN32 -DQUARTER_INTERNAL -DQUARTER_MAKE_DLL -DCOIN_DLL) @@ -306,6 +308,7 @@ SET(Gui_UIC_SRCS DlgAuthorization.ui DlgChooseIcon.ui DlgCommands.ui + DlgCreateNewPreferencePack.ui DlgCustomizeSpNavSettings.ui DlgDisplayProperties.ui DlgEditor.ui @@ -403,6 +406,7 @@ SET(Dialog_CPP_SRCS Clipping.cpp DemoMode.cpp DlgActivateWindowImp.cpp + DlgCreateNewPreferencePackImp.cpp DlgUnitsCalculatorImp.cpp DlgDisplayPropertiesImp.cpp DlgInputDialogImp.cpp @@ -440,6 +444,7 @@ SET(Dialog_HPP_SRCS Clipping.h DemoMode.h DlgActivateWindowImp.h + DlgCreateNewPreferencePackImp.h DlgUnitsCalculatorImp.h DlgDisplayPropertiesImp.h DlgInputDialogImp.h @@ -499,6 +504,7 @@ SET(Dialog_SRCS DlgCheckableMessageBox.ui DlgTreeWidget.ui DlgExpressionInput.ui + DlgCreateNewPreferencePack.ui DownloadManager.ui DownloadItem.ui DocumentRecovery.ui @@ -1139,6 +1145,7 @@ SET(FreeCADGui_CPP_SRCS resource.cpp Control.cpp SpaceballEvent.cpp + PreferencePackManager.cpp Thumbnail.cpp Utilities.cpp WaitCursor.cpp @@ -1171,6 +1178,7 @@ SET(FreeCADGui_SRCS Qt4All.h Control.h SpaceballEvent.h + PreferencePackManager.h Thumbnail.h Utilities.h WaitCursor.h diff --git a/src/Gui/DlgCreateNewPreferencePack.ui b/src/Gui/DlgCreateNewPreferencePack.ui new file mode 100644 index 0000000000..5165aeffe1 --- /dev/null +++ b/src/Gui/DlgCreateNewPreferencePack.ui @@ -0,0 +1,107 @@ + + + Gui::Dialog::DlgCreateNewPreferencePack + + + Qt::ApplicationModal + + + + 0 + 0 + 580 + 520 + + + + Create New Preference Pack + + + true + + + + + + + + Name + + + + + + + + + + + + 50 + + + 250 + + + true + + + + Property group templates + + + + + Template Type + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + Gui::Dialog::DlgCreateNewPreferencePack + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Gui::Dialog::DlgCreateNewPreferencePack + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/Gui/DlgCreateNewPreferencePackImp.cpp b/src/Gui/DlgCreateNewPreferencePackImp.cpp new file mode 100644 index 0000000000..498199f7b6 --- /dev/null +++ b/src/Gui/DlgCreateNewPreferencePackImp.cpp @@ -0,0 +1,153 @@ +/*************************************************************************** + * Copyright (c) 2021 Chris Hennes * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library 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 library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + + +#include "PreCompiled.h" +#ifndef _PreComp_ +#endif + +#include "DlgCreateNewPreferencePackImp.h" +#include "ui_DlgCreateNewPreferencePack.h" + +#include + +using namespace Gui::Dialog; + +const auto TemplateRole = Qt::UserRole; + +/* TRANSLATOR Gui::Dialog::DlgCreateNewPreferencePackImp */ + +/** + * Constructs a Gui::Dialog::DlgCreateNewPreferencePackImp as a child of 'parent' + */ +DlgCreateNewPreferencePackImp::DlgCreateNewPreferencePackImp(QWidget* parent) + : QDialog(parent) + , ui(new Ui_DlgCreateNewPreferencePack) +{ + ui->setupUi(this); + + QRegExp validNames(QString::fromUtf8("[^/\\\\?%*:|\"<>]+")); + _nameValidator.setRegExp(validNames); + ui->lineEdit->setValidator(&_nameValidator); + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + connect(ui->treeWidget, &QTreeWidget::itemChanged, this, &DlgCreateNewPreferencePackImp::onItemChanged); +} + + +DlgCreateNewPreferencePackImp::~DlgCreateNewPreferencePackImp() +{ +} + +void DlgCreateNewPreferencePackImp::setPreferencePackTemplates(const std::vector& availableTemplates) +{ + ui->treeWidget->clear(); + _groups.clear(); + + ui->treeWidget->header()->setDefaultSectionSize(250); + + _templates = availableTemplates; + for (const auto &t : _templates) { + + QTreeWidgetItem* group; + if (auto foundGroup = _groups.find(t.group); foundGroup != _groups.end()) { + group = foundGroup->second; + } + else { + group = new QTreeWidgetItem(ui->treeWidget, QStringList(QString::fromStdString(t.group))); + group->setCheckState(0, Qt::Checked); + group->setExpanded(true); + _groups.insert(std::make_pair(t.group, group)); + } + + QStringList itemColumns; + itemColumns.push_back(QString::fromStdString(t.name)); + switch (t.type) { + case Gui::PreferencePack::Type::Appearance: itemColumns.push_back(tr("Appearance")); break; + case Gui::PreferencePack::Type::Behavior: itemColumns.push_back(tr("Behavior")); break; + case Gui::PreferencePack::Type::Combination: itemColumns.push_back(tr("Combination")); break; + } + auto newItem = new QTreeWidgetItem(group, itemColumns); + newItem->setCheckState(0, Qt::Checked); + if (group->checkState(0) != newItem->checkState(0)) + group->setCheckState(0, Qt::PartiallyChecked); + newItem->setData(0, TemplateRole, QVariant::fromValue(t)); + group->addChild(newItem); + } +} + +std::vector DlgCreateNewPreferencePackImp::selectedTemplates() const +{ + std::vector results; + + for (const auto& group : _groups) + for (int childIndex = 0; childIndex < group.second->childCount(); ++childIndex) + if (auto child = group.second->child(childIndex); child->checkState(0) == Qt::Checked) + if (child->data(0, TemplateRole).canConvert()) + results.push_back(child->data(0, TemplateRole).value()); + + return results; +} + +std::string DlgCreateNewPreferencePackImp::preferencePackName() const +{ + return ui->lineEdit->text().toStdString(); +} + +void DlgCreateNewPreferencePackImp::onItemChanged(QTreeWidgetItem* item, int column) +{ + Q_UNUSED(column); + const QSignalBlocker blocker(ui->treeWidget); + if (auto group = item->parent(); group) { + // Child clicked + bool firstItemChecked = false; + for (int childIndex = 0; childIndex < group->childCount(); ++childIndex) { + auto child = group->child(childIndex); + if (childIndex == 0) { + firstItemChecked = child->checkState(0) == Qt::Checked; + } + else { + bool thisItemChecked = child->checkState(0) == Qt::Checked; + if (firstItemChecked != thisItemChecked) { + group->setCheckState(0, Qt::PartiallyChecked); + return; + } + } + } + group->setCheckState(0, firstItemChecked ? Qt::Checked : Qt::Unchecked); + } + else { + // Group clicked: + auto groupCheckState = item->checkState(0); + for (int childIndex = 0; childIndex < item->childCount(); ++childIndex) { + auto child = item->child(childIndex); + child->setCheckState(0, groupCheckState); + } + } +} + +void DlgCreateNewPreferencePackImp::on_lineEdit_textEdited(const QString& text) +{ + ui->buttonBox->button(QDialogButtonBox::Ok)->setDisabled(text.isEmpty()); +} + + +#include "moc_DlgCreateNewPreferencePackImp.cpp" diff --git a/src/Gui/DlgCreateNewPreferencePackImp.h b/src/Gui/DlgCreateNewPreferencePackImp.h new file mode 100644 index 0000000000..6e4f75058b --- /dev/null +++ b/src/Gui/DlgCreateNewPreferencePackImp.h @@ -0,0 +1,78 @@ +/*************************************************************************** + * Copyright (c) 2021 Chris Hennes * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library 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 library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + + +#ifndef GUI_DIALOG_DLGCREATENEWTHEMEIMP_H +#define GUI_DIALOG_DLGCREATENEWTHEMEIMP_H + +#include +#include +#include + +#include "PreferencePackManager.h" + +class QTreeWidgetItem; + +namespace Gui { + +namespace Dialog { + +class Ui_DlgCreateNewPreferencePack; + +/** + * \class DlgCreateNewPreferencePackImp + * + * A dialog to request a preferencePack name and a set of preferencePack templates. + * + * \author Chris Hennes + */ +class GuiExport DlgCreateNewPreferencePackImp : public QDialog +{ + Q_OBJECT + +public: + + DlgCreateNewPreferencePackImp(QWidget* parent = nullptr); + ~DlgCreateNewPreferencePackImp(); + + void setPreferencePackTemplates(const std::vector &availableTemplates); + + std::vector selectedTemplates() const; + std::string preferencePackName() const; + +protected Q_SLOTS: + + void onItemChanged(QTreeWidgetItem* item, int column); + + void on_lineEdit_textEdited(const QString &text); + +private: + std::unique_ptr ui; + std::map _groups; + std::vector _templates; + QRegExpValidator _nameValidator; +}; + +} // namespace Dialog +} // namespace Gui + +#endif // GUI_DIALOG_DLGCREATENEWTHEMEIMP_H diff --git a/src/Gui/DlgGeneral.ui b/src/Gui/DlgGeneral.ui index 0b83dac463..411e8cb717 100644 --- a/src/Gui/DlgGeneral.ui +++ b/src/Gui/DlgGeneral.ui @@ -14,7 +14,16 @@ General - + + 9 + + + 9 + + + 9 + + 9 @@ -32,7 +41,16 @@ Language - + + 11 + + + 11 + + + 11 + + 11 @@ -43,7 +61,16 @@ 6 - + + 0 + + + 0 + + + 0 + + 0 @@ -65,6 +92,146 @@ + + + + Preference Packs + + + + + + + + QAbstractItemView::NoEditTriggers + + + false + + + false + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + false + + + true + + + false + + + false + + + true + + + true + + + 75 + + + 200 + + + true + + + false + + + 16 + + + 24 + + + + Name + + + + 75 + true + + + + + + Type + + + + 75 + true + + + + + + + + + + + + 0 + 0 + + + + Run + + + + + + + Save new... + + + + + + + + 0 + 0 + + + + Manage... + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + @@ -74,7 +241,16 @@ 6 - + + 11 + + + 11 + + + 11 + + 11 @@ -82,7 +258,16 @@ 6 - + + 0 + + + 0 + + + 0 + + 0 @@ -131,7 +316,16 @@ 6 - + + 0 + + + 0 + + + 0 + + 0 @@ -152,7 +346,16 @@ See the FreeCAD Wiki for details about the image. 6 - + + 0 + + + 0 + + + 0 + + 0 @@ -176,7 +379,16 @@ See the FreeCAD Wiki for details about the image. 6 - + + 0 + + + 0 + + + 0 + + 0 @@ -201,7 +413,16 @@ this according to your screen size or personal taste 6 - + + 0 + + + 0 + + + 0 + + 0 @@ -214,7 +435,7 @@ this according to your screen size or personal taste - Customize how tree view is shown in the panel (restart required). + Customize how tree view is shown in the panel (restart required). 'ComboView': combine tree view and property view into one panel. 'TreeView and PropertyView': split tree view and property view into separate panel. @@ -233,7 +454,16 @@ this according to your screen size or personal taste Start up - + + 11 + + + 11 + + + 11 + + 11 @@ -262,7 +492,16 @@ display the splash screen - + + 0 + + + 0 + + + 0 + + 0 @@ -294,7 +533,16 @@ after FreeCAD launches Python console - + + 11 + + + 11 + + + 11 + + 11 @@ -323,19 +571,6 @@ horizontal space in Python console - - - - Qt::Vertical - - - - 352 - 221 - - - - @@ -353,11 +588,6 @@ horizontal space in Python console QSpinBox
Gui/PrefWidgets.h
- - Gui::PrefComboBox - QComboBox -
Gui/PrefWidgets.h
-
Languages diff --git a/src/Gui/DlgGeneralImp.cpp b/src/Gui/DlgGeneralImp.cpp index 39075aefa9..b4bd12ebea 100644 --- a/src/Gui/DlgGeneralImp.cpp +++ b/src/Gui/DlgGeneralImp.cpp @@ -33,11 +33,16 @@ #include "ui_DlgGeneral.h" #include "Action.h" #include "Application.h" +#include "Command.h" #include "DockWindowManager.h" #include "MainWindow.h" #include "PrefWidgets.h" #include "PythonConsole.h" #include "Language/Translator.h" +#include "Gui/PreferencePackManager.h" +#include "DlgPreferencesImp.h" + +#include "DlgCreateNewPreferencePackImp.h" using namespace Gui::Dialog; @@ -82,6 +87,15 @@ DlgGeneralImp::DlgGeneralImp( QWidget* parent ) else ui->AutoloadModuleCombo->addItem(px, it.key(), QVariant(it.value())); } + + recreatePreferencePackMenu(); + connect(ui->PreferencePacks, &QTableWidget::itemSelectionChanged, this, &DlgGeneralImp::preferencePackSelectionChanged); + connect(ui->ApplyPreferencePack, &QPushButton::clicked, this, &DlgGeneralImp::applyPreferencePackClicked); + connect(ui->SaveNewPreferencePack, &QPushButton::clicked, this, &DlgGeneralImp::saveAsNewPreferencePack); + + // Future work: the Add-On Manager will be modified to include a section for Preference Packs, at which point this + // button will be enabled to open the Add-On Manager to that tab. + ui->ManagePreferencePacks->hide(); } /** @@ -299,4 +313,90 @@ void DlgGeneralImp::changeEvent(QEvent *e) } } +void DlgGeneralImp::recreatePreferencePackMenu() +{ + // Populate the Preference Packs list + auto appearancePacks = Application::Instance->prefPackManager()->preferencePackNames(PreferencePack::Type::Appearance); + auto behaviorPacks = Application::Instance->prefPackManager()->preferencePackNames(PreferencePack::Type::Behavior); + auto combinationPacks = Application::Instance->prefPackManager()->preferencePackNames(PreferencePack::Type::Combination); + + ui->PreferencePacks->setRowCount(appearancePacks.size() + behaviorPacks.size() + combinationPacks.size()); + + int row = 0; + for (const auto& pack : appearancePacks) { + auto name = new QTableWidgetItem(QString::fromStdString(pack)); + ui->PreferencePacks->setItem(row, 0, name); + auto kind = new QTableWidgetItem(tr("Appearance")); + ui->PreferencePacks->setItem(row, 1, kind); + ++row; + } + for (const auto& pack : behaviorPacks) { + auto name = new QTableWidgetItem(QString::fromStdString(pack)); + ui->PreferencePacks->setItem(row, 0, name); + auto kind = new QTableWidgetItem(tr("Behavior")); + ui->PreferencePacks->setItem(row, 1, kind); + ++row; + } + for (const auto& pack : combinationPacks) { + auto name = new QTableWidgetItem(QString::fromStdString(pack)); + ui->PreferencePacks->setItem(row, 0, name); + auto kind = new QTableWidgetItem(tr("Combination")); + ui->PreferencePacks->setItem(row, 1, kind); + ++row; + } + ui->PreferencePacks->setRangeSelected(QTableWidgetSelectionRange(), true); + ui->ApplyPreferencePack->setEnabled(false); +} + +void DlgGeneralImp::preferencePackSelectionChanged() +{ + if (ui->PreferencePacks->selectedItems().isEmpty()) + ui->ApplyPreferencePack->setEnabled(false); + else + ui->ApplyPreferencePack->setEnabled(true); +} + +void DlgGeneralImp::saveAsNewPreferencePack() +{ + // Create and run a modal New PreferencePack dialog box + newPreferencePackDialog = std::make_unique(this); + newPreferencePackDialog->setPreferencePackTemplates(Application::Instance->prefPackManager()->templateFiles()); + connect(newPreferencePackDialog.get(), &DlgCreateNewPreferencePackImp::accepted, this, &DlgGeneralImp::newPreferencePackDialogAccepted); + newPreferencePackDialog->open(); +} + +void DlgGeneralImp::newPreferencePackDialogAccepted() +{ + auto preferencePackTemplates = Application::Instance->prefPackManager()->templateFiles(); + auto selection = newPreferencePackDialog->selectedTemplates(); + std::vector selectedTemplates; + std::copy_if(preferencePackTemplates.begin(), preferencePackTemplates.end(), std::back_inserter(selectedTemplates), [selection](PreferencePackManager::TemplateFile& t) { + for (const auto& item : selection) + if (item.group == t.group && item.name == t.name) + return true; + return false; + }); + auto preferencePackName = newPreferencePackDialog->preferencePackName(); + Application::Instance->prefPackManager()->save(preferencePackName, selectedTemplates); + Application::Instance->prefPackManager()->rescan(); + recreatePreferencePackMenu(); +} + +void DlgGeneralImp::applyPreferencePackClicked() +{ + auto selectedPreferencePacks = ui->PreferencePacks->selectedItems(); + + for (const auto pack : selectedPreferencePacks) { + if (pack->column() == 0) { + auto packName = pack->text().toStdString(); + if (Application::Instance->prefPackManager()->apply(packName)) { + auto parentDialog = qobject_cast (this->window()); + if (parentDialog) + parentDialog->reload(); + } + } + } +} + + #include "moc_DlgGeneralImp.cpp" diff --git a/src/Gui/DlgGeneralImp.h b/src/Gui/DlgGeneralImp.h index eac13fa8b8..6c928efd3e 100644 --- a/src/Gui/DlgGeneralImp.h +++ b/src/Gui/DlgGeneralImp.h @@ -32,6 +32,7 @@ class QTabWidget; namespace Gui { namespace Dialog { class Ui_DlgGeneral; +class DlgCreateNewPreferencePackImp; /** This class implements the settings for the application. * You can change window style, size of pixmaps, size of recent file list and so on @@ -51,11 +52,19 @@ public: protected: void changeEvent(QEvent *e); +protected Q_SLOTS: + void preferencePackSelectionChanged(); + void applyPreferencePackClicked(); + void recreatePreferencePackMenu(); + void newPreferencePackDialogAccepted(); + private: void setRecentFileSize(); + void saveAsNewPreferencePack(); private: std::unique_ptr ui; + std::unique_ptr newPreferencePackDialog; }; } // namespace Dialog diff --git a/src/Gui/DlgPreferencesImp.cpp b/src/Gui/DlgPreferencesImp.cpp index d547121d94..92a1f1f559 100644 --- a/src/Gui/DlgPreferencesImp.cpp +++ b/src/Gui/DlgPreferencesImp.cpp @@ -466,4 +466,17 @@ void DlgPreferencesImp::changeEvent(QEvent *e) } } +void DlgPreferencesImp::reload() +{ + for (int i = 0; i < ui->tabWidgetStack->count(); i++) { + QTabWidget* tabWidget = (QTabWidget*)ui->tabWidgetStack->widget(i); + for (int j = 0; j < tabWidget->count(); j++) { + PreferencePage* page = qobject_cast(tabWidget->widget(j)); + if (page) + page->loadSettings(); + } + } + applyChanges(); +} + #include "moc_DlgPreferencesImp.cpp" diff --git a/src/Gui/DlgPreferencesImp.h b/src/Gui/DlgPreferencesImp.h index 172baba36d..5ac6fcb190 100644 --- a/src/Gui/DlgPreferencesImp.h +++ b/src/Gui/DlgPreferencesImp.h @@ -113,11 +113,13 @@ class GuiExport DlgPreferencesImp : public QDialog public: static void addPage(const std::string& className, const std::string& group); static void removePage(const std::string& className, const std::string& group); + static void reloadSettings(); DlgPreferencesImp(QWidget* parent = 0, Qt::WindowFlags fl = Qt::WindowFlags()); ~DlgPreferencesImp(); void accept(); + void reload(); void activateGroupPage(const QString& group, int id); protected: @@ -135,11 +137,11 @@ private: /** @name for internal use only */ //@{ void setupPages(); + void reloadPages(); QTabWidget* createTabForGroup(const std::string& groupName); void createPageInGroup(QTabWidget* tabWidget, const std::string& pageName); void applyChanges(); void restoreDefaults(); - void reloadPages(); //@} private: diff --git a/src/Gui/PreferencePackManager.cpp b/src/Gui/PreferencePackManager.cpp new file mode 100644 index 0000000000..3f71314702 --- /dev/null +++ b/src/Gui/PreferencePackManager.cpp @@ -0,0 +1,454 @@ +/*************************************************************************** + * Copyright (c) 2021 Chris Hennes * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library 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 library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + + +#include "PreCompiled.h" + +#ifndef _PreComp_ +# include +# include +# include +#endif + +#include +#include + +#include "PreferencePackManager.h" +#include "App/Metadata.h" +#include "Base/Parameter.h" +#include "Base/Interpreter.h" +#include "Base/Console.h" + +#include + +#include // For generating a timestamped filename + + +using namespace Gui; +using namespace xercesc; +namespace fs = boost::filesystem; + +PreferencePack::PreferencePack(const fs::path& path, const App::Metadata& metadata) : + _path(path), _metadata(metadata) +{ + if (!fs::exists(_path)) { + throw std::runtime_error{ "Cannot access " + path.string() }; + } + + auto qssPaths = QDir::searchPaths(QString::fromUtf8("qss")); + auto cssPaths = QDir::searchPaths(QString::fromUtf8("css")); + + qssPaths.append(QString::fromStdString(_path.string())); + cssPaths.append(QString::fromStdString(_path.string())); + + QDir::setSearchPaths(QString::fromUtf8("qss"), qssPaths); + QDir::setSearchPaths(QString::fromUtf8("css"), cssPaths); +} + +std::string PreferencePack::name() const +{ + return _metadata.name(); +} + +bool PreferencePack::apply() const +{ + // Run the pre.FCMacro, if it exists: if it raises an exception, abort the process + auto preMacroPath = _path / "pre.FCMacro"; + if (fs::exists(preMacroPath)) { + try { + Base::Interpreter().runFile(preMacroPath.string().c_str(), false); + } + catch (...) { + Base::Console().Message("PreferencePack application aborted by the preferencePack's pre.FCMacro"); + return false; + } + } + + // Back up the old config file + auto savedPreferencePacksDirectory = fs::path(App::Application::getUserAppDataDir()) / "SavedPreferencePacks"; + auto backupFile = savedPreferencePacksDirectory / "user.cfg.backup"; + try { + fs::remove(backupFile); + } + catch (...) {} + App::GetApplication().GetUserParameter().SaveDocument(backupFile.string().c_str()); + + // Apply the config settings + applyConfigChanges(); + + // Run the Post.FCMacro, if it exists + auto postMacroPath = _path / "post.FCMacro"; + if (fs::exists(postMacroPath)) { + try { + Base::Interpreter().runFile(postMacroPath.string().c_str(), false); + } + catch (...) { + Base::Console().Message("PreferencePack application reverted by the preferencePack's post.FCMacro"); + App::GetApplication().GetUserParameter().LoadDocument(backupFile.string().c_str()); + return false; + } + } + + return true; +} + +PreferencePack::Type PreferencePack::type() const +{ + auto typeList = _metadata["type"]; + if (typeList.empty()) + return Type::Combination; + + auto typeString = typeList.front().contents; + if (typeString == "appearance") + return Type::Appearance; + else if (typeString == "behavior" || typeString == "behaviour") + return Type::Behavior; + else + return Type::Combination; +} + +void PreferencePack::applyConfigChanges() const +{ + auto configFile = _path / (_metadata.name() + ".cfg"); + if (fs::exists(configFile)) { + ParameterManager newParameters; + newParameters.LoadDocument(configFile.string().c_str()); + auto baseAppGroup = App::GetApplication().GetUserParameter().GetGroup("BaseApp"); + newParameters.GetGroup("BaseApp")->copyTo(baseAppGroup); + } +} + + + +PreferencePackManager::PreferencePackManager() +{ + auto modPath = fs::path(App::Application::getUserAppDataDir()) / "Mod"; + auto savedPath = fs::path(App::Application::getUserAppDataDir()) / "SavedPreferencePacks"; + auto resourcePath = fs::path(App::Application::getResourceDir()) / "Gui" / "PreferencePacks"; + _preferencePackPaths.push_back(resourcePath); + _preferencePackPaths.push_back(modPath); + _preferencePackPaths.push_back(savedPath); + rescan(); + + // Housekeeping: + DeleteOldBackups(); +} + +void PreferencePackManager::rescan() +{ + std::lock_guard lock(_mutex); + for (const auto& path : _preferencePackPaths) { + if (fs::exists(path) && fs::is_directory(path)) { + FindPreferencePacksInPackage(path); + for (const auto& mod : fs::directory_iterator(path)) { + if (fs::is_directory(mod)) { + FindPreferencePacksInPackage(mod); + } + } + } + } +} + +void Gui::PreferencePackManager::FindPreferencePacksInPackage(const fs::path& mod) +{ + auto packageMetadataFile = mod / "package.xml"; + if (fs::exists(packageMetadataFile) && fs::is_regular_file(packageMetadataFile)) { + try { + App::Metadata metadata(packageMetadataFile); + auto content = metadata.content(); + for (const auto& item : content) { + if (item.first == "preferencepack") { + PreferencePack newPreferencePack(mod / item.second.name(), item.second); + _preferencePacks.insert(std::make_pair(newPreferencePack.name(), newPreferencePack)); + } + } + } + catch (...) { + // Failed to read the metadata, or to create the preferencePack based on it... + Base::Console().Error(("Failed to read " + packageMetadataFile.string()).c_str()); + } + } +} + +std::vector PreferencePackManager::preferencePackNames(PreferencePack::Type type) const +{ + std::lock_guard lock(_mutex); + std::vector names; + for (const auto& preferencePack : _preferencePacks) + if (preferencePack.second.type() == type) + names.push_back(preferencePack.first); + return names; +} + +bool PreferencePackManager::apply(const std::string& preferencePackName) const +{ + std::lock_guard lock(_mutex); + if (auto preferencePack = _preferencePacks.find(preferencePackName); preferencePack != _preferencePacks.end()) { + BackupCurrentConfig(); + return preferencePack->second.apply(); + } + else { + throw std::runtime_error("No such Preference Pack: " + preferencePackName); + } +} + +void copyTemplateParameters(Base::Reference templateGroup, const std::string& path, Base::Reference outputGroup) +{ + auto userParameterHandle = App::GetApplication().GetParameterGroupByPath(path.c_str()); + + auto boolMap = templateGroup->GetBoolMap(); + for (const auto& kv : boolMap) { + auto currentValue = userParameterHandle->GetBool(kv.first.c_str(), kv.second); + outputGroup->SetBool(kv.first.c_str(), currentValue); + } + + auto intMap = templateGroup->GetIntMap(); + for (const auto& kv : intMap) { + auto currentValue = userParameterHandle->GetInt(kv.first.c_str(), kv.second); + outputGroup->SetInt(kv.first.c_str(), currentValue); + } + + auto uintMap = templateGroup->GetUnsignedMap(); + for (const auto& kv : uintMap) { + auto currentValue = userParameterHandle->GetUnsigned(kv.first.c_str(), kv.second); + outputGroup->SetUnsigned(kv.first.c_str(), currentValue); + } + + auto floatMap = templateGroup->GetFloatMap(); + for (const auto& kv : floatMap) { + auto currentValue = userParameterHandle->GetFloat(kv.first.c_str(), kv.second); + outputGroup->SetFloat(kv.first.c_str(), currentValue); + } + + auto asciiMap = templateGroup->GetASCIIMap(); + for (const auto& kv : asciiMap) { + auto currentValue = userParameterHandle->GetASCII(kv.first.c_str(), kv.second.c_str()); + outputGroup->SetASCII(kv.first.c_str(), currentValue.c_str()); + } + + // Recurse... + auto templateSubgroups = templateGroup->GetGroups(); + for (auto& templateSubgroup : templateSubgroups) { + std::string sgName = templateSubgroup->GetGroupName(); + auto outputSubgroupHandle = outputGroup->GetGroup(sgName.c_str()); + copyTemplateParameters(templateSubgroup, path + "/" + sgName, outputSubgroupHandle); + } +} + +void copyTemplateParameters(/*const*/ ParameterManager& templateParameterManager, ParameterManager& outputParameterManager) +{ + auto groups = templateParameterManager.GetGroups(); + for (auto& group : groups) { + std::string name = group->GetGroupName(); + auto groupHandle = outputParameterManager.GetGroup(name.c_str()); + copyTemplateParameters(group, "User parameter:" + name, groupHandle); + } +} + +void PreferencePackManager::save(const std::string& name, const std::vector& templates) +{ + if (templates.empty()) + return; + + std::lock_guard lock(_mutex); + auto savedPreferencePacksDirectory = fs::path(App::Application::getUserAppDataDir()) / "SavedPreferencePacks"; + fs::path preferencePackDirectory(savedPreferencePacksDirectory / name); + if (fs::exists(preferencePackDirectory) && !fs::is_directory(preferencePackDirectory)) + throw std::runtime_error("Cannot create " + savedPreferencePacksDirectory.string() + ": file with that name exists already"); + + if (!fs::exists(preferencePackDirectory)) + fs::create_directories(preferencePackDirectory); + + // Create or update the saved user preferencePacks package.xml metadata file + std::unique_ptr metadata; + if (fs::exists(savedPreferencePacksDirectory / "package.xml")) { + metadata = std::make_unique(savedPreferencePacksDirectory / "package.xml"); + } + else { + // Create and set all of the required metadata to make it easier for PreferencePack authors to copy this + // file into their preferencePack distributions. + metadata = std::make_unique(); + metadata->setName("User-Saved PreferencePacks"); + metadata->setDescription("Generated automatically -- edits may be lost when saving new preferencePacks"); + metadata->setVersion(1); + metadata->addMaintainer(App::Meta::Contact("No Maintainer", "email@freecadweb.org")); + metadata->addLicense(App::Meta::License("(Unspecified)", "(Unspecified)")); + metadata->addUrl(App::Meta::Url("https://github.com/FreeCAD/FreeCAD", App::Meta::UrlType::repository)); + } + App::Metadata newPreferencePackMetadata; + newPreferencePackMetadata.setName(name); + newPreferencePackMetadata.setVersion(1); + + auto templateType = templates.front().type; + for (const auto& t : templates) { + if (t.type != templateType) { + templateType = PreferencePack::Type::Combination; + break; + } + } + std::string typeString; + switch (templateType) { + case PreferencePack::Type::Appearance: typeString = "appearance"; break; + case PreferencePack::Type::Behavior: typeString = "behavior"; break; + case PreferencePack::Type::Combination: typeString = "combination"; break; + } + newPreferencePackMetadata.addGenericMetadata("type", App::Meta::GenericMetadata(typeString)); + + metadata->addContentItem("preferencepack", newPreferencePackMetadata); + metadata->write(savedPreferencePacksDirectory / "package.xml"); + + // Create the config file + ParameterManager outputParameterManager; + outputParameterManager.CreateDocument(); + for (const auto& t : templates) { + ParameterManager templateParameterManager; + templateParameterManager.LoadDocument(t.path.string().c_str()); + copyTemplateParameters(templateParameterManager, outputParameterManager); + } + auto cfgFilename = savedPreferencePacksDirectory / name / (name + ".cfg"); + outputParameterManager.SaveDocument(cfgFilename.string().c_str()); +} + +// Needed until we support only C++20 and above and can use std::string's built-in ends_with() +bool fc_ends_with(std::string_view str, std::string_view suffix) +{ + return str.size() >= suffix.size() && str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0; +} + +std::vector scanForTemplateFolders(const std::string& groupName, const fs::path& entry) +{ + // From this location, find the folder(s) called "PreferencePackTemplates" + std::vector templateFolders; + if (fs::exists(entry)) { + if (fs::is_directory(entry)) { + if (entry.filename() == "PreferencePackTemplates" || + entry.filename() == "preference_pack_templates") { + templateFolders.push_back(entry); + } + else { + std::string subgroupName = groupName + "/" + entry.filename().string(); + for (const auto& subentry : fs::directory_iterator(entry)) { + auto contents = scanForTemplateFolders(subgroupName, subentry); + std::copy(contents.begin(), contents.end(), std::back_inserter(templateFolders)); + } + } + } + } + return templateFolders; +} + +std::vector scanForTemplateFiles(const std::string& groupName, const fs::path& entry) +{ + auto templateFolders = scanForTemplateFolders(groupName, entry); + + std::vector templateFiles; + for (const auto& dir : templateFolders) { + auto templateDirs = std::vector>({ + std::make_pair(dir / "Appearance", PreferencePack::Type::Appearance), + std::make_pair(dir / "appearance", PreferencePack::Type::Appearance), + std::make_pair(dir / "Behavior", PreferencePack::Type::Behavior), + std::make_pair(dir / "behavior", PreferencePack::Type::Behavior), + std::make_pair(dir / "Behaviour", PreferencePack::Type::Behavior), + std::make_pair(dir / "behaviour", PreferencePack::Type::Behavior) }); + for (const auto& templateDir : templateDirs) { + if (!fs::exists(templateDir.first) || !fs::is_directory(templateDir.first)) + continue; + for (const auto& entry : fs::directory_iterator(templateDir.first)) { + if (entry.path().extension() == ".cfg") { + auto name = entry.path().filename().stem().string(); + std::replace(name.begin(), name.end(), '_', ' '); + // Make sure we don't insert the same thing twice... + if (std::find_if(templateFiles.begin(), templateFiles.end(), [groupName, name](const auto &rhs)->bool { + return groupName == rhs.group && name == rhs.name; + } ) != templateFiles.end()) + continue; + templateFiles.push_back({ groupName, name, entry, templateDir.second }); + } + } + } + } + return templateFiles; +} + +std::vector PreferencePackManager::templateFiles(bool rescan) +{ + std::lock_guard lock(_mutex); + if (!_templateFiles.empty() && !rescan) + return _templateFiles; + + // Locate all of the template files available on this system + // Template files end in ".cfg" -- They are located in: + // * $INSTALL_DIR/data/Gui/PreferencePackTemplates/(Appearance|Behavior)/* + // * $DATA_DIR/Mod/**/PreferencePackTemplates/(Appearance|Behavior)/* + // (alternate spellings are provided for packages using CamelCase and snake_case, and both major English dialects) + + auto resourcePath = fs::path(App::Application::getResourceDir()) / "Gui"; + auto modPath = fs::path(App::Application::getUserAppDataDir()) / "Mod"; + + std::string group = "Built-In"; + if (fs::exists(resourcePath) && fs::is_directory(resourcePath)) { + const auto localFiles = scanForTemplateFiles(group, resourcePath); + std::copy(localFiles.begin(), localFiles.end(), std::back_inserter(_templateFiles)); + } + + if (fs::exists(modPath) && fs::is_directory(modPath)) { + for (const auto& mod : fs::directory_iterator(modPath)) { + group = mod.path().filename().string(); + const auto localFiles = scanForTemplateFiles(group, mod); + std::copy(localFiles.begin(), localFiles.end(), std::back_inserter(_templateFiles)); + } + } + + return _templateFiles; +} + +void Gui::PreferencePackManager::BackupCurrentConfig() const +{ + auto backupDirectory = fs::path(App::Application::getUserAppDataDir()) / "SavedPreferencePacks" / "Backups"; + fs::create_directories(backupDirectory); + + // Create a timestamped filename: + auto time = std::time(nullptr); + std::ostringstream timestampStream; + timestampStream << "user." << time << ".cfg"; + auto filename = backupDirectory / timestampStream.str(); + + // Save the current config: + App::GetApplication().GetUserParameter().SaveDocument(filename.string().c_str()); +} + +void Gui::PreferencePackManager::DeleteOldBackups() const +{ + constexpr auto oneWeek = 60.0 * 60.0 * 24.0 * 7.0; + const auto now = std::time(nullptr); + auto backupDirectory = fs::path(App::Application::getUserAppDataDir()) / "SavedPreferencePacks" / "Backups"; + if (fs::exists(backupDirectory) && fs::is_directory(backupDirectory)) { + for (const auto& backup : fs::directory_iterator(backupDirectory)) { + if (std::difftime(now, fs::last_write_time(backup)) > oneWeek) { + try { + fs::remove(backup); + } + catch (...) {} + } + } + } +} \ No newline at end of file diff --git a/src/Gui/PreferencePackManager.h b/src/Gui/PreferencePackManager.h new file mode 100644 index 0000000000..33bc43bdf6 --- /dev/null +++ b/src/Gui/PreferencePackManager.h @@ -0,0 +1,190 @@ +/*************************************************************************** + * Copyright (c) 2021 Chris Hennes * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library 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 library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + +#ifndef BASE_THEMEMANAGER_H +#define BASE_THEMEMANAGER_H + +#include +#include +#include + +#include "App/Metadata.h" + +namespace Gui { + + /** + * \class PreferencePack A collection of user preferences stored in files on disk + */ + class PreferencePack { + + public: + + /** + * Construct a preferencePack from a directory + * + * \param path A path to a mod directory that contains a preferencePack + * \param metadata The metadata from the package.xml file describing this preferencePack + */ + PreferencePack(const boost::filesystem::path& path, const App::Metadata& metadata); + + ~PreferencePack() = default; + + /** + * Get the name of the PreferencePack + */ + std::string name() const; + + /** + * Apply the PreferencePack over the top of the current preferences set + * \returns True if the preferencePack was applied, or false if not + */ + bool apply() const; + + enum class Type { + Appearance, + Behavior, + Combination + }; + + /** + * Get the type of PreferencePack (appearance, behavior, or a combination of the two) + */ + Type type() const; + + private: + + void applyConfigChanges() const; + + boost::filesystem::path _path; + App::Metadata _metadata; + + }; + + + + + /** + * \class PreferencePackManager handles storable and loadable collections of user preferences + * + * This class provides some additional utility functions for allowing users to save their current + * preferences as a PreferencePack based on a set of template files provided either in the main + * FreeCAD distribution, or inside various installed mods. + */ + class PreferencePackManager { + public: + PreferencePackManager(); + ~PreferencePackManager() = default; + + /** + * Rescan the preferencePack directory and update the available PreferencePacks + */ + void rescan(); + + /** + * Get an alphabetical list of names of all installed PreferencePacks of a given type + */ + std::vector preferencePackNames(PreferencePack::Type type) const; + + /** + * Apply the named preferencePack + * \return True if the preferencePack was applied, or false if it was not + */ + bool apply(const std::string& preferencePackName) const; + + /** + * \struct TemplateFile A file containing a set of preferences that can be saved into + * a PreferencePack + * + * PreferencePacks can contain any parameters at all, but inside FreeCAD there is no + * centralized list of all of these parameters, and at any given time the user.cfg file + * usually does not store a value for all possible parameters. Instead, it simply allows + * calling code to use whatever default values that code sets. This is all completely + * hidden from the users: FreeCAD behaves as though those values exist in the config file. + * + * When a user saves their current configuration as a pack, they likely expect that saved + * configuration to include those default values, so that if they later apply their saved + * configuration those defaults are restored. To enable this a set of template files + * listing default values for various types of parameters can be used. Each file is + * presented to the user as a checkable box when saving a new preferences pack, and the + * intention is that these files will list out the various user-facing parameters that + * someone might want to save into a preferences pack. + * + * These files are themselves valid user.cfg files, that if loaded and applied will result + * in the default values of their contained variables being set. For this to work, these + * files should be kept up-to-date with the default values set in the code. They are not + * required to contain an exhaustive listing of all possible parameters: in most cases it + * is enough that they list the variables that a user would expect for a given name. For + * example, "Sketcher Colors.cfg" should list out all of the default colors used in + * Sketcher that a user can set in Preferences, but it is not necessary that it contain any + * color that is only used internally, and it should not include things like fonts, or + * behavior information. + * + * Template files are always located in a directory hierarchy that differentiates between + * templates that only affect appearance, and those that affect behavior. + * + * The base FreeCAD installation includes default templates in: + * $INSTALL_DIR/data/Gui/PreferencePackTemplates/(Appearance|Behavior)/ + * + * External add-ons are also searched for any directory called PreferencePackTemplates or + * preference_pack_templates -- either of which is expected to contain appearance and/or + * behavior subdirectories. In this way external add-ons can allow a user to easily save + * their preferences to a PreferencePack, or even to add additional templates representing + * sets of core FreeCAD preferences. + */ + struct TemplateFile { + std::string group; // Generally the Add-On/Mod/Package name + std::string name; + boost::filesystem::path path; + PreferencePack::Type type; + }; + + /** + * Save current settings as a (possibly new) preferencePack + * + * If the named preferencePack does not exist, this creates it on disk. If it does exist, this overwrites the original. + */ + void save(const std::string& name, const std::vector& templates); + + + std::vector templateFiles(bool rescan = false); + + private: + + void FindPreferencePacksInPackage(const boost::filesystem::path& mod); + + void BackupCurrentConfig() const; + + void DeleteOldBackups() const; + + std::vector _preferencePackPaths; + std::vector _templateFiles; + std::map _preferencePacks; + mutable std::mutex _mutex; + + }; + +} + +Q_DECLARE_METATYPE(Gui::PreferencePackManager::TemplateFile) // So it can be used with QVariant + + +#endif diff --git a/src/Gui/PreferencePackTemplates/Appearance/Arch_Colors.cfg b/src/Gui/PreferencePackTemplates/Appearance/Arch_Colors.cfg new file mode 100644 index 0000000000..9b7d90987c --- /dev/null +++ b/src/Gui/PreferencePackTemplates/Appearance/Arch_Colors.cfg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Gui/PreferencePackTemplates/Appearance/CMakeLists.txt b/src/Gui/PreferencePackTemplates/Appearance/CMakeLists.txt new file mode 100644 index 0000000000..6cb3d4c0dc --- /dev/null +++ b/src/Gui/PreferencePackTemplates/Appearance/CMakeLists.txt @@ -0,0 +1,26 @@ + +SET(PreferencePackTemplates_Files + Arch_Colors.cfg + Console_Colors.cfg + Draft_Colors.cfg + Editor_Colors.cfg + Path_Colors.cfg + Sketcher_Colors.cfg + Start_Colors.cfg + TechDraw_Colors.cfg + Window_Colors.cfg +) + +ADD_CUSTOM_TARGET(PreferencePackTemplates_data ALL + SOURCES ${PreferencePackTemplates_Files} +) + +fc_copy_sources(PreferencePackTemplates_data "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_DATADIR}/Gui/PreferencePackTemplates/Appearance" + ${PreferencePackTemplates_Files}) + +INSTALL( + FILES + ${PreferencePackTemplates_Files} + DESTINATION + ${CMAKE_INSTALL_DATADIR}/Gui/PreferencePackTemplates/Appearance +) \ No newline at end of file diff --git a/src/Gui/PreferencePackTemplates/Appearance/Console_Colors.cfg b/src/Gui/PreferencePackTemplates/Appearance/Console_Colors.cfg new file mode 100644 index 0000000000..7e4f4c2d20 --- /dev/null +++ b/src/Gui/PreferencePackTemplates/Appearance/Console_Colors.cfg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/Gui/PreferencePackTemplates/Appearance/Draft_Colors.cfg b/src/Gui/PreferencePackTemplates/Appearance/Draft_Colors.cfg new file mode 100644 index 0000000000..7f68a8e53c --- /dev/null +++ b/src/Gui/PreferencePackTemplates/Appearance/Draft_Colors.cfg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/Gui/PreferencePackTemplates/Appearance/Editor_Colors.cfg b/src/Gui/PreferencePackTemplates/Appearance/Editor_Colors.cfg new file mode 100644 index 0000000000..93ecd1cff1 --- /dev/null +++ b/src/Gui/PreferencePackTemplates/Appearance/Editor_Colors.cfg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Gui/PreferencePackTemplates/Appearance/Path_Colors.cfg b/src/Gui/PreferencePackTemplates/Appearance/Path_Colors.cfg new file mode 100644 index 0000000000..0780cf0255 --- /dev/null +++ b/src/Gui/PreferencePackTemplates/Appearance/Path_Colors.cfg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Gui/PreferencePackTemplates/Appearance/Sketcher_Colors.cfg b/src/Gui/PreferencePackTemplates/Appearance/Sketcher_Colors.cfg new file mode 100644 index 0000000000..44b4d9f535 --- /dev/null +++ b/src/Gui/PreferencePackTemplates/Appearance/Sketcher_Colors.cfg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Gui/PreferencePackTemplates/Appearance/Start_Colors.cfg b/src/Gui/PreferencePackTemplates/Appearance/Start_Colors.cfg new file mode 100644 index 0000000000..2465eae672 --- /dev/null +++ b/src/Gui/PreferencePackTemplates/Appearance/Start_Colors.cfg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/Gui/PreferencePackTemplates/Appearance/TechDraw_Colors.cfg b/src/Gui/PreferencePackTemplates/Appearance/TechDraw_Colors.cfg new file mode 100644 index 0000000000..7a1525ad91 --- /dev/null +++ b/src/Gui/PreferencePackTemplates/Appearance/TechDraw_Colors.cfg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Gui/PreferencePackTemplates/Appearance/Window_Colors.cfg b/src/Gui/PreferencePackTemplates/Appearance/Window_Colors.cfg new file mode 100644 index 0000000000..3821bc48a2 --- /dev/null +++ b/src/Gui/PreferencePackTemplates/Appearance/Window_Colors.cfg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Gui/PreferencePackTemplates/Behavior/CMakeLists.txt b/src/Gui/PreferencePackTemplates/Behavior/CMakeLists.txt new file mode 100644 index 0000000000..7a6174cc9e --- /dev/null +++ b/src/Gui/PreferencePackTemplates/Behavior/CMakeLists.txt @@ -0,0 +1,18 @@ + +SET(PreferencePackBehaviorTemplates_Files + Main_window_layout.cfg +) + +ADD_CUSTOM_TARGET(PreferencePackBehaviorTemplates_data ALL + SOURCES ${PreferencePackBehaviorTemplates_Files} +) + +fc_copy_sources(PreferencePackBehaviorTemplates_data "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_DATADIR}/Gui/PreferencePackTemplates/Behavior" + ${PreferencePackBehaviorTemplates_Files}) + +INSTALL( + FILES + ${PreferencePackBehaviorTemplates_Files} + DESTINATION + ${CMAKE_INSTALL_DATADIR}/Gui/PreferencePackTemplates/Behavior +) \ No newline at end of file diff --git a/src/Gui/PreferencePackTemplates/Behavior/Main_window_layout.cfg b/src/Gui/PreferencePackTemplates/Behavior/Main_window_layout.cfg new file mode 100644 index 0000000000..1e9c8d023c --- /dev/null +++ b/src/Gui/PreferencePackTemplates/Behavior/Main_window_layout.cfg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Gui/PreferencePackTemplates/CMakeLists.txt b/src/Gui/PreferencePackTemplates/CMakeLists.txt new file mode 100644 index 0000000000..b5c71577cf --- /dev/null +++ b/src/Gui/PreferencePackTemplates/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(Appearance) +add_subdirectory(Behavior) \ No newline at end of file diff --git a/src/Gui/PreferencePacks/CMakeLists.txt b/src/Gui/PreferencePacks/CMakeLists.txt new file mode 100644 index 0000000000..e7c235b9e0 --- /dev/null +++ b/src/Gui/PreferencePacks/CMakeLists.txt @@ -0,0 +1,28 @@ + +SET(PreferencePacks_Files +"package.xml" +) + +SET(PreferencePacks_Directories +"FreeCAD Classic Colors" +) + +ADD_CUSTOM_TARGET(PreferencePacks_data ALL +SOURCES ${PreferencePacks_Files} ${PreferencePacks_Directories} +) + +FILE(COPY ${PreferencePacks_Files} ${PreferencePacks_Directories} DESTINATION "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_DATADIR}/Gui/PreferencePacks") + +INSTALL( +FILES + ${PreferencePacks_Files} +DESTINATION + ${CMAKE_INSTALL_DATADIR}/Gui/PreferencePacks +) + +INSTALL( +DIRECTORY + ${PreferencePacks_Directories} +DESTINATION + ${CMAKE_INSTALL_DATADIR}/Gui/PreferencePacks +) \ No newline at end of file diff --git a/src/Gui/PreferencePacks/FreeCAD Classic Colors/FreeCAD Classic Colors.cfg b/src/Gui/PreferencePacks/FreeCAD Classic Colors/FreeCAD Classic Colors.cfg new file mode 100644 index 0000000000..c18797a155 --- /dev/null +++ b/src/Gui/PreferencePacks/FreeCAD Classic Colors/FreeCAD Classic Colors.cfg @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Gui/PreferencePacks/package.xml b/src/Gui/PreferencePacks/package.xml new file mode 100644 index 0000000000..4791afca43 --- /dev/null +++ b/src/Gui/PreferencePacks/package.xml @@ -0,0 +1,19 @@ + + + Built-In Preference Packs + Preference Packs included with the FreeCAD distribution + 1.0.0 + No Maintainer + LGPL2 + https://github.com/FreeCAD/FreeCAD + + + + FreeCAD Classic Colors + FreeCAD default colors for core app and included Mods. + 1.0.0 + appearance + + + + From 0d5a1758b2cab334ac97ec3268b038122a031db3 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Wed, 4 Aug 2021 11:40:31 -0500 Subject: [PATCH 03/16] Fix bug in preference pack application. --- src/Gui/PreferencePackManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gui/PreferencePackManager.cpp b/src/Gui/PreferencePackManager.cpp index 3f71314702..f692abad81 100644 --- a/src/Gui/PreferencePackManager.cpp +++ b/src/Gui/PreferencePackManager.cpp @@ -133,7 +133,7 @@ void PreferencePack::applyConfigChanges() const ParameterManager newParameters; newParameters.LoadDocument(configFile.string().c_str()); auto baseAppGroup = App::GetApplication().GetUserParameter().GetGroup("BaseApp"); - newParameters.GetGroup("BaseApp")->copyTo(baseAppGroup); + newParameters.GetGroup("BaseApp")->insertTo(baseAppGroup); } } From 3acc8c16132cad4aa0e4c513082239289f99cb14 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Wed, 4 Aug 2021 12:17:19 -0500 Subject: [PATCH 04/16] Added Editor_Font.cfg preference pack template. --- .../Appearance/Editor_Font.cfg | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/Gui/PreferencePackTemplates/Appearance/Editor_Font.cfg diff --git a/src/Gui/PreferencePackTemplates/Appearance/Editor_Font.cfg b/src/Gui/PreferencePackTemplates/Appearance/Editor_Font.cfg new file mode 100644 index 0000000000..445b017576 --- /dev/null +++ b/src/Gui/PreferencePackTemplates/Appearance/Editor_Font.cfg @@ -0,0 +1,13 @@ + + + + + + + + MS Shell Dlg 2 + + + + + From 279fdcd926fb4c6b3552cb72c57514a41a5ebb6e Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Fri, 6 Aug 2021 15:38:36 -0500 Subject: [PATCH 05/16] Correct cMake file. --- src/Gui/PreferencePackTemplates/Appearance/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Gui/PreferencePackTemplates/Appearance/CMakeLists.txt b/src/Gui/PreferencePackTemplates/Appearance/CMakeLists.txt index 6cb3d4c0dc..8a88548d5e 100644 --- a/src/Gui/PreferencePackTemplates/Appearance/CMakeLists.txt +++ b/src/Gui/PreferencePackTemplates/Appearance/CMakeLists.txt @@ -4,6 +4,7 @@ SET(PreferencePackTemplates_Files Console_Colors.cfg Draft_Colors.cfg Editor_Colors.cfg + Editor_Font.cfg Path_Colors.cfg Sketcher_Colors.cfg Start_Colors.cfg From 554ea62cd114be2073840d03ea87624d8e389a98 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Thu, 19 Aug 2021 12:29:08 -0500 Subject: [PATCH 06/16] Add code to ensure unique preference pack names. --- src/Gui/DlgCreateNewPreferencePackImp.cpp | 19 +++++++++++++++++++ src/Gui/DlgCreateNewPreferencePackImp.h | 4 ++++ src/Gui/DlgGeneralImp.cpp | 7 +++++++ 3 files changed, 30 insertions(+) diff --git a/src/Gui/DlgCreateNewPreferencePackImp.cpp b/src/Gui/DlgCreateNewPreferencePackImp.cpp index 498199f7b6..94af8a73f7 100644 --- a/src/Gui/DlgCreateNewPreferencePackImp.cpp +++ b/src/Gui/DlgCreateNewPreferencePackImp.cpp @@ -94,6 +94,11 @@ void DlgCreateNewPreferencePackImp::setPreferencePackTemplates(const std::vector } } +void Gui::Dialog::DlgCreateNewPreferencePackImp::setPreferencePackNames(const std::vector& usedNames) +{ + _existingPackNames = usedNames; +} + std::vector DlgCreateNewPreferencePackImp::selectedTemplates() const { std::vector results; @@ -149,5 +154,19 @@ void DlgCreateNewPreferencePackImp::on_lineEdit_textEdited(const QString& text) ui->buttonBox->button(QDialogButtonBox::Ok)->setDisabled(text.isEmpty()); } +void Gui::Dialog::DlgCreateNewPreferencePackImp::accept() +{ + // Ensure that the chosen name is either unique, or that the user actually wants to overwrite the old one + if (auto chosenName = ui->lineEdit->text().toStdString(); + std::find(_existingPackNames.begin(), _existingPackNames.end(), chosenName) != _existingPackNames.end()) { + auto result = QMessageBox::warning(this, tr("Pack already exists"), + tr("A preference pack with that name already exists. Do you want to overwrite it?"), + QMessageBox::Yes | QMessageBox::Cancel); + if (result == QMessageBox::Cancel) + return; + } + QDialog::accept(); +} + #include "moc_DlgCreateNewPreferencePackImp.cpp" diff --git a/src/Gui/DlgCreateNewPreferencePackImp.h b/src/Gui/DlgCreateNewPreferencePackImp.h index 6e4f75058b..b6d0caf705 100644 --- a/src/Gui/DlgCreateNewPreferencePackImp.h +++ b/src/Gui/DlgCreateNewPreferencePackImp.h @@ -55,6 +55,7 @@ public: ~DlgCreateNewPreferencePackImp(); void setPreferencePackTemplates(const std::vector &availableTemplates); + void setPreferencePackNames(const std::vector& usedNames); std::vector selectedTemplates() const; std::string preferencePackName() const; @@ -65,11 +66,14 @@ protected Q_SLOTS: void on_lineEdit_textEdited(const QString &text); + void accept() override; + private: std::unique_ptr ui; std::map _groups; std::vector _templates; QRegExpValidator _nameValidator; + std::vector _existingPackNames; }; } // namespace Dialog diff --git a/src/Gui/DlgGeneralImp.cpp b/src/Gui/DlgGeneralImp.cpp index b4bd12ebea..6f7fc277b9 100644 --- a/src/Gui/DlgGeneralImp.cpp +++ b/src/Gui/DlgGeneralImp.cpp @@ -359,8 +359,15 @@ void DlgGeneralImp::preferencePackSelectionChanged() void DlgGeneralImp::saveAsNewPreferencePack() { // Create and run a modal New PreferencePack dialog box + auto appearancePacks = Application::Instance->prefPackManager()->preferencePackNames(PreferencePack::Type::Appearance); + auto behaviorPacks = Application::Instance->prefPackManager()->preferencePackNames(PreferencePack::Type::Behavior); + auto combinationPacks = Application::Instance->prefPackManager()->preferencePackNames(PreferencePack::Type::Combination); + auto allPacks = appearancePacks; + allPacks.insert(allPacks.end(), behaviorPacks.begin(), behaviorPacks.end()); + allPacks.insert(allPacks.end(), combinationPacks.begin(), combinationPacks.end()); newPreferencePackDialog = std::make_unique(this); newPreferencePackDialog->setPreferencePackTemplates(Application::Instance->prefPackManager()->templateFiles()); + newPreferencePackDialog->setPreferencePackNames(allPacks); connect(newPreferencePackDialog.get(), &DlgCreateNewPreferencePackImp::accepted, this, &DlgGeneralImp::newPreferencePackDialogAccepted); newPreferencePackDialog->open(); } From 525a92cfc56e9a5e6c02179d8f76dcd2b89c6519 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Fri, 20 Aug 2021 20:04:19 -0500 Subject: [PATCH 07/16] Modify GetASCIIMap to include empty strings --- src/Base/Parameter.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Base/Parameter.cpp b/src/Base/Parameter.cpp index 377a669410..53236af1cc 100644 --- a/src/Base/Parameter.cpp +++ b/src/Base/Parameter.cpp @@ -754,6 +754,8 @@ std::vector ParameterGrp::GetASCIIs(const char * sFilter) const DOMNode *pcElem2 = pcTemp->getFirstChild(); if (pcElem2) vrValues.emplace_back(StrXUTF8(pcElem2->getNodeValue()).c_str() ); + else + vrValues.emplace_back(""); // For a string, an empty value is possible and allowed } pcTemp = FindNextElement(pcTemp,"FCText"); } @@ -776,6 +778,8 @@ std::vector > ParameterGrp::GetASCIIMap(const DOMNode *pcElem2 = pcTemp->getFirstChild(); if (pcElem2) vrValues.emplace_back(Name, std::string(StrXUTF8(pcElem2->getNodeValue()).c_str())); + else + vrValues.emplace_back(Name, std::string()); // For a string, an empty value is possible and allowed } pcTemp = FindNextElement(pcTemp,"FCText"); } From 19cd3d5929587ad9d145029110ed5079da6cb655 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Sat, 21 Aug 2021 13:24:00 -0500 Subject: [PATCH 08/16] Add missing header file --- src/Gui/DlgCreateNewPreferencePackImp.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Gui/DlgCreateNewPreferencePackImp.cpp b/src/Gui/DlgCreateNewPreferencePackImp.cpp index 94af8a73f7..74bcfb752f 100644 --- a/src/Gui/DlgCreateNewPreferencePackImp.cpp +++ b/src/Gui/DlgCreateNewPreferencePackImp.cpp @@ -29,6 +29,7 @@ #include "ui_DlgCreateNewPreferencePack.h" #include +#include using namespace Gui::Dialog; From ada7327deec5e156c5c2a64995bc0d4f42675e68 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Sat, 21 Aug 2021 14:12:51 -0500 Subject: [PATCH 09/16] Enabled Manage... button to show directory --- src/Gui/DlgGeneralImp.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Gui/DlgGeneralImp.cpp b/src/Gui/DlgGeneralImp.cpp index 6f7fc277b9..8a965d4f17 100644 --- a/src/Gui/DlgGeneralImp.cpp +++ b/src/Gui/DlgGeneralImp.cpp @@ -44,6 +44,11 @@ #include "DlgCreateNewPreferencePackImp.h" +// Only needed until PreferencePacks can be managed from the AddonManager: +#include +namespace fs = boost::filesystem; + + using namespace Gui::Dialog; /* TRANSLATOR Gui::Dialog::DlgGeneralImp */ @@ -94,8 +99,10 @@ DlgGeneralImp::DlgGeneralImp( QWidget* parent ) connect(ui->SaveNewPreferencePack, &QPushButton::clicked, this, &DlgGeneralImp::saveAsNewPreferencePack); // Future work: the Add-On Manager will be modified to include a section for Preference Packs, at which point this - // button will be enabled to open the Add-On Manager to that tab. - ui->ManagePreferencePacks->hide(); + // button will be modified to open the Add-On Manager to that tab. + auto savedPreferencePacksDirectory = fs::path(App::Application::getUserAppDataDir()) / "SavedPreferencePacks"; + QString pathToSavedPacks(QString::fromStdString(savedPreferencePacksDirectory.string())); + connect(ui->ManagePreferencePacks, &QPushButton::clicked, this, [pathToSavedPacks]() { QDesktopServices::openUrl(QUrl::fromLocalFile(pathToSavedPacks)); }); } /** From 98b34779e517a3affdbaa51c7b04f7fd16706a2d Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Mon, 23 Aug 2021 11:30:13 -0500 Subject: [PATCH 10/16] Add loadState() function to DockWindowManager Just changing the preference for hiding or showing a dock window does not actually trigger a state change. To enable that, the preferences pack manager must manually instruct the DockWindowManager to save its state into the preferences before storing a preference pack, and must instruct the DockWindowManager to load its new state from the preferences after loading a pack. --- src/Gui/DockWindowManager.cpp | 15 +++++++++++++++ src/Gui/DockWindowManager.h | 1 + src/Gui/PreferencePackManager.cpp | 15 ++++++++++++++- 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/Gui/DockWindowManager.cpp b/src/Gui/DockWindowManager.cpp index 2e35a9c85c..45289b00be 100644 --- a/src/Gui/DockWindowManager.cpp +++ b/src/Gui/DockWindowManager.cpp @@ -381,6 +381,21 @@ void DockWindowManager::saveState() } } +void DockWindowManager::loadState() +{ + ParameterGrp::handle hPref = App::GetApplication().GetUserParameter().GetGroup("BaseApp") + ->GetGroup("MainWindow")->GetGroup("DockWindows"); + const QList& dockItems = d->_dockWindowItems.dockWidgets(); + for (QList::ConstIterator it = dockItems.begin(); it != dockItems.end(); ++it) { + QDockWidget* dw = findDockWidget(d->_dockedWindows, it->name); + if (dw) { + QByteArray dockName = it->name.toLatin1(); + bool visible = hPref->GetBool(dockName.constData(), it->visibility); + dw->setVisible(visible); + } + } +} + QDockWidget* DockWindowManager::findDockWidget(const QList& dw, const QString& name) const { for (QList::ConstIterator it = dw.begin(); it != dw.end(); ++it) { diff --git a/src/Gui/DockWindowManager.h b/src/Gui/DockWindowManager.h index a38d89a2b1..5e5825b83a 100644 --- a/src/Gui/DockWindowManager.h +++ b/src/Gui/DockWindowManager.h @@ -88,6 +88,7 @@ public: QList getDockWindows() const; void saveState(); + void loadState(); void retranslate(); private Q_SLOTS: diff --git a/src/Gui/PreferencePackManager.cpp b/src/Gui/PreferencePackManager.cpp index f692abad81..9bdecfee26 100644 --- a/src/Gui/PreferencePackManager.cpp +++ b/src/Gui/PreferencePackManager.cpp @@ -37,6 +37,7 @@ #include "Base/Parameter.h" #include "Base/Interpreter.h" #include "Base/Console.h" +#include "DockWindowManager.h" #include @@ -204,7 +205,14 @@ bool PreferencePackManager::apply(const std::string& preferencePackName) const std::lock_guard lock(_mutex); if (auto preferencePack = _preferencePacks.find(preferencePackName); preferencePack != _preferencePacks.end()) { BackupCurrentConfig(); - return preferencePack->second.apply(); + bool wasApplied = preferencePack->second.apply(); + if (wasApplied) { + // If the visibility state of the dock windows was changed we have to manually reload their state + Gui::DockWindowManager* pDockMgr = Gui::DockWindowManager::instance(); + pDockMgr->loadState(); + + // TODO: Are there other things that have to be manually triggered? + } } else { throw std::runtime_error("No such Preference Pack: " + preferencePackName); @@ -215,9 +223,14 @@ void copyTemplateParameters(Base::Reference templateGroup, const s { auto userParameterHandle = App::GetApplication().GetParameterGroupByPath(path.c_str()); + // Ensure that the DockWindowManager has saved its current state: + Gui::DockWindowManager* pDockMgr = Gui::DockWindowManager::instance(); + pDockMgr->saveState(); + auto boolMap = templateGroup->GetBoolMap(); for (const auto& kv : boolMap) { auto currentValue = userParameterHandle->GetBool(kv.first.c_str(), kv.second); + Base::Console().Message("Parameter %s = %d\n", kv.first.c_str(), currentValue); outputGroup->SetBool(kv.first.c_str(), currentValue); } From 85b32bb3c827bb130b40a5b2815cba3ccec70e17 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Mon, 23 Aug 2021 14:43:30 -0500 Subject: [PATCH 11/16] Fix Linux compilation bugs --- src/Gui/DlgGeneralImp.cpp | 1 + src/Gui/PreferencePackManager.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Gui/DlgGeneralImp.cpp b/src/Gui/DlgGeneralImp.cpp index 8a965d4f17..b1a6f469b9 100644 --- a/src/Gui/DlgGeneralImp.cpp +++ b/src/Gui/DlgGeneralImp.cpp @@ -27,6 +27,7 @@ # include # include # include +# include #endif #include "DlgGeneralImp.h" diff --git a/src/Gui/PreferencePackManager.cpp b/src/Gui/PreferencePackManager.cpp index 9bdecfee26..2978b2c39f 100644 --- a/src/Gui/PreferencePackManager.cpp +++ b/src/Gui/PreferencePackManager.cpp @@ -213,6 +213,7 @@ bool PreferencePackManager::apply(const std::string& preferencePackName) const // TODO: Are there other things that have to be manually triggered? } + return wasApplied; } else { throw std::runtime_error("No such Preference Pack: " + preferencePackName); From 2f7bb5c2706be9d0098f2a6b3cbf432c02891efb Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Mon, 23 Aug 2021 16:01:41 -0500 Subject: [PATCH 12/16] Add ToolBarManager Save and Restore calls --- src/Gui/PreferencePackManager.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Gui/PreferencePackManager.cpp b/src/Gui/PreferencePackManager.cpp index 2978b2c39f..e0260b43e8 100644 --- a/src/Gui/PreferencePackManager.cpp +++ b/src/Gui/PreferencePackManager.cpp @@ -38,6 +38,7 @@ #include "Base/Interpreter.h" #include "Base/Console.h" #include "DockWindowManager.h" +#include "ToolBarManager.h" #include @@ -211,6 +212,10 @@ bool PreferencePackManager::apply(const std::string& preferencePackName) const Gui::DockWindowManager* pDockMgr = Gui::DockWindowManager::instance(); pDockMgr->loadState(); + // Same goes for toolbars: + Gui::ToolBarManager* pToolbarMgr = Gui::ToolBarManager::getInstance(); + pToolbarMgr->restoreState(); + // TODO: Are there other things that have to be manually triggered? } return wasApplied; @@ -228,6 +233,10 @@ void copyTemplateParameters(Base::Reference templateGroup, const s Gui::DockWindowManager* pDockMgr = Gui::DockWindowManager::instance(); pDockMgr->saveState(); + // Do the same for ToolBars + Gui::ToolBarManager* pToolbarMgr = Gui::ToolBarManager::getInstance(); + pToolbarMgr->saveState(); + auto boolMap = templateGroup->GetBoolMap(); for (const auto& kv : boolMap) { auto currentValue = userParameterHandle->GetBool(kv.first.c_str(), kv.second); From 01ba4030ff3efe41774723ec7ad7b4a6cc79bf01 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Tue, 24 Aug 2021 11:49:07 -0500 Subject: [PATCH 13/16] Clean up missing directory behavior --- src/Gui/DlgGeneralImp.cpp | 5 +++++ src/Gui/PreferencePackManager.cpp | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Gui/DlgGeneralImp.cpp b/src/Gui/DlgGeneralImp.cpp index b1a6f469b9..8b64e55cf1 100644 --- a/src/Gui/DlgGeneralImp.cpp +++ b/src/Gui/DlgGeneralImp.cpp @@ -102,6 +102,11 @@ DlgGeneralImp::DlgGeneralImp( QWidget* parent ) // Future work: the Add-On Manager will be modified to include a section for Preference Packs, at which point this // button will be modified to open the Add-On Manager to that tab. auto savedPreferencePacksDirectory = fs::path(App::Application::getUserAppDataDir()) / "SavedPreferencePacks"; + + // If that directory hasn't been created yet, just send the user to the preferences directory + if (!(fs::exists(savedPreferencePacksDirectory) && fs::is_directory(savedPreferencePacksDirectory))) + savedPreferencePacksDirectory = fs::path(App::Application::getUserAppDataDir()); + QString pathToSavedPacks(QString::fromStdString(savedPreferencePacksDirectory.string())); connect(ui->ManagePreferencePacks, &QPushButton::clicked, this, [pathToSavedPacks]() { QDesktopServices::openUrl(QUrl::fromLocalFile(pathToSavedPacks)); }); } diff --git a/src/Gui/PreferencePackManager.cpp b/src/Gui/PreferencePackManager.cpp index e0260b43e8..cb4e8e2a5b 100644 --- a/src/Gui/PreferencePackManager.cpp +++ b/src/Gui/PreferencePackManager.cpp @@ -240,7 +240,6 @@ void copyTemplateParameters(Base::Reference templateGroup, const s auto boolMap = templateGroup->GetBoolMap(); for (const auto& kv : boolMap) { auto currentValue = userParameterHandle->GetBool(kv.first.c_str(), kv.second); - Base::Console().Message("Parameter %s = %d\n", kv.first.c_str(), currentValue); outputGroup->SetBool(kv.first.c_str(), currentValue); } From 4427de68c2bb331e18ab50f66e2057c450103570 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Fri, 24 Sep 2021 16:09:17 -0500 Subject: [PATCH 14/16] Redesign pref pack GUI --- src/Gui/DlgGeneral.ui | 52 +++++++++++---------- src/Gui/DlgGeneralImp.cpp | 95 +++++++++++++++++++-------------------- src/Gui/DlgGeneralImp.h | 3 +- 3 files changed, 73 insertions(+), 77 deletions(-) diff --git a/src/Gui/DlgGeneral.ui b/src/Gui/DlgGeneral.ui index 411e8cb717..6e0f6c1fda 100644 --- a/src/Gui/DlgGeneral.ui +++ b/src/Gui/DlgGeneral.ui @@ -99,9 +99,15 @@
- + + + Qt::ScrollBarAlwaysOff + + + QAbstractScrollArea::AdjustToContents + QAbstractItemView::NoEditTriggers @@ -112,7 +118,7 @@ false
- QAbstractItemView::SingleSelection + QAbstractItemView::NoSelection QAbstractItemView::SelectRows @@ -136,10 +142,10 @@ true - 75 + 30 - 200 + 100 true @@ -175,22 +181,27 @@ + + + Load + +
- + - - - - 0 - 0 - + + + Qt::Horizontal - - Run + + + 40 + 20 + - + @@ -212,19 +223,6 @@
- - - - Qt::Vertical - - - - 20 - 40 - - - - diff --git a/src/Gui/DlgGeneralImp.cpp b/src/Gui/DlgGeneralImp.cpp index 8b64e55cf1..daa38fed61 100644 --- a/src/Gui/DlgGeneralImp.cpp +++ b/src/Gui/DlgGeneralImp.cpp @@ -95,8 +95,6 @@ DlgGeneralImp::DlgGeneralImp( QWidget* parent ) } recreatePreferencePackMenu(); - connect(ui->PreferencePacks, &QTableWidget::itemSelectionChanged, this, &DlgGeneralImp::preferencePackSelectionChanged); - connect(ui->ApplyPreferencePack, &QPushButton::clicked, this, &DlgGeneralImp::applyPreferencePackClicked); connect(ui->SaveNewPreferencePack, &QPushButton::clicked, this, &DlgGeneralImp::saveAsNewPreferencePack); // Future work: the Add-On Manager will be modified to include a section for Preference Packs, at which point this @@ -328,45 +326,53 @@ void DlgGeneralImp::changeEvent(QEvent *e) void DlgGeneralImp::recreatePreferencePackMenu() { - // Populate the Preference Packs list - auto appearancePacks = Application::Instance->prefPackManager()->preferencePackNames(PreferencePack::Type::Appearance); - auto behaviorPacks = Application::Instance->prefPackManager()->preferencePackNames(PreferencePack::Type::Behavior); - auto combinationPacks = Application::Instance->prefPackManager()->preferencePackNames(PreferencePack::Type::Combination); + ui->PreferencePacks->setRowCount(0); // Begin by clearing whatever is there + ui->PreferencePacks->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft); + ui->PreferencePacks->setColumnCount(3); + ui->PreferencePacks->setSelectionMode(QAbstractItemView::SelectionMode::NoSelection); + ui->PreferencePacks->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeMode::Stretch); + ui->PreferencePacks->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeMode::Stretch); + ui->PreferencePacks->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeMode::ResizeToContents); + QStringList columnHeaders; + columnHeaders << tr("Preference Pack Name") + << tr("Type", "Whether a preference pack sets appearance, behavior, or both") + << QString(); // for the "Load" buttons + ui->PreferencePacks->setHorizontalHeaderLabels(columnHeaders); - ui->PreferencePacks->setRowCount(appearancePacks.size() + behaviorPacks.size() + combinationPacks.size()); + // Populate the Preference Packs list + std::map> packNames; + packNames[PreferencePack::Type::Appearance] = Application::Instance->prefPackManager()->preferencePackNames(PreferencePack::Type::Appearance); + packNames[PreferencePack::Type::Behavior] = Application::Instance->prefPackManager()->preferencePackNames(PreferencePack::Type::Behavior); + packNames[PreferencePack::Type::Combination] = Application::Instance->prefPackManager()->preferencePackNames(PreferencePack::Type::Combination); + + ui->PreferencePacks->setRowCount( + packNames[PreferencePack::Type::Appearance].size() + + packNames[PreferencePack::Type::Behavior].size() + + packNames[PreferencePack::Type::Combination].size()); int row = 0; - for (const auto& pack : appearancePacks) { - auto name = new QTableWidgetItem(QString::fromStdString(pack)); - ui->PreferencePacks->setItem(row, 0, name); - auto kind = new QTableWidgetItem(tr("Appearance")); - ui->PreferencePacks->setItem(row, 1, kind); - ++row; - } - for (const auto& pack : behaviorPacks) { - auto name = new QTableWidgetItem(QString::fromStdString(pack)); - ui->PreferencePacks->setItem(row, 0, name); - auto kind = new QTableWidgetItem(tr("Behavior")); - ui->PreferencePacks->setItem(row, 1, kind); - ++row; - } - for (const auto& pack : combinationPacks) { - auto name = new QTableWidgetItem(QString::fromStdString(pack)); - ui->PreferencePacks->setItem(row, 0, name); - auto kind = new QTableWidgetItem(tr("Combination")); - ui->PreferencePacks->setItem(row, 1, kind); - ++row; + for (const auto& packGroup : packNames) { + for (const auto& pack : packGroup.second) { + auto name = new QTableWidgetItem(QString::fromStdString(pack)); + ui->PreferencePacks->setItem(row, 0, name); + QTableWidgetItem* kind; + switch (packGroup.first) { + case PreferencePack::Type::Appearance: kind = new QTableWidgetItem(tr("Appearance")); break; + case PreferencePack::Type::Behavior: kind = new QTableWidgetItem(tr("Behavior")); break; + case PreferencePack::Type::Combination: kind = new QTableWidgetItem(tr("Combination")); break; + default: kind = new QTableWidgetItem(QString::fromUtf8("[ERR: UNKNOWN TYPE]")); break; + } + ui->PreferencePacks->setItem(row, 1, kind); + auto button = new QPushButton(tr("Apply")); + button->setToolTip(tr("Apply the ") + + QString::fromStdString(pack) + QString::fromUtf8(" ") + + tr("Preference Pack")); + connect(button, &QPushButton::clicked, this, [this, pack]() { onLoadPreferencePackClicked(pack); }); + ui->PreferencePacks->setCellWidget(row, 2, button); + ++row; + } } ui->PreferencePacks->setRangeSelected(QTableWidgetSelectionRange(), true); - ui->ApplyPreferencePack->setEnabled(false); -} - -void DlgGeneralImp::preferencePackSelectionChanged() -{ - if (ui->PreferencePacks->selectedItems().isEmpty()) - ui->ApplyPreferencePack->setEnabled(false); - else - ui->ApplyPreferencePack->setEnabled(true); } void DlgGeneralImp::saveAsNewPreferencePack() @@ -402,19 +408,12 @@ void DlgGeneralImp::newPreferencePackDialogAccepted() recreatePreferencePackMenu(); } -void DlgGeneralImp::applyPreferencePackClicked() +void DlgGeneralImp::onLoadPreferencePackClicked(const std::string& packName) { - auto selectedPreferencePacks = ui->PreferencePacks->selectedItems(); - - for (const auto pack : selectedPreferencePacks) { - if (pack->column() == 0) { - auto packName = pack->text().toStdString(); - if (Application::Instance->prefPackManager()->apply(packName)) { - auto parentDialog = qobject_cast (this->window()); - if (parentDialog) - parentDialog->reload(); - } - } + if (Application::Instance->prefPackManager()->apply(packName)) { + auto parentDialog = qobject_cast (this->window()); + if (parentDialog) + parentDialog->reload(); } } diff --git a/src/Gui/DlgGeneralImp.h b/src/Gui/DlgGeneralImp.h index 6c928efd3e..1e08282669 100644 --- a/src/Gui/DlgGeneralImp.h +++ b/src/Gui/DlgGeneralImp.h @@ -53,8 +53,7 @@ protected: void changeEvent(QEvent *e); protected Q_SLOTS: - void preferencePackSelectionChanged(); - void applyPreferencePackClicked(); + void onLoadPreferencePackClicked(const std::string &packName); void recreatePreferencePackMenu(); void newPreferencePackDialogAccepted(); From c8a019c312544c5f695532203e69b4bda55034d0 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Sat, 25 Sep 2021 17:17:35 -0500 Subject: [PATCH 15/16] Improve display of Apply button --- src/Gui/DlgGeneralImp.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Gui/DlgGeneralImp.cpp b/src/Gui/DlgGeneralImp.cpp index daa38fed61..697c263213 100644 --- a/src/Gui/DlgGeneralImp.cpp +++ b/src/Gui/DlgGeneralImp.cpp @@ -330,6 +330,7 @@ void DlgGeneralImp::recreatePreferencePackMenu() ui->PreferencePacks->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft); ui->PreferencePacks->setColumnCount(3); ui->PreferencePacks->setSelectionMode(QAbstractItemView::SelectionMode::NoSelection); + ui->PreferencePacks->horizontalHeader()->setStretchLastSection(false); ui->PreferencePacks->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeMode::Stretch); ui->PreferencePacks->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeMode::Stretch); ui->PreferencePacks->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeMode::ResizeToContents); @@ -351,6 +352,7 @@ void DlgGeneralImp::recreatePreferencePackMenu() packNames[PreferencePack::Type::Combination].size()); int row = 0; + QIcon icon = style()->standardIcon(QStyle::SP_DialogApplyButton); for (const auto& packGroup : packNames) { for (const auto& pack : packGroup.second) { auto name = new QTableWidgetItem(QString::fromStdString(pack)); @@ -363,16 +365,13 @@ void DlgGeneralImp::recreatePreferencePackMenu() default: kind = new QTableWidgetItem(QString::fromUtf8("[ERR: UNKNOWN TYPE]")); break; } ui->PreferencePacks->setItem(row, 1, kind); - auto button = new QPushButton(tr("Apply")); - button->setToolTip(tr("Apply the ") + - QString::fromStdString(pack) + QString::fromUtf8(" ") + - tr("Preference Pack")); + auto button = new QPushButton(icon, tr("Apply")); + button->setToolTip(tr("Apply the %1 preference pack").arg(QString::fromStdString(pack))); connect(button, &QPushButton::clicked, this, [this, pack]() { onLoadPreferencePackClicked(pack); }); ui->PreferencePacks->setCellWidget(row, 2, button); ++row; } } - ui->PreferencePacks->setRangeSelected(QTableWidgetSelectionRange(), true); } void DlgGeneralImp::saveAsNewPreferencePack() From 1076ba0df52049025b75a705217aba708eb2db38 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Mon, 11 Oct 2021 13:22:28 -0500 Subject: [PATCH 16/16] [Gui] PrefPacks - remove "type" parameter In display, show the "tags" instead. --- src/Gui/DlgCreateNewPreferencePack.ui | 8 +- src/Gui/DlgCreateNewPreferencePackImp.cpp | 5 -- src/Gui/DlgGeneralImp.cpp | 61 +++++++-------- src/Gui/PreferencePackManager.cpp | 75 ++++++------------- src/Gui/PreferencePackManager.h | 26 +++---- .../Appearance/CMakeLists.txt | 27 ------- .../{Appearance => }/Arch_Colors.cfg | 0 .../Behavior/CMakeLists.txt | 18 ----- .../PreferencePackTemplates/CMakeLists.txt | 30 +++++++- .../{Appearance => }/Console_Colors.cfg | 0 .../{Appearance => }/Draft_Colors.cfg | 0 .../{Appearance => }/Editor_Colors.cfg | 0 .../{Appearance => }/Editor_Font.cfg | 0 .../{Behavior => }/Main_window_layout.cfg | 0 .../{Appearance => }/Path_Colors.cfg | 0 .../{Appearance => }/Sketcher_Colors.cfg | 0 .../{Appearance => }/Start_Colors.cfg | 0 .../{Appearance => }/TechDraw_Colors.cfg | 0 .../{Appearance => }/Window_Colors.cfg | 0 src/Gui/PreferencePacks/package.xml | 3 +- 20 files changed, 95 insertions(+), 158 deletions(-) delete mode 100644 src/Gui/PreferencePackTemplates/Appearance/CMakeLists.txt rename src/Gui/PreferencePackTemplates/{Appearance => }/Arch_Colors.cfg (100%) delete mode 100644 src/Gui/PreferencePackTemplates/Behavior/CMakeLists.txt rename src/Gui/PreferencePackTemplates/{Appearance => }/Console_Colors.cfg (100%) rename src/Gui/PreferencePackTemplates/{Appearance => }/Draft_Colors.cfg (100%) rename src/Gui/PreferencePackTemplates/{Appearance => }/Editor_Colors.cfg (100%) rename src/Gui/PreferencePackTemplates/{Appearance => }/Editor_Font.cfg (100%) rename src/Gui/PreferencePackTemplates/{Behavior => }/Main_window_layout.cfg (100%) rename src/Gui/PreferencePackTemplates/{Appearance => }/Path_Colors.cfg (100%) rename src/Gui/PreferencePackTemplates/{Appearance => }/Sketcher_Colors.cfg (100%) rename src/Gui/PreferencePackTemplates/{Appearance => }/Start_Colors.cfg (100%) rename src/Gui/PreferencePackTemplates/{Appearance => }/TechDraw_Colors.cfg (100%) rename src/Gui/PreferencePackTemplates/{Appearance => }/Window_Colors.cfg (100%) diff --git a/src/Gui/DlgCreateNewPreferencePack.ui b/src/Gui/DlgCreateNewPreferencePack.ui index 5165aeffe1..98f4589f0e 100644 --- a/src/Gui/DlgCreateNewPreferencePack.ui +++ b/src/Gui/DlgCreateNewPreferencePack.ui @@ -36,6 +36,9 @@ + + 1 + 50 @@ -50,11 +53,6 @@ Property group templates - - - Template Type - - diff --git a/src/Gui/DlgCreateNewPreferencePackImp.cpp b/src/Gui/DlgCreateNewPreferencePackImp.cpp index 74bcfb752f..47c6bda8f0 100644 --- a/src/Gui/DlgCreateNewPreferencePackImp.cpp +++ b/src/Gui/DlgCreateNewPreferencePackImp.cpp @@ -81,11 +81,6 @@ void DlgCreateNewPreferencePackImp::setPreferencePackTemplates(const std::vector QStringList itemColumns; itemColumns.push_back(QString::fromStdString(t.name)); - switch (t.type) { - case Gui::PreferencePack::Type::Appearance: itemColumns.push_back(tr("Appearance")); break; - case Gui::PreferencePack::Type::Behavior: itemColumns.push_back(tr("Behavior")); break; - case Gui::PreferencePack::Type::Combination: itemColumns.push_back(tr("Combination")); break; - } auto newItem = new QTreeWidgetItem(group, itemColumns); newItem->setCheckState(0, Qt::Checked); if (group->checkState(0) != newItem->checkState(0)) diff --git a/src/Gui/DlgGeneralImp.cpp b/src/Gui/DlgGeneralImp.cpp index 697c263213..a1053da4dd 100644 --- a/src/Gui/DlgGeneralImp.cpp +++ b/src/Gui/DlgGeneralImp.cpp @@ -102,10 +102,13 @@ DlgGeneralImp::DlgGeneralImp( QWidget* parent ) auto savedPreferencePacksDirectory = fs::path(App::Application::getUserAppDataDir()) / "SavedPreferencePacks"; // If that directory hasn't been created yet, just send the user to the preferences directory - if (!(fs::exists(savedPreferencePacksDirectory) && fs::is_directory(savedPreferencePacksDirectory))) + if (!(fs::exists(savedPreferencePacksDirectory) && fs::is_directory(savedPreferencePacksDirectory))) { savedPreferencePacksDirectory = fs::path(App::Application::getUserAppDataDir()); + ui->ManagePreferencePacks->hide(); + } QString pathToSavedPacks(QString::fromStdString(savedPreferencePacksDirectory.string())); + ui->ManagePreferencePacks->setToolTip(tr("Open the directory of saved user preference packs")); connect(ui->ManagePreferencePacks, &QPushButton::clicked, this, [pathToSavedPacks]() { QDesktopServices::openUrl(QUrl::fromLocalFile(pathToSavedPacks)); }); } @@ -336,56 +339,46 @@ void DlgGeneralImp::recreatePreferencePackMenu() ui->PreferencePacks->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeMode::ResizeToContents); QStringList columnHeaders; columnHeaders << tr("Preference Pack Name") - << tr("Type", "Whether a preference pack sets appearance, behavior, or both") + << tr("Tags") << QString(); // for the "Load" buttons ui->PreferencePacks->setHorizontalHeaderLabels(columnHeaders); // Populate the Preference Packs list - std::map> packNames; - packNames[PreferencePack::Type::Appearance] = Application::Instance->prefPackManager()->preferencePackNames(PreferencePack::Type::Appearance); - packNames[PreferencePack::Type::Behavior] = Application::Instance->prefPackManager()->preferencePackNames(PreferencePack::Type::Behavior); - packNames[PreferencePack::Type::Combination] = Application::Instance->prefPackManager()->preferencePackNames(PreferencePack::Type::Combination); + auto packs = Application::Instance->prefPackManager()->preferencePacks(); - ui->PreferencePacks->setRowCount( - packNames[PreferencePack::Type::Appearance].size() + - packNames[PreferencePack::Type::Behavior].size() + - packNames[PreferencePack::Type::Combination].size()); + ui->PreferencePacks->setRowCount(packs.size()); int row = 0; QIcon icon = style()->standardIcon(QStyle::SP_DialogApplyButton); - for (const auto& packGroup : packNames) { - for (const auto& pack : packGroup.second) { - auto name = new QTableWidgetItem(QString::fromStdString(pack)); - ui->PreferencePacks->setItem(row, 0, name); - QTableWidgetItem* kind; - switch (packGroup.first) { - case PreferencePack::Type::Appearance: kind = new QTableWidgetItem(tr("Appearance")); break; - case PreferencePack::Type::Behavior: kind = new QTableWidgetItem(tr("Behavior")); break; - case PreferencePack::Type::Combination: kind = new QTableWidgetItem(tr("Combination")); break; - default: kind = new QTableWidgetItem(QString::fromUtf8("[ERR: UNKNOWN TYPE]")); break; - } - ui->PreferencePacks->setItem(row, 1, kind); - auto button = new QPushButton(icon, tr("Apply")); - button->setToolTip(tr("Apply the %1 preference pack").arg(QString::fromStdString(pack))); - connect(button, &QPushButton::clicked, this, [this, pack]() { onLoadPreferencePackClicked(pack); }); - ui->PreferencePacks->setCellWidget(row, 2, button); - ++row; + for (const auto& pack : packs) { + auto name = new QTableWidgetItem(QString::fromStdString(pack.first)); + name->setToolTip(QString::fromStdString(pack.second.metadata().description())); + ui->PreferencePacks->setItem(row, 0, name); + auto tags = pack.second.metadata().tag(); + QString tagString; + for (const auto& tag : tags) { + if (tagString.isEmpty()) + tagString.append(QString::fromStdString(tag)); + else + tagString.append(QStringLiteral(", ") + QString::fromStdString(tag)); } + QTableWidgetItem* kind = new QTableWidgetItem(tagString); + ui->PreferencePacks->setItem(row, 1, kind); + auto button = new QPushButton(icon, tr("Apply")); + button->setToolTip(tr("Apply the %1 preference pack").arg(QString::fromStdString(pack.first))); + connect(button, &QPushButton::clicked, this, [this, pack]() { onLoadPreferencePackClicked(pack.first); }); + ui->PreferencePacks->setCellWidget(row, 2, button); + ++row; } } void DlgGeneralImp::saveAsNewPreferencePack() { // Create and run a modal New PreferencePack dialog box - auto appearancePacks = Application::Instance->prefPackManager()->preferencePackNames(PreferencePack::Type::Appearance); - auto behaviorPacks = Application::Instance->prefPackManager()->preferencePackNames(PreferencePack::Type::Behavior); - auto combinationPacks = Application::Instance->prefPackManager()->preferencePackNames(PreferencePack::Type::Combination); - auto allPacks = appearancePacks; - allPacks.insert(allPacks.end(), behaviorPacks.begin(), behaviorPacks.end()); - allPacks.insert(allPacks.end(), combinationPacks.begin(), combinationPacks.end()); + auto packs = Application::Instance->prefPackManager()->preferencePackNames(); newPreferencePackDialog = std::make_unique(this); newPreferencePackDialog->setPreferencePackTemplates(Application::Instance->prefPackManager()->templateFiles()); - newPreferencePackDialog->setPreferencePackNames(allPacks); + newPreferencePackDialog->setPreferencePackNames(packs); connect(newPreferencePackDialog.get(), &DlgCreateNewPreferencePackImp::accepted, this, &DlgGeneralImp::newPreferencePackDialogAccepted); newPreferencePackDialog->open(); } diff --git a/src/Gui/PreferencePackManager.cpp b/src/Gui/PreferencePackManager.cpp index cb4e8e2a5b..3bb79c2c5e 100644 --- a/src/Gui/PreferencePackManager.cpp +++ b/src/Gui/PreferencePackManager.cpp @@ -113,19 +113,10 @@ bool PreferencePack::apply() const return true; } -PreferencePack::Type PreferencePack::type() const -{ - auto typeList = _metadata["type"]; - if (typeList.empty()) - return Type::Combination; - auto typeString = typeList.front().contents; - if (typeString == "appearance") - return Type::Appearance; - else if (typeString == "behavior" || typeString == "behaviour") - return Type::Behavior; - else - return Type::Combination; +App::Metadata Gui::PreferencePack::metadata() const +{ + return _metadata; } void PreferencePack::applyConfigChanges() const @@ -191,16 +182,20 @@ void Gui::PreferencePackManager::FindPreferencePacksInPackage(const fs::path& mo } } -std::vector PreferencePackManager::preferencePackNames(PreferencePack::Type type) const +std::vector PreferencePackManager::preferencePackNames() const { std::lock_guard lock(_mutex); std::vector names; for (const auto& preferencePack : _preferencePacks) - if (preferencePack.second.type() == type) - names.push_back(preferencePack.first); + names.push_back(preferencePack.first); return names; } +std::map Gui::PreferencePackManager::preferencePacks() const +{ + return _preferencePacks; +} + bool PreferencePackManager::apply(const std::string& preferencePackName) const { std::lock_guard lock(_mutex); @@ -320,21 +315,6 @@ void PreferencePackManager::save(const std::string& name, const std::vectoraddContentItem("preferencepack", newPreferencePackMetadata); metadata->write(savedPreferencePacksDirectory / "package.xml"); @@ -383,28 +363,19 @@ std::vector scanForTemplateFiles(const std: auto templateFolders = scanForTemplateFolders(groupName, entry); std::vector templateFiles; - for (const auto& dir : templateFolders) { - auto templateDirs = std::vector>({ - std::make_pair(dir / "Appearance", PreferencePack::Type::Appearance), - std::make_pair(dir / "appearance", PreferencePack::Type::Appearance), - std::make_pair(dir / "Behavior", PreferencePack::Type::Behavior), - std::make_pair(dir / "behavior", PreferencePack::Type::Behavior), - std::make_pair(dir / "Behaviour", PreferencePack::Type::Behavior), - std::make_pair(dir / "behaviour", PreferencePack::Type::Behavior) }); - for (const auto& templateDir : templateDirs) { - if (!fs::exists(templateDir.first) || !fs::is_directory(templateDir.first)) - continue; - for (const auto& entry : fs::directory_iterator(templateDir.first)) { - if (entry.path().extension() == ".cfg") { - auto name = entry.path().filename().stem().string(); - std::replace(name.begin(), name.end(), '_', ' '); - // Make sure we don't insert the same thing twice... - if (std::find_if(templateFiles.begin(), templateFiles.end(), [groupName, name](const auto &rhs)->bool { - return groupName == rhs.group && name == rhs.name; - } ) != templateFiles.end()) - continue; - templateFiles.push_back({ groupName, name, entry, templateDir.second }); - } + for (const auto& templateDir : templateFolders) { + if (!fs::exists(templateDir) || !fs::is_directory(templateDir)) + continue; + for (const auto& entry : fs::directory_iterator(templateDir)) { + if (entry.path().extension() == ".cfg") { + auto name = entry.path().filename().stem().string(); + std::replace(name.begin(), name.end(), '_', ' '); + // Make sure we don't insert the same thing twice... + if (std::find_if(templateFiles.begin(), templateFiles.end(), [groupName, name](const auto &rhs)->bool { + return groupName == rhs.group && name == rhs.name; + } ) != templateFiles.end()) + continue; + templateFiles.push_back({ groupName, name, entry }); } } } diff --git a/src/Gui/PreferencePackManager.h b/src/Gui/PreferencePackManager.h index 33bc43bdf6..78fd80c236 100644 --- a/src/Gui/PreferencePackManager.h +++ b/src/Gui/PreferencePackManager.h @@ -20,8 +20,8 @@ * * ***************************************************************************/ -#ifndef BASE_THEMEMANAGER_H -#define BASE_THEMEMANAGER_H +#ifndef BASE_PREFERENCEPACKMANAGER_H +#define BASE_PREFERENCEPACKMANAGER_H #include #include @@ -59,16 +59,10 @@ namespace Gui { */ bool apply() const; - enum class Type { - Appearance, - Behavior, - Combination - }; - /** - * Get the type of PreferencePack (appearance, behavior, or a combination of the two) + * Get the complete metadata object for this preference pack */ - Type type() const; + App::Metadata metadata() const; private: @@ -100,9 +94,14 @@ namespace Gui { void rescan(); /** - * Get an alphabetical list of names of all installed PreferencePacks of a given type + * Get an alphabetical list of names of all installed PreferencePacks */ - std::vector preferencePackNames(PreferencePack::Type type) const; + std::vector preferencePackNames() const; + + /** + * Get a map of all installed PreferencePack names and their associated packs + */ + std::map preferencePacks() const; /** * Apply the named preferencePack @@ -142,7 +141,7 @@ namespace Gui { * templates that only affect appearance, and those that affect behavior. * * The base FreeCAD installation includes default templates in: - * $INSTALL_DIR/data/Gui/PreferencePackTemplates/(Appearance|Behavior)/ + * $INSTALL_DIR/data/Gui/PreferencePackTemplates/ * * External add-ons are also searched for any directory called PreferencePackTemplates or * preference_pack_templates -- either of which is expected to contain appearance and/or @@ -154,7 +153,6 @@ namespace Gui { std::string group; // Generally the Add-On/Mod/Package name std::string name; boost::filesystem::path path; - PreferencePack::Type type; }; /** diff --git a/src/Gui/PreferencePackTemplates/Appearance/CMakeLists.txt b/src/Gui/PreferencePackTemplates/Appearance/CMakeLists.txt deleted file mode 100644 index 8a88548d5e..0000000000 --- a/src/Gui/PreferencePackTemplates/Appearance/CMakeLists.txt +++ /dev/null @@ -1,27 +0,0 @@ - -SET(PreferencePackTemplates_Files - Arch_Colors.cfg - Console_Colors.cfg - Draft_Colors.cfg - Editor_Colors.cfg - Editor_Font.cfg - Path_Colors.cfg - Sketcher_Colors.cfg - Start_Colors.cfg - TechDraw_Colors.cfg - Window_Colors.cfg -) - -ADD_CUSTOM_TARGET(PreferencePackTemplates_data ALL - SOURCES ${PreferencePackTemplates_Files} -) - -fc_copy_sources(PreferencePackTemplates_data "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_DATADIR}/Gui/PreferencePackTemplates/Appearance" - ${PreferencePackTemplates_Files}) - -INSTALL( - FILES - ${PreferencePackTemplates_Files} - DESTINATION - ${CMAKE_INSTALL_DATADIR}/Gui/PreferencePackTemplates/Appearance -) \ No newline at end of file diff --git a/src/Gui/PreferencePackTemplates/Appearance/Arch_Colors.cfg b/src/Gui/PreferencePackTemplates/Arch_Colors.cfg similarity index 100% rename from src/Gui/PreferencePackTemplates/Appearance/Arch_Colors.cfg rename to src/Gui/PreferencePackTemplates/Arch_Colors.cfg diff --git a/src/Gui/PreferencePackTemplates/Behavior/CMakeLists.txt b/src/Gui/PreferencePackTemplates/Behavior/CMakeLists.txt deleted file mode 100644 index 7a6174cc9e..0000000000 --- a/src/Gui/PreferencePackTemplates/Behavior/CMakeLists.txt +++ /dev/null @@ -1,18 +0,0 @@ - -SET(PreferencePackBehaviorTemplates_Files - Main_window_layout.cfg -) - -ADD_CUSTOM_TARGET(PreferencePackBehaviorTemplates_data ALL - SOURCES ${PreferencePackBehaviorTemplates_Files} -) - -fc_copy_sources(PreferencePackBehaviorTemplates_data "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_DATADIR}/Gui/PreferencePackTemplates/Behavior" - ${PreferencePackBehaviorTemplates_Files}) - -INSTALL( - FILES - ${PreferencePackBehaviorTemplates_Files} - DESTINATION - ${CMAKE_INSTALL_DATADIR}/Gui/PreferencePackTemplates/Behavior -) \ No newline at end of file diff --git a/src/Gui/PreferencePackTemplates/CMakeLists.txt b/src/Gui/PreferencePackTemplates/CMakeLists.txt index b5c71577cf..e9023ab237 100644 --- a/src/Gui/PreferencePackTemplates/CMakeLists.txt +++ b/src/Gui/PreferencePackTemplates/CMakeLists.txt @@ -1,2 +1,28 @@ -add_subdirectory(Appearance) -add_subdirectory(Behavior) \ No newline at end of file + +SET(PreferencePackTemplates_Files + Arch_Colors.cfg + Console_Colors.cfg + Draft_Colors.cfg + Editor_Colors.cfg + Editor_Font.cfg + Main_window_layout.cfg + Path_Colors.cfg + Sketcher_Colors.cfg + Start_Colors.cfg + TechDraw_Colors.cfg + Window_Colors.cfg +) + +ADD_CUSTOM_TARGET(PreferencePackTemplates_data ALL + SOURCES ${PreferencePackTemplates_Files} +) + +fc_copy_sources(PreferencePackTemplates_data "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_DATADIR}/Gui/PreferencePackTemplates" + ${PreferencePackTemplates_Files}) + +INSTALL( + FILES + ${PreferencePackTemplates_Files} + DESTINATION + ${CMAKE_INSTALL_DATADIR}/Gui/PreferencePackTemplates +) diff --git a/src/Gui/PreferencePackTemplates/Appearance/Console_Colors.cfg b/src/Gui/PreferencePackTemplates/Console_Colors.cfg similarity index 100% rename from src/Gui/PreferencePackTemplates/Appearance/Console_Colors.cfg rename to src/Gui/PreferencePackTemplates/Console_Colors.cfg diff --git a/src/Gui/PreferencePackTemplates/Appearance/Draft_Colors.cfg b/src/Gui/PreferencePackTemplates/Draft_Colors.cfg similarity index 100% rename from src/Gui/PreferencePackTemplates/Appearance/Draft_Colors.cfg rename to src/Gui/PreferencePackTemplates/Draft_Colors.cfg diff --git a/src/Gui/PreferencePackTemplates/Appearance/Editor_Colors.cfg b/src/Gui/PreferencePackTemplates/Editor_Colors.cfg similarity index 100% rename from src/Gui/PreferencePackTemplates/Appearance/Editor_Colors.cfg rename to src/Gui/PreferencePackTemplates/Editor_Colors.cfg diff --git a/src/Gui/PreferencePackTemplates/Appearance/Editor_Font.cfg b/src/Gui/PreferencePackTemplates/Editor_Font.cfg similarity index 100% rename from src/Gui/PreferencePackTemplates/Appearance/Editor_Font.cfg rename to src/Gui/PreferencePackTemplates/Editor_Font.cfg diff --git a/src/Gui/PreferencePackTemplates/Behavior/Main_window_layout.cfg b/src/Gui/PreferencePackTemplates/Main_window_layout.cfg similarity index 100% rename from src/Gui/PreferencePackTemplates/Behavior/Main_window_layout.cfg rename to src/Gui/PreferencePackTemplates/Main_window_layout.cfg diff --git a/src/Gui/PreferencePackTemplates/Appearance/Path_Colors.cfg b/src/Gui/PreferencePackTemplates/Path_Colors.cfg similarity index 100% rename from src/Gui/PreferencePackTemplates/Appearance/Path_Colors.cfg rename to src/Gui/PreferencePackTemplates/Path_Colors.cfg diff --git a/src/Gui/PreferencePackTemplates/Appearance/Sketcher_Colors.cfg b/src/Gui/PreferencePackTemplates/Sketcher_Colors.cfg similarity index 100% rename from src/Gui/PreferencePackTemplates/Appearance/Sketcher_Colors.cfg rename to src/Gui/PreferencePackTemplates/Sketcher_Colors.cfg diff --git a/src/Gui/PreferencePackTemplates/Appearance/Start_Colors.cfg b/src/Gui/PreferencePackTemplates/Start_Colors.cfg similarity index 100% rename from src/Gui/PreferencePackTemplates/Appearance/Start_Colors.cfg rename to src/Gui/PreferencePackTemplates/Start_Colors.cfg diff --git a/src/Gui/PreferencePackTemplates/Appearance/TechDraw_Colors.cfg b/src/Gui/PreferencePackTemplates/TechDraw_Colors.cfg similarity index 100% rename from src/Gui/PreferencePackTemplates/Appearance/TechDraw_Colors.cfg rename to src/Gui/PreferencePackTemplates/TechDraw_Colors.cfg diff --git a/src/Gui/PreferencePackTemplates/Appearance/Window_Colors.cfg b/src/Gui/PreferencePackTemplates/Window_Colors.cfg similarity index 100% rename from src/Gui/PreferencePackTemplates/Appearance/Window_Colors.cfg rename to src/Gui/PreferencePackTemplates/Window_Colors.cfg diff --git a/src/Gui/PreferencePacks/package.xml b/src/Gui/PreferencePacks/package.xml index 4791afca43..42ae04e8ca 100644 --- a/src/Gui/PreferencePacks/package.xml +++ b/src/Gui/PreferencePacks/package.xml @@ -12,7 +12,8 @@ FreeCAD Classic Colors FreeCAD default colors for core app and included Mods. 1.0.0 - appearance + built-in + colors