#include "FractoriumPch.h" #include "QssDialog.h" #include "ui_QssDialog.h" #include "qcssscanner.h" /// /// 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. /// /// /// 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. /// /// The first string to compare /// The second string to compare /// True if s1 < s2 with special rules for 'Q' taken into account. 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; } /// /// Construct a QssDialog. /// This manually constructs much of the menu GUI via code rather /// than in the designer. /// /// The main Fractorium window. 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> colorVec; colorVec.reserve(12); colorVec.push_back(QPair("color", "")); colorVec.push_back(QPair("background-color", "")); colorVec.push_back(QPair("alternate-background-color", "")); colorVec.push_back(QPair("border-color", "")); colorVec.push_back(QPair("border-top-color", "")); colorVec.push_back(QPair("border-right-color", "")); colorVec.push_back(QPair("border-bottom-color", "")); colorVec.push_back(QPair("border-left-color", "")); colorVec.push_back(QPair("gridline-color", "")); colorVec.push_back(QPair("selection-color", "")); colorVec.push_back(QPair("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> geomVec; geomVec.reserve(12); geomVec.push_back(QPair("width", "100px")); geomVec.push_back(QPair("height", "50px")); geomVec.push_back(QPair("spacing", "10")); geomVec.push_back(QPair("padding", "3px")); geomVec.push_back(QPair("padding-top", "3px")); geomVec.push_back(QPair("padding-right", "3px")); geomVec.push_back(QPair("padding-bottom", "3px")); geomVec.push_back(QPair("padding-left", "3px")); geomVec.push_back(QPair("margin", "3px")); geomVec.push_back(QPair("margin-top", "3px")); geomVec.push_back(QPair("margin-right", "3px")); geomVec.push_back(QPair("margin-bottom", "3px")); geomVec.push_back(QPair("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> borderVec; borderVec.reserve(8); borderVec.push_back(QPair("border", "1px solid black")); borderVec.push_back(QPair("border-top", "1px inset black")); borderVec.push_back(QPair("border-right", "1px outset black")); borderVec.push_back(QPair("border-bottom", "1px ridge black")); borderVec.push_back(QPair("border-left", "1px groove black")); borderVec.push_back(QPair("border-style", "double")); borderVec.push_back(QPair("border-width", "1px")); borderVec.push_back(QPair("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())); } /// /// Destructor that stops the apply timer and deletes the ui. /// QssDialog::~QssDialog() { m_ApplyTimer->stop(); delete ui; } /// /// Thin wrapper around getting the text from the main text box as plain text. /// /// The plain text of the main text box QString QssDialog::Text() const { return ui->QssEdit->toPlainText(); } /// /// Thin wrapper around setting the text of the main text box. /// /// The text to set void QssDialog::SetText(const QString& t) { ui->QssEdit->setText(t); } /// /// 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. /// /// Whether to get the individual object names as well /// A list of all class names with optional entries for each individual object QList QssDialog::GetClassNames(bool includeObjectNames) { QSet classNames; QList> dialogClassNames; auto widgetList = m_Parent->findChildren(); 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(widgetList[i]); if (dlg)//Dialogs only nest one level deep, so no need for generalized recursion. { QSet dlgSet; auto dlgWidgetList = dlg->findChildren();//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(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; } /// /// 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. /// /// The stylesheet text to analyze. /// True if valid, else false. 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); } /// /// 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. /// void QssDialog::accept() { if (m_Theme) m_Parent->m_Settings->Theme(m_Theme->objectName()); SaveAsDefault(); QDialog::accept(); } /// /// 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. /// 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(); } /// /// Shows the event. /// /// The e. 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); } /// /// 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. /// void QssDialog::SlotTextChanged() { if (isVisible())//Sometimes this fires even though the window is not shown yet. m_ApplyTimer->start(); } /// /// Add a color string to the stylesheet text. /// Called when the user clicks the add color menu. /// /// The color string selector to add 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); } /// /// Adds a geometry string to the stylesheet text. /// /// The geometry string to add void QssDialog::SlotAddGeom(const QString& s) { auto val = m_GeomMap[s]; InsertCssProperty(s, val); } /// /// Adds a border string to the stylesheet text. /// /// The border string to add void QssDialog::SlotAddBorder(const QString& s) { auto val = m_BorderMap[s]; InsertCssProperty(s, val); } /// /// Set the theme to the user selection. /// Called when the user selects an item on the theme combo box. /// /// The s. void QssDialog::SlotSetTheme(const QString& s) { if (auto theme = QStyleFactory::create(s)) { m_Theme = theme; m_Parent->setStyle(m_Theme); } } /// /// Add a font string. /// Called when the user clicks the add font menu button. /// 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); } } /// /// Check if the current stylesheet is valid and apply it if so. /// Also indicate via label whether it was valid. /// 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")); } } /// /// Load a stylesheet from disk. /// Called when the user clicks the load button. /// 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); } /// /// 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. /// 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"); } } /// /// Save the stylesheet to the default.qss on disk. /// This will be loaded the next time Fractorium runs. /// Called when the user clicks ok. /// 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"); } /// /// Fill the main text box with the most basic style. /// Called when the Basic button is clicked. /// void QssDialog::BasicButton_clicked() { SetText(BaseStyle()); setWindowTitle("QSS Editor"); } /// /// 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. /// 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"); } /// /// 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. /// 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"); } /// /// Insert a CSS property. /// This is called whenever the user inserts a value via the menus. /// /// The name of the property to insert /// The value of the property to insert 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); } } /// /// Initial file dialog creation. /// This will perform lazy instantiation since it takes a long time. /// 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(parent())->Urls()); } #endif } /// /// Present a file open dialog and retun the file selected. /// /// The file selected if any, else empty string. 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 } /// /// Present a file save dialog and retun the file selected. /// /// The file selected for saving if any, else empty string. 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 }