#include "FractoriumPch.h" #include "QssDialog.h" #include "ui_QssDialog.h" #include "qcssscanner.h" /// <summary> /// The code in this file did not originate in Fractorium. /// It was taken either in whole or in part from the source code /// of Qt Creator. Their license applies. /// </summary> /// <summary> /// Comparison for sorting object names. /// Strings not starting with the letter 'Q' take precedence. /// This has the effect of putting custom derived classes first before /// all Q* classes. /// </summary> /// <param name="s1">The first string to compare</param> /// <param name="s2">The second string to compare</param> /// <returns>True if s1 < s2 with special rules for 'Q' taken into account.</returns> bool CaseInsensitiveLessThanQ(const QString& s1, const QString& s2) { if (s1.length() && s2.length()) { if (s1[0] == 'Q' && s2[0] == 'Q') return s1.toLower() < s2.toLower(); else if (s1[0] == 'Q') return false; else if (s2[0] == 'Q') return true; else return s1.toLower() < s2.toLower(); } return false; } /// <summary> /// Construct a QssDialog. /// This manually constructs much of the menu GUI via code rather /// than in the designer. /// </summary> /// <param name="parent">The main Fractorium window.</param> QssDialog::QssDialog(Fractorium* parent) : QDialog(parent), ui(new Ui::QssDialog), m_Parent(parent), m_AddColorAction(new QAction(tr("Add Color"), this)), m_AddGeomAction(new QAction(tr("Add Geometry"), this)), m_AddBorderAction(new QAction(tr("Add Border"), this)), m_AddFontAction(new QAction(tr("Add Font..."), this)), m_AddStyleAction(new QAction(tr("Set Theme"), this)) { ui->setupUi(this); m_LastStyle = m_Parent->styleSheet(); setWindowTitle("QSS Editor - default.qss"); connect(ui->QssEdit, SIGNAL(textChanged()), this, SLOT(SlotTextChanged())); QToolBar* toolBar = new QToolBar(this); QMenu* colorActionMenu = new QMenu(this); QMenu* geomActionMenu = new QMenu(this); QMenu* borderActionMenu = new QMenu(this); QMenu* styleActionMenu = new QMenu(this); (m_ColorActionMapper = new QSignalMapper(this))->setMapping(m_AddColorAction, QString()); (m_GeomActionMapper = new QSignalMapper(this))->setMapping(m_AddGeomAction, QString()); (m_BorderActionMapper = new QSignalMapper(this))->setMapping(m_AddBorderAction, QString()); (m_StyleActionMapper = new QSignalMapper(this))->setMapping(m_AddStyleAction, QString()); connect(ui->QssLoadButton, SIGNAL(clicked()), this, SLOT(LoadButton_clicked()), Qt::QueuedConnection); connect(ui->QssSaveButton, SIGNAL(clicked()), this, SLOT(SaveButton_clicked()), Qt::QueuedConnection); connect(ui->QssBasicButton, SIGNAL(clicked()), this, SLOT(BasicButton_clicked()), Qt::QueuedConnection); connect(ui->QssMediumButton, SIGNAL(clicked()), this, SLOT(MediumButton_clicked()), Qt::QueuedConnection); connect(ui->QssAdvancedButton, SIGNAL(clicked()), this, SLOT(AdvancedButton_clicked()), Qt::QueuedConnection); connect(m_AddFontAction, SIGNAL(triggered()), this, SLOT(SlotAddFont())); QVector<QPair<QString, QString>> colorVec; colorVec.reserve(12); colorVec.push_back(QPair<QString, QString>("color", "")); colorVec.push_back(QPair<QString, QString>("background-color", "")); colorVec.push_back(QPair<QString, QString>("alternate-background-color", "")); colorVec.push_back(QPair<QString, QString>("border-color", "")); colorVec.push_back(QPair<QString, QString>("border-top-color", "")); colorVec.push_back(QPair<QString, QString>("border-right-color", "")); colorVec.push_back(QPair<QString, QString>("border-bottom-color", "")); colorVec.push_back(QPair<QString, QString>("border-left-color", "")); colorVec.push_back(QPair<QString, QString>("gridline-color", "")); colorVec.push_back(QPair<QString, QString>("selection-color", "")); colorVec.push_back(QPair<QString, QString>("selection-background-color", "")); for (auto& c : colorVec) { auto colorAction = colorActionMenu->addAction(c.first); m_ColorMap[c.first] = c.second; connect(colorAction, SIGNAL(triggered()), m_ColorActionMapper, SLOT(map())); m_ColorActionMapper->setMapping(colorAction, c.first); } QVector<QPair<QString, QString>> geomVec; geomVec.reserve(12); geomVec.push_back(QPair<QString, QString>("width", "100px")); geomVec.push_back(QPair<QString, QString>("height", "50px")); geomVec.push_back(QPair<QString, QString>("spacing", "10")); geomVec.push_back(QPair<QString, QString>("padding", "3px")); geomVec.push_back(QPair<QString, QString>("padding-top", "3px")); geomVec.push_back(QPair<QString, QString>("padding-right", "3px")); geomVec.push_back(QPair<QString, QString>("padding-bottom", "3px")); geomVec.push_back(QPair<QString, QString>("padding-left", "3px")); geomVec.push_back(QPair<QString, QString>("margin", "3px")); geomVec.push_back(QPair<QString, QString>("margin-top", "3px")); geomVec.push_back(QPair<QString, QString>("margin-right", "3px")); geomVec.push_back(QPair<QString, QString>("margin-bottom", "3px")); geomVec.push_back(QPair<QString, QString>("margin-left", "3px")); for (auto& g : geomVec) { auto geomAction = geomActionMenu->addAction(g.first); m_GeomMap[g.first] = g.second; connect(geomAction, SIGNAL(triggered()), m_GeomActionMapper, SLOT(map())); m_GeomActionMapper->setMapping(geomAction, g.first); } QVector<QPair<QString, QString>> borderVec; borderVec.reserve(8); borderVec.push_back(QPair<QString, QString>("border", "1px solid black")); borderVec.push_back(QPair<QString, QString>("border-top", "1px inset black")); borderVec.push_back(QPair<QString, QString>("border-right", "1px outset black")); borderVec.push_back(QPair<QString, QString>("border-bottom", "1px ridge black")); borderVec.push_back(QPair<QString, QString>("border-left", "1px groove black")); borderVec.push_back(QPair<QString, QString>("border-style", "double")); borderVec.push_back(QPair<QString, QString>("border-width", "1px")); borderVec.push_back(QPair<QString, QString>("border-radius", "10px")); for (auto& b : borderVec) { auto borderAction = borderActionMenu->addAction(b.first); m_BorderMap[b.first] = b.second; connect(borderAction, SIGNAL(triggered()), m_BorderActionMapper, SLOT(map())); m_BorderActionMapper->setMapping(borderAction, b.first); } auto styles = QStyleFactory::keys(); for (auto& s : styles) { auto styleAction = styleActionMenu->addAction(s); m_StyleMap[s] = s; connect(styleAction, SIGNAL(triggered()), m_StyleActionMapper, SLOT(map())); m_StyleActionMapper->setMapping(styleAction, s); } connect(m_ColorActionMapper, SIGNAL(mapped(QString)), this, SLOT(SlotAddColor(QString))); connect(m_GeomActionMapper, SIGNAL(mapped(QString)), this, SLOT(SlotAddGeom(QString))); connect(m_BorderActionMapper, SIGNAL(mapped(QString)), this, SLOT(SlotAddBorder(QString))); connect(m_StyleActionMapper, SIGNAL(mapped(QString)), this, SLOT(SlotSetTheme(QString))); m_AddColorAction->setMenu(colorActionMenu); m_AddGeomAction->setMenu(geomActionMenu); m_AddBorderAction->setMenu(borderActionMenu); m_AddStyleAction->setMenu(styleActionMenu); toolBar->addAction(m_AddColorAction); toolBar->addAction(m_AddGeomAction); toolBar->addAction(m_AddBorderAction); toolBar->addAction(m_AddFontAction); toolBar->addAction(m_AddStyleAction); ui->verticalLayout->insertWidget(0, toolBar); ui->QssEdit->setFocus(); m_ApplyTimer = new QTimer(this); m_ApplyTimer->setSingleShot(true); m_ApplyTimer->setInterval(1000); connect(m_ApplyTimer, SIGNAL(timeout()), this, SLOT(SlotApplyCss())); } /// <summary> /// Destructor that stops the apply timer and deletes the ui. /// </summary> QssDialog::~QssDialog() { m_ApplyTimer->stop(); delete ui; } /// <summary> /// Thin wrapper around getting the text from the main text box as plain text. /// </summary> /// <returns>The plain text of the main text box</returns> QString QssDialog::Text() const { return ui->QssEdit->toPlainText(); } /// <summary> /// Thin wrapper around setting the text of the main text box. /// </summary> /// <param name="t">The text to set</param> void QssDialog::SetText(const QString& t) { ui->QssEdit->setText(t); } /// <summary> /// Get the class names of all objects in the application. /// This only makes one entry for each class type. /// It will also optionally return the object names as well for advanced QSS editing. /// </summary> /// <param name="includeObjectNames">Whether to get the individual object names as well</param> /// <returns>A list of all class names with optional entries for each individual object</returns> QList<QString> QssDialog::GetClassNames(bool includeObjectNames) { QSet<QString> classNames; QList<QList<QString>> dialogClassNames; auto widgetList = m_Parent->findChildren<QWidget*>(); for (int i = 0; i < widgetList.size(); i++) { auto classAndName = QString(widgetList[i]->metaObject()->className()); if (!includeObjectNames) { classNames.insert(classAndName); } else { auto dlg = qobject_cast<QDialog*>(widgetList[i]); if (dlg)//Dialogs only nest one level deep, so no need for generalized recursion. { QSet<QString> dlgSet; auto dlgWidgetList = dlg->findChildren<QWidget*>();//Find all children of the dialog. dlgSet.insert(classAndName);//Add the basic dialog class name, opening curly brace will be added later. classAndName += " "; for (int i = 0; i < dlgWidgetList.size(); i++) { auto dlgClassAndName = classAndName + QString(dlgWidgetList[i]->metaObject()->className()); dlgSet.insert(dlgClassAndName); if (!dlgWidgetList[i]->objectName().isEmpty())//Add the class with object name for individual control customization. { dlgClassAndName += "#" + dlgWidgetList[i]->objectName(); dlgSet.insert(dlgClassAndName); } } auto dlgList = dlgSet.toList();//Convert set to list and sort. qSort(dlgList.begin(), dlgList.end(), CaseInsensitiveLessThanQ); dialogClassNames.push_back(dlgList);//Add this to the full list after sorting at the end. } else if (GetAllParents<QDialog*>(widgetList[i]).empty())//Skip widgets on dialogs, they are added above. { classNames.insert(classAndName);//Add the basic class name. if (!widgetList[i]->objectName().isEmpty())//Add the class with object name for individual control customization. { classAndName += "#" + widgetList[i]->objectName(); classNames.insert(classAndName); } } } } auto l = classNames.toList(); qSort(l.begin(), l.end(), CaseInsensitiveLessThanQ); for (auto& d : dialogClassNames) l.append(d); return l; } /// <summary> /// Determines whether the passed in stylesheet text is valid. /// If the initial parse fails, a second attempt is made by wrapping the entire /// text in curly braces. /// </summary> /// <param name="styleSheet">The stylesheet text to analyze.</param> /// <returns>True if valid, else false.</returns> bool QssDialog::IsStyleSheetValid(const QString& styleSheet) { QCss::Parser parser(styleSheet); QCss::StyleSheet sheet; if (parser.parse(&sheet)) return true; QString fullSheet = QStringLiteral("* { "); fullSheet += styleSheet; fullSheet += QLatin1Char('}'); QCss::Parser parser2(fullSheet); return parser2.parse(&sheet); } /// <summary> /// Save the current stylesheet text to default.qss. /// Also save the selected theme to the settings. /// Called when the user clicks ok. /// Not called if cancelled or closed with the X. /// </summary> void QssDialog::accept() { if (m_Theme) m_Parent->m_Settings->Theme(m_Theme->objectName()); SaveAsDefault(); QDialog::accept(); } /// <summary> /// Restore the stylesheet and theme to what it was when the dialog was opened. /// Called when the user clicks cancel or closes with the X. /// </summary> void QssDialog::reject() { if (!m_LastStyle.isEmpty()) m_Parent->setStyleSheet(m_LastStyle); if (m_LastTheme) { m_Parent->setStyle(m_LastTheme); m_Parent->m_Settings->Theme(m_LastTheme->objectName()); } m_Parent->m_Controller->FillVariationTreeWithCurrentXform(); QDialog::reject(); } /// <summary> /// Shows the event. /// </summary> /// <param name="e">The e.</param> void QssDialog::showEvent(QShowEvent* e) { if (m_Parent) { m_LastStyle = m_Parent->styleSheet(); m_LastTheme = m_Parent->m_Theme;//The style() member cannot be relied upon, it is *not* the same object passed to setStyle(); SetText(m_LastStyle); } QDialog::showEvent(e); } /// <summary> /// Start the timer which will analyze and apply the current stylesheet text. /// Each successive keystroke will reset the timer if it has not timed out yet. /// This is only called when the dialog is visible because it seems to be spurriously /// called on startup. /// Called when the user changes the text in main text box. /// </summary> void QssDialog::SlotTextChanged() { if (isVisible())//Sometimes this fires even though the window is not shown yet. m_ApplyTimer->start(); } /// <summary> /// Add a color string to the stylesheet text. /// Called when the user clicks the add color menu. /// </summary> /// <param name="s">The color string selector to add</param> void QssDialog::SlotAddColor(const QString& s) { const QColor color = QColorDialog::getColor(0xffffffff, this, QString(), QColorDialog::ShowAlphaChannel); if (!color.isValid()) return; QString colorStr; if (color.alpha() == 255) { colorStr = QString(QStringLiteral("rgb(%1, %2, %3)")).arg( color.red()).arg(color.green()).arg(color.blue()); } else { colorStr = QString(QStringLiteral("rgba(%1, %2, %3, %4)")).arg( color.red()).arg(color.green()).arg(color.blue()).arg(color.alpha()); } InsertCssProperty(s, colorStr); } /// <summary> /// Adds a geometry string to the stylesheet text. /// </summary> /// <param name="s">The geometry string to add</param> void QssDialog::SlotAddGeom(const QString& s) { auto val = m_GeomMap[s]; InsertCssProperty(s, val); } /// <summary> /// Adds a border string to the stylesheet text. /// </summary> /// <param name="s">The border string to add</param> void QssDialog::SlotAddBorder(const QString& s) { auto val = m_BorderMap[s]; InsertCssProperty(s, val); } /// <summary> /// Set the theme to the user selection. /// Called when the user selects an item on the theme combo box. /// </summary> /// <param name="s">The s.</param> void QssDialog::SlotSetTheme(const QString& s) { if (auto theme = QStyleFactory::create(s)) { m_Theme = theme; m_Parent->setStyle(m_Theme); } } /// <summary> /// Add a font string. /// Called when the user clicks the add font menu button. /// </summary> void QssDialog::SlotAddFont() { bool ok; auto font = QFontDialog::getFont(&ok, this); if (ok) { QString fontStr; if (font.weight() != QFont::Normal) { fontStr += QString::number(font.weight()); fontStr += QLatin1Char(' '); } switch (font.style()) { case QFont::StyleItalic: fontStr += QStringLiteral("italic "); break; case QFont::StyleOblique: fontStr += QStringLiteral("oblique "); break; default: break; } fontStr += QString::number(font.pointSize()); fontStr += QStringLiteral("pt \""); fontStr += font.family(); fontStr += QLatin1Char('"'); InsertCssProperty(QStringLiteral("font"), fontStr); } } /// <summary> /// Check if the current stylesheet is valid and apply it if so. /// Also indicate via label whether it was valid. /// </summary> void QssDialog::SlotApplyCss() { auto label = ui->QssValidityLabel; auto style = Text(); const bool valid = IsStyleSheetValid(style); ui->QssButtonBox->button(QDialogButtonBox::Ok)->setEnabled(valid); if (valid) { label->setText(tr("Valid Style Sheet")); label->setStyleSheet(QStringLiteral("color: green")); m_Parent->setStyleSheet(style); m_Parent->m_Controller->FillVariationTreeWithCurrentXform(); } else { label->setText(tr("Invalid Style Sheet")); label->setStyleSheet(QStringLiteral("color: red")); } } /// <summary> /// Load a stylesheet from disk. /// Called when the user clicks the load button. /// </summary> void QssDialog::LoadButton_clicked() { string s; auto f = OpenFile(); if (!f.isEmpty() && ReadFile(f.toStdString().c_str(), s) && !s.empty()) SetText(QString::fromStdString(s)); setWindowTitle("QSS Editor - " + f); } /// <summary> /// Save the stylesheet to disk. /// Called when the user clicks the save button. /// The user cannot save to default.qss, as it's a special placeholder. /// When they exit the dialog by clicking OK, the currently displayed stylesheet /// will be saved to default.qss. /// </summary> void QssDialog::SaveButton_clicked() { auto path = SaveFile(); if (path.toLower().endsWith("default.qss")) { QMessageBox::critical(this, "File save error", "Stylesheet cannot be saved to default.qss. Save it to a different file name, then exit the dialog by clicking OK which will set it as the default."); return; } if (!path.isEmpty()) { ofstream of(path.toStdString()); string s = Text().toStdString(); if (of.is_open()) of << s; else QMessageBox::critical(this, "File save error", "Failed to save " + path + ", style will not be set as default"); } } /// <summary> /// Save the stylesheet to the default.qss on disk. /// This will be loaded the next time Fractorium runs. /// Called when the user clicks ok. /// </summary> void QssDialog::SaveAsDefault() { auto path = m_Parent->m_SettingsPath + "/default.qss"; ofstream of(path.toStdString()); auto s = Text().toStdString(); if (of.is_open()) of << s; else QMessageBox::critical(this, "File save error", "Failed to save " + path + ", style will not be set as default"); } /// <summary> /// Fill the main text box with the most basic style. /// Called when the Basic button is clicked. /// </summary> void QssDialog::BasicButton_clicked() { SetText(BaseStyle()); setWindowTitle("QSS Editor"); } /// <summary> /// Fill the main text box with a medium specificity style. /// This will expose all control types in the application. /// Called when the Medium button is clicked. /// </summary> void QssDialog::MediumButton_clicked() { QString str = BaseStyle(); auto names = GetClassNames(false); for (auto& it : names) str += it + QString("\n{\n\t\n}\n\n"); SetText(str); setWindowTitle("QSS Editor"); } /// <summary> /// Fill the main text box with the most advanced style. /// This will expose all control types in the application as well as their named instances. /// Called when the Advanced button is clicked. /// </summary> void QssDialog::AdvancedButton_clicked() { QString str = BaseStyle(); auto names = GetClassNames(true); for (auto& it : names) str += it + QString("\n{\n\t\n}\n\n"); SetText(str); setWindowTitle("QSS Editor"); } /// <summary> /// Insert a CSS property. /// This is called whenever the user inserts a value via the menus. /// </summary> /// <param name="name">The name of the property to insert</param> /// <param name="value">The value of the property to insert</param> void QssDialog::InsertCssProperty(const QString& name, const QString& value) { auto editor = ui->QssEdit; auto cursor = editor->textCursor(); if (!name.isEmpty()) { cursor.beginEditBlock(); cursor.removeSelectedText(); cursor.movePosition(QTextCursor::EndOfLine); //Simple check to see if we're in a selector scope. const QTextDocument* doc = editor->document(); const QTextCursor closing = doc->find(QStringLiteral("}"), cursor, QTextDocument::FindBackward); const QTextCursor opening = doc->find(QStringLiteral("{"), cursor, QTextDocument::FindBackward); const bool inSelector = !opening.isNull() && (closing.isNull() || closing.position() < opening.position()); QString insertion; //Reasonable attempt at positioning things correctly. This can and often is wrong, but is sufficient for our purposes. if (editor->textCursor().block().length() != 1 && !editor->textCursor().block().text().isEmpty()) insertion += QLatin1Char('\n'); if (inSelector && editor->textCursor().block().text() != "\t") insertion += QLatin1Char('\t'); insertion += name; insertion += QStringLiteral(": "); insertion += value; insertion += QLatin1Char(';'); cursor.insertText(insertion); cursor.endEditBlock(); } else { cursor.insertText(value); } } /// <summary> /// Initial file dialog creation. /// This will perform lazy instantiation since it takes a long time. /// </summary> void QssDialog::SetupFileDialog() { #ifndef __APPLE__ if (!m_FileDialog) { m_FileDialog = new QFileDialog(this); m_FileDialog->setViewMode(QFileDialog::List); m_FileDialog->setDirectory(m_Parent->m_SettingsPath); m_FileDialog->setOption(QFileDialog::DontUseNativeDialog, true); m_FileDialog->setSidebarUrls(dynamic_cast<Fractorium*>(parent())->Urls()); } #endif } /// <summary> /// Present a file open dialog and retun the file selected. /// </summary> /// <returns>The file selected if any, else empty string.</returns> QString QssDialog::OpenFile() { #ifndef __APPLE__ QStringList filenames; SetupFileDialog(); m_FileDialog->setFileMode(QFileDialog::ExistingFile); m_FileDialog->setAcceptMode(QFileDialog::AcceptOpen); m_FileDialog->setNameFilter("Qss (*.qss)"); m_FileDialog->setWindowTitle("Open Stylesheet"); m_FileDialog->selectNameFilter("*.qss"); if (m_FileDialog->exec() == QDialog::Accepted) filenames = m_FileDialog->selectedFiles(); return !filenames.empty() ? filenames[0] : ""; #else auto filename = QFileDialog::getOpenFileName(this, tr("Open Stylesheet"), m_Parent->m_SettingsPath, tr("Qss (*.qss)")); return filename.size() > 0 ? filename : ""; #endif } /// <summary> /// Present a file save dialog and retun the file selected. /// </summary> /// <returns>The file selected for saving if any, else empty string.</returns> QString QssDialog::SaveFile() { #ifndef __APPLE__ QStringList filenames; SetupFileDialog(); m_FileDialog->setFileMode(QFileDialog::AnyFile); m_FileDialog->setAcceptMode(QFileDialog::AcceptSave); m_FileDialog->setNameFilter("Qss (*.qss)"); m_FileDialog->setWindowTitle("Save Stylesheet"); m_FileDialog->selectNameFilter("*.qss"); if (m_FileDialog->exec() == QDialog::Accepted) filenames = m_FileDialog->selectedFiles(); return !filenames.empty() ? filenames[0] : ""; #else auto filename = QFileDialog::getSaveFileName(this, tr("Save Stylesheet"), m_Parent->m_SettingsPath, tr("Qss (*.qss)")); return filename.size() > 0 ? filename : ""; #endif }