1 | // Copyright (C) 2016 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 |
3 | |
4 | #include "stylesheeteditor_p.h" |
5 | #include "csshighlighter_p.h" |
6 | #include "iconselector_p.h" |
7 | #include "qtgradientmanager.h" |
8 | #include "qtgradientviewdialog.h" |
9 | #include "qtgradientutils.h" |
10 | #include "qdesigner_utils_p.h" |
11 | |
12 | #include <QtDesigner/abstractformwindow.h> |
13 | #include <QtDesigner/abstractformwindowcursor.h> |
14 | #include <QtDesigner/abstractformeditor.h> |
15 | #include <QtDesigner/propertysheet.h> |
16 | #include <QtDesigner/abstractintegration.h> |
17 | #include <QtDesigner/abstractsettings.h> |
18 | #include <QtDesigner/qextensionmanager.h> |
19 | |
20 | #include <texteditfindwidget.h> |
21 | |
22 | #include <QtWidgets/qcolordialog.h> |
23 | #include <QtWidgets/qdialogbuttonbox.h> |
24 | #include <QtWidgets/qfontdialog.h> |
25 | #include <QtWidgets/qmenu.h> |
26 | #include <QtWidgets/qpushbutton.h> |
27 | #include <QtWidgets/qtoolbar.h> |
28 | #include <QtWidgets/qboxlayout.h> |
29 | |
30 | #include <QtGui/qaction.h> |
31 | #include <QtGui/qevent.h> |
32 | #include <QtGui/qtextdocument.h> |
33 | |
34 | #include <private/qcssparser_p.h> |
35 | |
36 | QT_BEGIN_NAMESPACE |
37 | |
38 | using namespace Qt::StringLiterals; |
39 | |
40 | static const char styleSheetProperty[] = "styleSheet" ; |
41 | static const char StyleSheetDialogC[] = "StyleSheetDialog" ; |
42 | static const char seGeometry[] = "Geometry" ; |
43 | |
44 | namespace qdesigner_internal { |
45 | |
46 | StyleSheetEditor::StyleSheetEditor(QWidget *parent) |
47 | : QTextEdit(parent) |
48 | { |
49 | enum : int { DarkThreshold = 200 }; // Observed 239 on KDE/Dark |
50 | |
51 | setTabStopDistance(fontMetrics().horizontalAdvance(u' ') * 4); |
52 | setAcceptRichText(false); |
53 | |
54 | const QColor textColor = palette().color(cr: QPalette::WindowText); |
55 | const bool darkMode = textColor.red() > DarkThreshold |
56 | && textColor.green() > DarkThreshold |
57 | && textColor.blue() > DarkThreshold; |
58 | |
59 | CssHighlightColors colors; |
60 | colors.selector = darkMode ? QColor(Qt::red).lighter() : QColor(Qt::darkRed); |
61 | const QColor blue(Qt::blue); |
62 | colors.property = darkMode ? blue.lighter() : blue; |
63 | colors.pseudo1 = colors.pseudo2 = colors.value = textColor; |
64 | colors.quote = darkMode ? Qt::magenta : Qt::darkMagenta; |
65 | colors.comment = darkMode ? Qt::green : Qt::darkGreen; |
66 | |
67 | new CssHighlighter(colors, document()); |
68 | } |
69 | |
70 | // --- StyleSheetEditorDialog |
71 | StyleSheetEditorDialog::StyleSheetEditorDialog(QDesignerFormEditorInterface *core, QWidget *parent, Mode mode): |
72 | QDialog(parent), |
73 | m_buttonBox(new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel|QDialogButtonBox::Help)), |
74 | m_editor(new StyleSheetEditor), |
75 | m_findWidget(new TextEditFindWidget), |
76 | m_validityLabel(new QLabel(tr(s: "Valid Style Sheet" ))), |
77 | m_core(core), |
78 | m_addResourceAction(new QAction(tr(s: "Add Resource..." ), this)), |
79 | m_addGradientAction(new QAction(tr(s: "Add Gradient..." ), this)), |
80 | m_addColorAction(new QAction(tr(s: "Add Color..." ), this)), |
81 | m_addFontAction(new QAction(tr(s: "Add Font..." ), this)) |
82 | { |
83 | setWindowTitle(tr(s: "Edit Style Sheet" )); |
84 | |
85 | connect(sender: m_buttonBox, signal: &QDialogButtonBox::accepted, context: this, slot: &QDialog::accept); |
86 | connect(sender: m_buttonBox, signal: &QDialogButtonBox::rejected, context: this, slot: &QDialog::reject); |
87 | connect(sender: m_buttonBox, signal: &QDialogButtonBox::helpRequested, |
88 | context: this, slot: &StyleSheetEditorDialog::slotRequestHelp); |
89 | m_buttonBox->button(which: QDialogButtonBox::Help)->setShortcut(QKeySequence::HelpContents); |
90 | |
91 | connect(sender: m_editor, signal: &QTextEdit::textChanged, context: this, slot: &StyleSheetEditorDialog::validateStyleSheet); |
92 | m_findWidget->setTextEdit(m_editor); |
93 | |
94 | QToolBar *toolBar = new QToolBar; |
95 | |
96 | QGridLayout *layout = new QGridLayout; |
97 | layout->addWidget(toolBar, row: 0, column: 0, rowSpan: 1, columnSpan: 2); |
98 | layout->addWidget(m_editor, row: 1, column: 0, rowSpan: 1, columnSpan: 2); |
99 | layout->addWidget(m_findWidget, row: 2, column: 0, rowSpan: 1, columnSpan: 2); |
100 | layout->addWidget(m_validityLabel, row: 3, column: 0, rowSpan: 1, columnSpan: 1); |
101 | layout->addWidget(m_buttonBox, row: 3, column: 1, rowSpan: 1, columnSpan: 1); |
102 | setLayout(layout); |
103 | |
104 | m_editor->setContextMenuPolicy(Qt::CustomContextMenu); |
105 | connect(sender: m_editor, signal: &QWidget::customContextMenuRequested, |
106 | context: this, slot: &StyleSheetEditorDialog::slotContextMenuRequested); |
107 | |
108 | connect(sender: m_addResourceAction, signal: &QAction::triggered, |
109 | context: this, slot: [this] { this->slotAddResource(property: QString()); }); |
110 | connect(sender: m_addGradientAction, signal: &QAction::triggered, |
111 | context: this, slot: [this] { this->slotAddGradient(property: QString()); }); |
112 | connect(sender: m_addColorAction, signal: &QAction::triggered, |
113 | context: this, slot: [this] { this->slotAddColor(property: QString()); }); |
114 | connect(sender: m_addFontAction, signal: &QAction::triggered, context: this, slot: &StyleSheetEditorDialog::slotAddFont); |
115 | |
116 | m_addResourceAction->setEnabled(mode == ModePerForm); |
117 | |
118 | const char * const resourceProperties[] = { |
119 | "background-image" , |
120 | "border-image" , |
121 | "image" , |
122 | nullptr |
123 | }; |
124 | |
125 | const char * const colorProperties[] = { |
126 | "color" , |
127 | "background-color" , |
128 | "alternate-background-color" , |
129 | "border-color" , |
130 | "border-top-color" , |
131 | "border-right-color" , |
132 | "border-bottom-color" , |
133 | "border-left-color" , |
134 | "gridline-color" , |
135 | "selection-color" , |
136 | "selection-background-color" , |
137 | nullptr |
138 | }; |
139 | |
140 | QMenu * = new QMenu(this); |
141 | QMenu * = new QMenu(this); |
142 | QMenu * = new QMenu(this); |
143 | |
144 | for (int resourceProperty = 0; resourceProperties[resourceProperty]; ++resourceProperty) { |
145 | const QString resourcePropertyName = QLatin1StringView(resourceProperties[resourceProperty]); |
146 | resourceActionMenu->addAction(text: resourcePropertyName, |
147 | args: this, args: [this, resourcePropertyName] { this->slotAddResource(property: resourcePropertyName); }); |
148 | } |
149 | |
150 | for (int colorProperty = 0; colorProperties[colorProperty]; ++colorProperty) { |
151 | const QString colorPropertyName = QLatin1StringView(colorProperties[colorProperty]); |
152 | colorActionMenu->addAction(text: colorPropertyName, |
153 | args: this, args: [this, colorPropertyName] { this->slotAddColor(property: colorPropertyName); }); |
154 | gradientActionMenu->addAction(text: colorPropertyName, |
155 | args: this, args: [this, colorPropertyName] { this->slotAddGradient(property: colorPropertyName); } ); |
156 | } |
157 | |
158 | m_addResourceAction->setMenu(resourceActionMenu); |
159 | m_addGradientAction->setMenu(gradientActionMenu); |
160 | m_addColorAction->setMenu(colorActionMenu); |
161 | |
162 | |
163 | toolBar->addAction(action: m_addResourceAction); |
164 | toolBar->addAction(action: m_addGradientAction); |
165 | toolBar->addAction(action: m_addColorAction); |
166 | toolBar->addAction(action: m_addFontAction); |
167 | m_findAction = m_findWidget->createFindAction(parent: toolBar); |
168 | toolBar->addAction(action: m_findAction); |
169 | |
170 | m_editor->setFocus(); |
171 | |
172 | QDesignerSettingsInterface *settings = core->settingsManager(); |
173 | settings->beginGroup(prefix: QLatin1StringView(StyleSheetDialogC)); |
174 | |
175 | if (settings->contains(key: QLatin1StringView(seGeometry))) |
176 | restoreGeometry(geometry: settings->value(key: QLatin1StringView(seGeometry)).toByteArray()); |
177 | |
178 | settings->endGroup(); |
179 | } |
180 | |
181 | StyleSheetEditorDialog::~StyleSheetEditorDialog() |
182 | { |
183 | QDesignerSettingsInterface *settings = m_core->settingsManager(); |
184 | settings->beginGroup(prefix: QLatin1StringView(StyleSheetDialogC)); |
185 | |
186 | settings->setValue(key: QLatin1StringView(seGeometry), value: saveGeometry()); |
187 | settings->endGroup(); |
188 | } |
189 | |
190 | void StyleSheetEditorDialog::setOkButtonEnabled(bool v) |
191 | { |
192 | m_buttonBox->button(which: QDialogButtonBox::Ok)->setEnabled(v); |
193 | if (QPushButton *applyButton = m_buttonBox->button(which: QDialogButtonBox::Apply)) |
194 | applyButton->setEnabled(v); |
195 | } |
196 | |
197 | void StyleSheetEditorDialog::(const QPoint &pos) |
198 | { |
199 | QMenu * = m_editor->createStandardContextMenu(); |
200 | menu->addSeparator(); |
201 | menu->addAction(action: m_findAction); |
202 | menu->addSeparator(); |
203 | menu->addAction(action: m_addResourceAction); |
204 | menu->addAction(action: m_addGradientAction); |
205 | menu->exec(pos: mapToGlobal(pos)); |
206 | delete menu; |
207 | } |
208 | |
209 | void StyleSheetEditorDialog::slotAddResource(const QString &property) |
210 | { |
211 | const QString path = IconSelector::choosePixmapResource(core: m_core, resourceModel: m_core->resourceModel(), oldPath: QString(), parent: this); |
212 | if (!path.isEmpty()) |
213 | insertCssProperty(name: property, value: "url("_L1 + path + u')'); |
214 | } |
215 | |
216 | void StyleSheetEditorDialog::slotAddGradient(const QString &property) |
217 | { |
218 | bool ok; |
219 | const QGradient grad = QtGradientViewDialog::getGradient(ok: &ok, manager: m_core->gradientManager(), parent: this); |
220 | if (ok) |
221 | insertCssProperty(name: property, value: QtGradientUtils::styleSheetCode(gradient: grad)); |
222 | } |
223 | |
224 | void StyleSheetEditorDialog::slotAddColor(const QString &property) |
225 | { |
226 | const QColor color = QColorDialog::getColor(initial: 0xffffffff, parent: this, title: QString(), options: QColorDialog::ShowAlphaChannel); |
227 | if (!color.isValid()) |
228 | return; |
229 | |
230 | QString colorStr; |
231 | |
232 | if (color.alpha() == 255) { |
233 | colorStr = QString::asprintf(format: "rgb(%d, %d, %d)" , |
234 | color.red(), color.green(), color.blue()); |
235 | } else { |
236 | colorStr = QString::asprintf(format: "rgba(%d, %d, %d, %d)" , |
237 | color.red(), color.green(), color.blue(), |
238 | color.alpha()); |
239 | } |
240 | |
241 | insertCssProperty(name: property, value: colorStr); |
242 | } |
243 | |
244 | void StyleSheetEditorDialog::slotAddFont() |
245 | { |
246 | bool ok; |
247 | QFont font = QFontDialog::getFont(ok: &ok, parent: this); |
248 | if (ok) { |
249 | QString fontStr; |
250 | if (font.weight() != QFont::Normal) |
251 | fontStr += QString::number(font.weight()) + u' '; |
252 | |
253 | switch (font.style()) { |
254 | case QFont::StyleItalic: |
255 | fontStr += "italic "_L1 ; |
256 | break; |
257 | case QFont::StyleOblique: |
258 | fontStr += "oblique "_L1 ; |
259 | break; |
260 | default: |
261 | break; |
262 | } |
263 | fontStr += QString::number(font.pointSize()); |
264 | fontStr += "pt \""_L1 ; |
265 | fontStr += font.family(); |
266 | fontStr += u'"'; |
267 | |
268 | insertCssProperty(name: u"font"_s , value: fontStr); |
269 | QString decoration; |
270 | if (font.underline()) |
271 | decoration += "underline"_L1 ; |
272 | if (font.strikeOut()) { |
273 | if (!decoration.isEmpty()) |
274 | decoration += u' '; |
275 | decoration += "line-through"_L1 ; |
276 | } |
277 | insertCssProperty(name: u"text-decoration"_s , value: decoration); |
278 | } |
279 | } |
280 | |
281 | void StyleSheetEditorDialog::insertCssProperty(const QString &name, const QString &value) |
282 | { |
283 | if (!value.isEmpty()) { |
284 | QTextCursor cursor = m_editor->textCursor(); |
285 | if (!name.isEmpty()) { |
286 | cursor.beginEditBlock(); |
287 | cursor.removeSelectedText(); |
288 | cursor.movePosition(op: QTextCursor::EndOfLine); |
289 | |
290 | // Simple check to see if we're in a selector scope |
291 | const QTextDocument *doc = m_editor->document(); |
292 | const QTextCursor closing = doc->find(subString: u"}"_s , cursor, options: QTextDocument::FindBackward); |
293 | const QTextCursor opening = doc->find(subString: u"{"_s , cursor, options: QTextDocument::FindBackward); |
294 | const bool inSelector = !opening.isNull() && (closing.isNull() || |
295 | closing.position() < opening.position()); |
296 | QString insertion; |
297 | if (m_editor->textCursor().block().length() != 1) |
298 | insertion += u'\n'; |
299 | if (inSelector) |
300 | insertion += u'\t'; |
301 | insertion += name; |
302 | insertion += ": "_L1 ; |
303 | insertion += value; |
304 | insertion += u';'; |
305 | cursor.insertText(text: insertion); |
306 | cursor.endEditBlock(); |
307 | } else { |
308 | cursor.insertText(text: value); |
309 | } |
310 | } |
311 | } |
312 | |
313 | void StyleSheetEditorDialog::slotRequestHelp() |
314 | { |
315 | m_core->integration()->emitHelpRequested(manual: u"qtwidgets"_s , |
316 | document: u"stylesheet-reference.html"_s ); |
317 | } |
318 | |
319 | // See QDialog::keyPressEvent() |
320 | static inline bool isEnter(const QKeyEvent *e) |
321 | { |
322 | const bool isEnter = e->key() == Qt::Key_Enter; |
323 | const bool isReturn = e->key() == Qt::Key_Return; |
324 | return (e->modifiers() == Qt::KeyboardModifiers() && (isEnter || isReturn)) |
325 | || (e->modifiers().testFlag(flag: Qt::KeypadModifier) && isEnter); |
326 | } |
327 | |
328 | void StyleSheetEditorDialog::keyPressEvent(QKeyEvent *e) |
329 | { |
330 | // As long as the find widget is visible, suppress the default button |
331 | // behavior (close on Enter) of QDialog. |
332 | if (!(m_findWidget->isVisible() && isEnter(e))) |
333 | QDialog::keyPressEvent(e); |
334 | } |
335 | |
336 | QDialogButtonBox * StyleSheetEditorDialog::buttonBox() const |
337 | { |
338 | return m_buttonBox; |
339 | } |
340 | |
341 | QString StyleSheetEditorDialog::text() const |
342 | { |
343 | return m_editor->toPlainText(); |
344 | } |
345 | |
346 | void StyleSheetEditorDialog::setText(const QString &t) |
347 | { |
348 | m_editor->setText(t); |
349 | } |
350 | |
351 | bool StyleSheetEditorDialog::isStyleSheetValid(const QString &styleSheet) |
352 | { |
353 | QCss::Parser parser(styleSheet); |
354 | QCss::StyleSheet sheet; |
355 | if (parser.parse(styleSheet: &sheet)) |
356 | return true; |
357 | QCss::Parser parser2("* { "_L1 + styleSheet + '}'_L1); |
358 | return parser2.parse(styleSheet: &sheet); |
359 | } |
360 | |
361 | void StyleSheetEditorDialog::validateStyleSheet() |
362 | { |
363 | const bool valid = isStyleSheetValid(styleSheet: m_editor->toPlainText()); |
364 | setOkButtonEnabled(valid); |
365 | if (valid) { |
366 | m_validityLabel->setText(tr(s: "Valid Style Sheet" )); |
367 | m_validityLabel->setStyleSheet(u"color: green"_s ); |
368 | } else { |
369 | m_validityLabel->setText(tr(s: "Invalid Style Sheet" )); |
370 | m_validityLabel->setStyleSheet(u"color: red"_s ); |
371 | } |
372 | } |
373 | |
374 | // --- StyleSheetPropertyEditorDialog |
375 | StyleSheetPropertyEditorDialog::StyleSheetPropertyEditorDialog(QWidget *parent, |
376 | QDesignerFormWindowInterface *fw, |
377 | QWidget *widget): |
378 | StyleSheetEditorDialog(fw->core(), parent), |
379 | m_fw(fw), |
380 | m_widget(widget) |
381 | { |
382 | Q_ASSERT(m_fw != nullptr); |
383 | |
384 | QPushButton *apply = buttonBox()->addButton(button: QDialogButtonBox::Apply); |
385 | QObject::connect(sender: apply, signal: &QAbstractButton::clicked, |
386 | context: this, slot: &StyleSheetPropertyEditorDialog::applyStyleSheet); |
387 | QObject::connect(sender: buttonBox(), signal: &QDialogButtonBox::accepted, |
388 | context: this, slot: &StyleSheetPropertyEditorDialog::applyStyleSheet); |
389 | |
390 | QDesignerPropertySheetExtension *sheet = |
391 | qt_extension<QDesignerPropertySheetExtension*>(manager: m_fw->core()->extensionManager(), object: m_widget); |
392 | Q_ASSERT(sheet != nullptr); |
393 | const int index = sheet->indexOf(name: QLatin1StringView(styleSheetProperty)); |
394 | const PropertySheetStringValue value = qvariant_cast<PropertySheetStringValue>(v: sheet->property(index)); |
395 | setText(value.value()); |
396 | } |
397 | |
398 | void StyleSheetPropertyEditorDialog::applyStyleSheet() |
399 | { |
400 | const PropertySheetStringValue value(text(), false); |
401 | m_fw->cursor()->setWidgetProperty(widget: m_widget, name: QLatin1StringView(styleSheetProperty), value: QVariant::fromValue(value)); |
402 | } |
403 | |
404 | } // namespace qdesigner_internal |
405 | |
406 | QT_END_NAMESPACE |
407 | |