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 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"); } diff --git a/src/Gui/Application.cpp b/src/Gui/Application.cpp index be017ae1cc..99500fa9c4 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 @@ -73,6 +74,7 @@ #include "WidgetFactory.h" #include "Command.h" #include "Macro.h" +#include "PreferencePackManager.h" #include "ProgressBar.h" #include "Workbench.h" #include "WorkbenchManager.h" @@ -160,6 +162,9 @@ struct ApplicationP macroMngr = new MacroManager(); else macroMngr = nullptr; + + // Create the Theme Manager + prefPackManager = new PreferencePackManager(); } ~ApplicationP() @@ -173,6 +178,7 @@ struct ApplicationP Gui::Document* activeDocument; Gui::Document* editDocument; MacroManager* macroMngr; + PreferencePackManager* prefPackManager; /// List of all registered views std::list passive; bool isClosing; @@ -1661,6 +1667,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 6c436f97d9..426afe1526 100644 --- a/src/Gui/Application.h +++ b/src/Gui/Application.h @@ -44,6 +44,7 @@ class MacroManager; class MDIView; class MainWindow; class MenuItem; +class PreferencePackManager; class ViewProvider; class ViewProviderDocumentObject; @@ -219,6 +220,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 5cbdfe8b00..3c6a607e9c 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 @@ -1143,6 +1149,7 @@ SET(FreeCADGui_CPP_SRCS resource.cpp Control.cpp SpaceballEvent.cpp + PreferencePackManager.cpp Thumbnail.cpp Utilities.cpp WaitCursor.cpp @@ -1175,6 +1182,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..98f4589f0e --- /dev/null +++ b/src/Gui/DlgCreateNewPreferencePack.ui @@ -0,0 +1,105 @@ + + + Gui::Dialog::DlgCreateNewPreferencePack + + + Qt::ApplicationModal + + + + 0 + 0 + 580 + 520 + + + + Create New Preference Pack + + + true + + + + + + + + Name + + + + + + + + + + + + 1 + + + 50 + + + 250 + + + true + + + + Property group templates + + + + + + + + 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..47c6bda8f0 --- /dev/null +++ b/src/Gui/DlgCreateNewPreferencePackImp.cpp @@ -0,0 +1,168 @@ +/*************************************************************************** + * 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 +#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)); + 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); + } +} + +void Gui::Dialog::DlgCreateNewPreferencePackImp::setPreferencePackNames(const std::vector& usedNames) +{ + _existingPackNames = usedNames; +} + +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()); +} + +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 new file mode 100644 index 0000000000..b6d0caf705 --- /dev/null +++ b/src/Gui/DlgCreateNewPreferencePackImp.h @@ -0,0 +1,82 @@ +/*************************************************************************** + * 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); + void setPreferencePackNames(const std::vector& usedNames); + + std::vector selectedTemplates() const; + std::string preferencePackName() const; + +protected Q_SLOTS: + + void onItemChanged(QTreeWidgetItem* item, int column); + + 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 +} // namespace Gui + +#endif // GUI_DIALOG_DLGCREATENEWTHEMEIMP_H diff --git a/src/Gui/DlgGeneral.ui b/src/Gui/DlgGeneral.ui index 0b83dac463..6e0f6c1fda 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,144 @@ + + + + Preference Packs + + + + + + + + Qt::ScrollBarAlwaysOff + + + QAbstractScrollArea::AdjustToContents + + + QAbstractItemView::NoEditTriggers + + + false + + + false + + + QAbstractItemView::NoSelection + + + QAbstractItemView::SelectRows + + + false + + + true + + + false + + + false + + + true + + + true + + + 30 + + + 100 + + + true + + + false + + + 16 + + + 24 + + + + Name + + + + 75 + true + + + + + + Type + + + + 75 + true + + + + + + Load + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Save new... + + + + + + + + 0 + 0 + + + + Manage... + + + + + + + + + + @@ -74,7 +239,16 @@ 6 - + + 11 + + + 11 + + + 11 + + 11 @@ -82,7 +256,16 @@ 6 - + + 0 + + + 0 + + + 0 + + 0 @@ -131,7 +314,16 @@ 6 - + + 0 + + + 0 + + + 0 + + 0 @@ -152,7 +344,16 @@ See the FreeCAD Wiki for details about the image. 6 - + + 0 + + + 0 + + + 0 + + 0 @@ -176,7 +377,16 @@ See the FreeCAD Wiki for details about the image. 6 - + + 0 + + + 0 + + + 0 + + 0 @@ -201,7 +411,16 @@ this according to your screen size or personal taste 6 - + + 0 + + + 0 + + + 0 + + 0 @@ -214,7 +433,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 +452,16 @@ this according to your screen size or personal taste Start up - + + 11 + + + 11 + + + 11 + + 11 @@ -262,7 +490,16 @@ display the splash screen - + + 0 + + + 0 + + + 0 + + 0 @@ -294,7 +531,16 @@ after FreeCAD launches Python console - + + 11 + + + 11 + + + 11 + + 11 @@ -323,19 +569,6 @@ horizontal space in Python console - - - - Qt::Vertical - - - - 352 - 221 - - - - @@ -353,11 +586,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..a1053da4dd 100644 --- a/src/Gui/DlgGeneralImp.cpp +++ b/src/Gui/DlgGeneralImp.cpp @@ -27,17 +27,28 @@ # include # include # include +# include #endif #include "DlgGeneralImp.h" #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" + +// Only needed until PreferencePacks can be managed from the AddonManager: +#include +namespace fs = boost::filesystem; + using namespace Gui::Dialog; @@ -82,6 +93,23 @@ DlgGeneralImp::DlgGeneralImp( QWidget* parent ) else ui->AutoloadModuleCombo->addItem(px, it.key(), QVariant(it.value())); } + + recreatePreferencePackMenu(); + 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 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()); + 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)); }); } /** @@ -299,4 +327,87 @@ void DlgGeneralImp::changeEvent(QEvent *e) } } +void DlgGeneralImp::recreatePreferencePackMenu() +{ + 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()->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); + QStringList columnHeaders; + columnHeaders << tr("Preference Pack Name") + << tr("Tags") + << QString(); // for the "Load" buttons + ui->PreferencePacks->setHorizontalHeaderLabels(columnHeaders); + + // Populate the Preference Packs list + auto packs = Application::Instance->prefPackManager()->preferencePacks(); + + ui->PreferencePacks->setRowCount(packs.size()); + + int row = 0; + QIcon icon = style()->standardIcon(QStyle::SP_DialogApplyButton); + 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 packs = Application::Instance->prefPackManager()->preferencePackNames(); + newPreferencePackDialog = std::make_unique(this); + newPreferencePackDialog->setPreferencePackTemplates(Application::Instance->prefPackManager()->templateFiles()); + newPreferencePackDialog->setPreferencePackNames(packs); + 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::onLoadPreferencePackClicked(const std::string& packName) +{ + 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..1e08282669 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,18 @@ public: protected: void changeEvent(QEvent *e); +protected Q_SLOTS: + void onLoadPreferencePackClicked(const std::string &packName); + 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 a785e178db..d0ec3e3cbe 100644 --- a/src/Gui/DlgPreferencesImp.h +++ b/src/Gui/DlgPreferencesImp.h @@ -114,11 +114,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: @@ -136,11 +138,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/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 new file mode 100644 index 0000000000..3bb79c2c5e --- /dev/null +++ b/src/Gui/PreferencePackManager.cpp @@ -0,0 +1,447 @@ +/*************************************************************************** + * 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 "DockWindowManager.h" +#include "ToolBarManager.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; +} + + +App::Metadata Gui::PreferencePack::metadata() const +{ + return _metadata; +} + +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")->insertTo(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() const +{ + std::lock_guard lock(_mutex); + std::vector names; + for (const auto& preferencePack : _preferencePacks) + 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); + if (auto preferencePack = _preferencePacks.find(preferencePackName); preferencePack != _preferencePacks.end()) { + BackupCurrentConfig(); + 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(); + + // 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; + } + 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()); + + // Ensure that the DockWindowManager has saved its current state: + 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); + 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); + + 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& 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 }); + } + } + } + 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..78fd80c236 --- /dev/null +++ b/src/Gui/PreferencePackManager.h @@ -0,0 +1,188 @@ +/*************************************************************************** + * 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_PREFERENCEPACKMANAGER_H +#define BASE_PREFERENCEPACKMANAGER_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; + + /** + * Get the complete metadata object for this preference pack + */ + App::Metadata metadata() 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 + */ + std::vector preferencePackNames() const; + + /** + * Get a map of all installed PreferencePack names and their associated packs + */ + std::map preferencePacks() 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/ + * + * 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; + }; + + /** + * 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/Arch_Colors.cfg b/src/Gui/PreferencePackTemplates/Arch_Colors.cfg new file mode 100644 index 0000000000..9b7d90987c --- /dev/null +++ b/src/Gui/PreferencePackTemplates/Arch_Colors.cfg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Gui/PreferencePackTemplates/CMakeLists.txt b/src/Gui/PreferencePackTemplates/CMakeLists.txt new file mode 100644 index 0000000000..e9023ab237 --- /dev/null +++ b/src/Gui/PreferencePackTemplates/CMakeLists.txt @@ -0,0 +1,28 @@ + +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/Console_Colors.cfg b/src/Gui/PreferencePackTemplates/Console_Colors.cfg new file mode 100644 index 0000000000..7e4f4c2d20 --- /dev/null +++ b/src/Gui/PreferencePackTemplates/Console_Colors.cfg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/Gui/PreferencePackTemplates/Draft_Colors.cfg b/src/Gui/PreferencePackTemplates/Draft_Colors.cfg new file mode 100644 index 0000000000..7f68a8e53c --- /dev/null +++ b/src/Gui/PreferencePackTemplates/Draft_Colors.cfg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/Gui/PreferencePackTemplates/Editor_Colors.cfg b/src/Gui/PreferencePackTemplates/Editor_Colors.cfg new file mode 100644 index 0000000000..93ecd1cff1 --- /dev/null +++ b/src/Gui/PreferencePackTemplates/Editor_Colors.cfg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Gui/PreferencePackTemplates/Editor_Font.cfg b/src/Gui/PreferencePackTemplates/Editor_Font.cfg new file mode 100644 index 0000000000..445b017576 --- /dev/null +++ b/src/Gui/PreferencePackTemplates/Editor_Font.cfg @@ -0,0 +1,13 @@ + + + + + + + + MS Shell Dlg 2 + + + + + diff --git a/src/Gui/PreferencePackTemplates/Main_window_layout.cfg b/src/Gui/PreferencePackTemplates/Main_window_layout.cfg new file mode 100644 index 0000000000..1e9c8d023c --- /dev/null +++ b/src/Gui/PreferencePackTemplates/Main_window_layout.cfg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Gui/PreferencePackTemplates/Path_Colors.cfg b/src/Gui/PreferencePackTemplates/Path_Colors.cfg new file mode 100644 index 0000000000..0780cf0255 --- /dev/null +++ b/src/Gui/PreferencePackTemplates/Path_Colors.cfg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Gui/PreferencePackTemplates/Sketcher_Colors.cfg b/src/Gui/PreferencePackTemplates/Sketcher_Colors.cfg new file mode 100644 index 0000000000..44b4d9f535 --- /dev/null +++ b/src/Gui/PreferencePackTemplates/Sketcher_Colors.cfg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Gui/PreferencePackTemplates/Start_Colors.cfg b/src/Gui/PreferencePackTemplates/Start_Colors.cfg new file mode 100644 index 0000000000..2465eae672 --- /dev/null +++ b/src/Gui/PreferencePackTemplates/Start_Colors.cfg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/Gui/PreferencePackTemplates/TechDraw_Colors.cfg b/src/Gui/PreferencePackTemplates/TechDraw_Colors.cfg new file mode 100644 index 0000000000..7a1525ad91 --- /dev/null +++ b/src/Gui/PreferencePackTemplates/TechDraw_Colors.cfg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Gui/PreferencePackTemplates/Window_Colors.cfg b/src/Gui/PreferencePackTemplates/Window_Colors.cfg new file mode 100644 index 0000000000..3821bc48a2 --- /dev/null +++ b/src/Gui/PreferencePackTemplates/Window_Colors.cfg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..42ae04e8ca --- /dev/null +++ b/src/Gui/PreferencePacks/package.xml @@ -0,0 +1,20 @@ + + + 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 + built-in + colors + + + +