From 224ffe37e254259678461ac34919d99b6bc294a2 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Tue, 5 Oct 2021 23:20:29 -0500 Subject: [PATCH] [Gui] New widget supporting per-state styles The new StatefulLabel widget is designed to be customizable by optional preferences entries, Qt stylesheets, and default styles, set on a per-state basis, where "state" is a Qt property that can be changed dynamically at runtime. --- src/Gui/Widgets.cpp | 126 ++++++++++++++++++++++++++++++++------------ src/Gui/Widgets.h | 36 +++++++++---- 2 files changed, 118 insertions(+), 44 deletions(-) diff --git a/src/Gui/Widgets.cpp b/src/Gui/Widgets.cpp index fcf1a6379021..632feb4866eb 100644 --- a/src/Gui/Widgets.cpp +++ b/src/Gui/Widgets.cpp @@ -814,6 +814,7 @@ void ColorButton::onRejected() UrlLabel::UrlLabel(QWidget * parent, Qt::WindowFlags f) : QLabel(parent, f) + , _launchExternal(true) { _url = QString::fromLatin1("http://localhost"); setToolTip(this->_url); @@ -827,6 +828,11 @@ UrlLabel::~UrlLabel() { } +void Gui::UrlLabel::setLaunchExternal(bool l) +{ + _launchExternal = l; +} + void UrlLabel::enterEvent ( QEvent * ) { setCursor(Qt::PointingHandCursor); @@ -839,26 +845,32 @@ void UrlLabel::leaveEvent ( QEvent * ) void UrlLabel::mouseReleaseEvent (QMouseEvent *) { - // The webbrowser Python module allows to start the system browser in an OS-independent way - Base::PyGILStateLocker lock; - PyObject* module = PyImport_ImportModule("webbrowser"); - if (module) { - // get the methods dictionary and search for the 'open' method - PyObject* dict = PyModule_GetDict(module); - PyObject* func = PyDict_GetItemString(dict, "open"); - if (func) { - PyObject* args = Py_BuildValue("(s)", (const char*)this->_url.toLatin1()); + if (_launchExternal) { + // The webbrowser Python module allows to start the system browser in an OS-independent way + Base::PyGILStateLocker lock; + PyObject* module = PyImport_ImportModule("webbrowser"); + if (module) { + // get the methods dictionary and search for the 'open' method + PyObject* dict = PyModule_GetDict(module); + PyObject* func = PyDict_GetItemString(dict, "open"); + if (func) { + PyObject* args = Py_BuildValue("(s)", (const char*)this->_url.toLatin1()); #if PY_VERSION_HEX < 0x03090000 - PyObject* result = PyEval_CallObject(func,args); + PyObject* result = PyEval_CallObject(func, args); #else - PyObject* result = PyObject_CallObject(func,args); + PyObject* result = PyObject_CallObject(func, args); #endif - // decrement the args and module reference - Py_XDECREF(result); - Py_DECREF(args); - Py_DECREF(module); + // decrement the args and module reference + Py_XDECREF(result); + Py_DECREF(args); + Py_DECREF(module); + } } } + else { + // Someone else will deal with it... + Q_EMIT linkClicked(_url); + } } QString UrlLabel::url() const @@ -866,6 +878,11 @@ QString UrlLabel::url() const return this->_url; } +bool Gui::UrlLabel::launchExternal() const +{ + return _launchExternal; +} + void UrlLabel::setUrl(const QString& u) { this->_url = u; @@ -877,68 +894,105 @@ void UrlLabel::setUrl(const QString& u) StatefulLabel::StatefulLabel(QWidget* parent) : QLabel(parent) { + // Always attach to the parameter group that stores the main FreeCAD stylesheet + _stylesheetGroup = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/General"); + _stylesheetGroup->Attach(this); } StatefulLabel::~StatefulLabel() { + if (_parameterGroup.isValid()) + _parameterGroup->Detach(this); + _stylesheetGroup->Detach(this); } -QString StatefulLabel::state() const +void StatefulLabel::setDefaultStyle(const QString& defaultStyle) { - return _state; + _defaultStyle = defaultStyle; } -void StatefulLabel::setDefaultStyle(const QString& defaultStyle) +void StatefulLabel::setParameterGroup(const std::string& groupName) { - _defaultStyle = defaultStyle; + if (_parameterGroup.isValid()) + _parameterGroup->Detach(this); + + // Attach to the Parametergroup so we know when it changes + _parameterGroup = App::GetApplication().GetParameterGroupByPath(groupName.c_str()); + if (_parameterGroup.isValid()) + _parameterGroup->Attach(this); } void StatefulLabel::registerState(const QString& state, const QString& styleCSS, - const std::string& preferenceLocation, const std::string& preferenceName) { - _availableStates[state] = { QColor(), QColor(), styleCSS, preferenceLocation, preferenceName }; + _availableStates[state] = { QColor(), QColor(), styleCSS, preferenceName }; } void StatefulLabel::registerState(const QString& state, const QColor& color, - const std::string& preferenceLocation, const std::string& preferenceName) { - _availableStates[state] = {color, QColor(), QString(), preferenceLocation, preferenceName}; + _availableStates[state] = {color, QColor(), QString(), preferenceName}; } void StatefulLabel::registerState(const QString& state, const QColor& foreground, const QColor& background, const std::string& preferenceLocation, const std::string& preferenceName) { - _availableStates[state] = { foreground, background, QString(), preferenceLocation, preferenceName }; + _availableStates[state] = { foreground, background, QString(), preferenceName }; +} + +/** Observes the parameter group and clears the cache if it changes */ +void StatefulLabel::OnChange(Base::Subject& rCaller, const char* rcReason) +{ + auto changedItem = std::string(rcReason); + if (changedItem == "StyleSheet") { + _styleCache.clear(); + } + else { + for (const auto& state : _availableStates) { + if (state.second.preferenceString == changedItem) { + _styleCache.erase(_styleCache.find(state.first)); + } + } + } } -void StatefulLabel::setState(const QString& state) +void StatefulLabel::setState(QString state) { _state = state; + std::string stateIn = state.toStdString(); + + // Check the cache first: + if (auto style = _styleCache.find(_state); style != _styleCache.end()) { + auto test = style->second.toStdString(); + this->setStyleSheet(style->second); + return; + } + if (auto entry = _availableStates.find(state); entry != _availableStates.end()) { // Order of precedence: first, check if the user has set this in their preferences: - if (!entry->second.preferenceLocation.empty() && !entry->second.preferenceString.empty()) { - ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath(entry->second.preferenceLocation.c_str()); - + if (!entry->second.preferenceString.empty()) { // First, try to see if it's just stored a color (as an unsigned int): - auto availableColorPrefs = hGrp->GetUnsignedMap(); + auto availableColorPrefs = _parameterGroup->GetUnsignedMap(); + std::string lookingForGroup = entry->second.preferenceString; for (const auto &unsignedEntry : availableColorPrefs) { + std::string foundGroup = unsignedEntry.first; if (unsignedEntry.first == entry->second.preferenceString) { // Convert the stored Uint into usable color data: unsigned int col = unsignedEntry.second; QColor qcolor((col >> 24) & 0xff, (col >> 16) & 0xff, (col >> 8) & 0xff); this->setStyleSheet(QString::fromUtf8("Gui--StatefulLabel{ color : rgba(%1,%2,%3,%4) ;}").arg(qcolor.red()).arg(qcolor.green()).arg(qcolor.blue()).arg(qcolor.alpha())); + _styleCache[state] = this->styleSheet(); return; } } // If not, try to see if there's an entire style string set as ASCII: - auto availableStringPrefs = hGrp->GetASCIIMap(); + auto availableStringPrefs = _parameterGroup->GetASCIIMap(); for (const auto& stringEntry : availableStringPrefs) { if (stringEntry.first == entry->second.preferenceString) { this->setStyleSheet(QString::fromStdString(stringEntry.second)); + _styleCache[state] = this->styleSheet(); return; } } @@ -946,9 +1000,10 @@ void StatefulLabel::setState(const QString& state) // If there is no preferences entry for this label, allow the stylesheet to set it, and only set to the default // formatting if there is no stylesheet entry - if (styleSheet().isEmpty()) { + if (qApp->styleSheet().isEmpty()) { if (!entry->second.defaultCSS.isEmpty()) { this->setStyleSheet(entry->second.defaultCSS); + _styleCache[state] = this->styleSheet(); return; } else { @@ -959,16 +1014,21 @@ void StatefulLabel::setState(const QString& state) colorEntries.append(QString::fromUtf8("color : rgba(%1,%2,%3,%4);").arg(fg.red()).arg(fg.green()).arg(fg.blue()).arg(fg.alpha())); if (bg.isValid()) colorEntries.append(QString::fromUtf8("background-color : rgba(%1,%2,%3,%4);").arg(bg.red()).arg(bg.green()).arg(bg.blue()).arg(bg.alpha())); - std::string tempForTesting = QString::fromUtf8("Gui--StatefulLabel{ %1 }").arg(colorEntries).toStdString(); this->setStyleSheet(QString::fromUtf8("Gui--StatefulLabel{ %1 }").arg(colorEntries)); + _styleCache[state] = this->styleSheet(); return; } } - // else the stylesheet has already set our appearance, no need to do anything + // else the stylesheet sets our appearance: make sure it recalculates the appearance: + this->setStyleSheet(QString()); + this->setStyle(qApp->style()); + this->style()->unpolish(this); + this->style()->polish(this); } else { if (styleSheet().isEmpty()) { this->setStyleSheet(_defaultStyle); + _styleCache[state] = this->styleSheet(); } } } diff --git a/src/Gui/Widgets.h b/src/Gui/Widgets.h index 2b7384a7e6f2..a59ac1896c75 100644 --- a/src/Gui/Widgets.h +++ b/src/Gui/Widgets.h @@ -36,6 +36,7 @@ #include #include #include "ExpressionBinding.h" +#include "Base/Parameter.h" namespace Gui { class PrefCheckBox; @@ -254,15 +255,21 @@ class GuiExport UrlLabel : public QLabel { Q_OBJECT Q_PROPERTY( QString url READ url WRITE setUrl) + Q_PROPERTY( bool launchExternal READ launchExternal WRITE setLaunchExternal) public: UrlLabel ( QWidget * parent = 0, Qt::WindowFlags f = Qt::WindowFlags() ); virtual ~UrlLabel(); QString url() const; + bool launchExternal() const; + +Q_SIGNALS: + void linkClicked(QString url); public Q_SLOTS: void setUrl( const QString &u ); + void setLaunchExternal(bool l); protected: void enterEvent ( QEvent * ); @@ -271,6 +278,7 @@ public Q_SLOTS: private: QString _url; + bool _launchExternal; }; @@ -285,50 +293,56 @@ public Q_SLOTS: * * @author Chris Hennes */ -class GuiExport StatefulLabel : public QLabel +class GuiExport StatefulLabel : public QLabel, public Base::Observer { Q_OBJECT - Q_PROPERTY( QString state READ state WRITE setState) + Q_PROPERTY( QString state MEMBER _state WRITE setState ) public: StatefulLabel(QWidget* parent = nullptr); virtual ~StatefulLabel(); - QString state() const; - /** If an unrecognized state is set, use this style */ void setDefaultStyle(const QString &defaultStyle); + /** If any of the states have user preferences associated with them, this sets the parameter + group that stores those preferences. All states must be in the same parameter group, but + the group does not have to have entries for all of them. */ + void setParameterGroup(const std::string& groupName); + /** Register a state and its corresponding style (optionally attached to a user preference) */ void registerState(const QString &state, const QString &styleCSS, - const std::string& preferenceLocation = std::string(), const std::string& preferenceName = std::string()); /** For convenience, allow simple color-only states via QColor (optionally attached to a user preference) */ void registerState(const QString& state, const QColor& color, - const std::string& preferenceLocation = std::string(), const std::string& preferenceName = std::string()); /** For convenience, allow simple color-only states via QColor (optionally attached to a user preference) */ void registerState(const QString& state, const QColor& foreground, const QColor &background, - const std::string& preferenceLocation = std::string(), - const std::string& preferenceName = std::string()); + const std::string& foregroundPreference = std::string(), + const std::string& backgroundPreference = std::string()); + + /** Observes the parameter group and clears the cache if it changes */ + void OnChange(Base::Subject& rCaller, const char* rcReason); public Q_SLOTS: - void setState(const QString &state); + void setState(QString state); private: QString _state; + ParameterGrp::handle _parameterGroup; + ParameterGrp::handle _stylesheetGroup; struct StateData { QColor foregroundColor; QColor backgroundColor; QString defaultCSS; - std::string preferenceLocation; std::string preferenceString; }; - + std::map _availableStates; + std::map _styleCache; QString _defaultStyle; };