Skip to content

Commit

Permalink
[Gui] New widget supporting per-state styles
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
chennes committed Oct 6, 2021
1 parent ec249a8 commit 224ffe3
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 44 deletions.
126 changes: 93 additions & 33 deletions src/Gui/Widgets.cpp
Expand Up @@ -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);
Expand All @@ -827,6 +828,11 @@ UrlLabel::~UrlLabel()
{
}

void Gui::UrlLabel::setLaunchExternal(bool l)
{
_launchExternal = l;
}

void UrlLabel::enterEvent ( QEvent * )
{
setCursor(Qt::PointingHandCursor);
Expand All @@ -839,33 +845,44 @@ 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
{
return this->_url;
}

bool Gui::UrlLabel::launchExternal() const
{
return _launchExternal;
}

void UrlLabel::setUrl(const QString& u)
{
this->_url = u;
Expand All @@ -877,78 +894,116 @@ 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<const char*>& 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;
}
}
}

// 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 {
Expand All @@ -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();
}
}
}
Expand Down
36 changes: 25 additions & 11 deletions src/Gui/Widgets.h
Expand Up @@ -36,6 +36,7 @@
#include <QToolButton>
#include <QModelIndex>
#include "ExpressionBinding.h"
#include "Base/Parameter.h"

namespace Gui {
class PrefCheckBox;
Expand Down Expand Up @@ -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 * );
Expand All @@ -271,6 +278,7 @@ public Q_SLOTS:

private:
QString _url;
bool _launchExternal;
};


Expand All @@ -285,50 +293,56 @@ public Q_SLOTS:
*
* @author Chris Hennes
*/
class GuiExport StatefulLabel : public QLabel
class GuiExport StatefulLabel : public QLabel, public Base::Observer<const char*>
{
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<const char *>& 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<QString, StateData> _availableStates;
std::map<QString, QString> _styleCache;
QString _defaultStyle;
};

Expand Down

0 comments on commit 224ffe3

Please sign in to comment.