1/*
2 SPDX-FileCopyrightText: 1996 Bernd Johannes Wuebben <wuebben@kde.org>
3 SPDX-FileCopyrightText: 1999 Preston Brown <pbrown@kde.org>
4 SPDX-FileCopyrightText: 1999 Mario Weilguni <mweilguni@kde.org>
5
6 SPDX-License-Identifier: LGPL-2.0-or-later
7*/
8
9#include "kfontchooser.h"
10#include "fonthelpers_p.h"
11#include "ui_kfontchooserwidget.h"
12
13#include "loggingcategory.h"
14
15#include <QCheckBox>
16#include <QDoubleSpinBox>
17#include <QFontDatabase>
18#include <QGroupBox>
19#include <QGuiApplication>
20#include <QLabel>
21#include <QLayout>
22#include <QListWidget>
23#include <QLocale>
24#include <QScrollBar>
25#include <QSplitter>
26#include <QTextEdit>
27#include <QTimer>
28
29#include <algorithm>
30#include <cmath>
31
32// When message extraction needs to be avoided.
33#define TR_NOX tr
34
35static int minimumListWidth(const QListWidget *list)
36{
37 QFontMetrics fm = list->fontMetrics();
38
39 const int extraSpace = fm.horizontalAdvance(QLatin1Char(' ')) * 2;
40
41 // Minimum initial size
42 int width = 40;
43 for (int i = 0, rows = list->count(); i < rows; ++i) {
44 int itemWidth = fm.horizontalAdvance(list->item(row: i)->text());
45 // ...and add a space on both sides for a not too tight look.
46 itemWidth += extraSpace;
47 width = std::max(a: width, b: itemWidth);
48 }
49
50 width += list->frameWidth() * 2;
51 width += list->verticalScrollBar()->sizeHint().width();
52 return width;
53}
54
55static int minimumListHeight(const QListWidget *list, int numVisibleEntry)
56{
57 int w = list->fontMetrics().lineSpacing();
58 if (w < 0) {
59 w = 10;
60 }
61
62 if (numVisibleEntry <= 0) {
63 numVisibleEntry = 4;
64 }
65
66 w = w * numVisibleEntry;
67 w += list->frameWidth() * 2;
68 w += list->horizontalScrollBar()->sizeHint().height();
69 return w;
70}
71
72static QString formatFontSize(qreal size)
73{
74 return QLocale::system().toString(f: size, format: 'f', precision: (size == floor(x: size)) ? 0 : 1);
75}
76
77class KFontChooserPrivate
78{
79 Q_DECLARE_TR_FUNCTIONS(KFontChooser)
80
81public:
82 KFontChooserPrivate(KFontChooser::DisplayFlags flags, KFontChooser *qq)
83 : q(qq)
84 , m_flags(flags)
85 {
86 m_palette.setColor(acg: QPalette::Active, acr: QPalette::Text, acolor: Qt::black);
87 m_palette.setColor(acg: QPalette::Active, acr: QPalette::Base, acolor: Qt::white);
88 }
89
90 void init();
91 void setFamilyBoxItems(const QStringList &fonts = {});
92 int nearestSizeRow(qreal val, bool customize);
93 qreal fillSizeList(const QList<qreal> &sizes = QList<qreal>());
94 qreal setupSizeListBox(const QString &family, const QString &style);
95
96 void setupDisplay();
97 QString styleIdentifier(const QFont &font);
98
99 void slotFamilySelected(const QString &);
100 void slotSizeSelected(const QString &);
101 void slotStyleSelected(const QString &);
102 void displaySample(const QFont &font);
103 void slotSizeValue(double);
104 void slotFeaturesChanged(const QString &features);
105
106 KFontChooser *q;
107
108 std::unique_ptr<Ui_KFontChooserWidget> m_ui;
109
110 KFontChooser::DisplayFlags m_flags = KFontChooser::NoDisplayFlags;
111
112 QPalette m_palette;
113
114 QFont m_selectedFont;
115
116 QString m_selectedStyle;
117 qreal m_selectedSize = -1.0;
118
119 QString m_standardSizeAtCustom;
120 int m_customSizeRow = -1;
121
122 bool m_signalsAllowed = true;
123 bool m_usingFixed = false;
124
125 // Mappings of translated to Qt originated family and style strings.
126 FontFamiliesMap m_qtFamilies;
127 std::map<QString, QString> m_qtStyles;
128 // Mapping of translated style strings to internal style identifiers.
129 std::map<QString, QString> m_styleIDs;
130
131 QTimer m_fontFeatureChangedTimer;
132};
133
134KFontChooser::KFontChooser(QWidget *parent)
135 : QWidget(parent)
136 , d(new KFontChooserPrivate(KFontChooser::DisplayFrame, this))
137{
138 d->init();
139}
140
141KFontChooser::KFontChooser(const DisplayFlags flags, QWidget *parent)
142 : QWidget(parent)
143 , d(new KFontChooserPrivate(flags, this))
144{
145 d->init();
146}
147
148KFontChooser::~KFontChooser() = default;
149
150void KFontChooserPrivate::init()
151{
152 m_usingFixed = m_flags & KFontChooser::FixedFontsOnly;
153
154 // The main layout is divided horizontally into a top part with
155 // the font attribute widgets (family, style, size) and a bottom
156 // part with a preview of the selected font
157 QVBoxLayout *mainLayout = new QVBoxLayout(q);
158 mainLayout->setContentsMargins(left: 0, top: 0, right: 0, bottom: 0);
159
160 QWidget *page = m_flags & KFontChooser::DisplayFrame ? new QGroupBox(KFontChooser::tr(s: "Requested Font", c: "@title:group"), q) : new QWidget(q);
161 mainLayout->addWidget(page);
162
163 m_ui.reset(p: new Ui_KFontChooserWidget);
164 m_ui->setupUi(page);
165
166 // Increase spacing on top of the preview field and then reset the other layouts
167 // back to a standard value.
168 m_ui->sampleTextEditLayout->setSpacing(q->style()->pixelMetric(metric: QStyle::PM_LayoutVerticalSpacing));
169 m_ui->mainHorizontalLayout->setSpacing(q->style()->pixelMetric(metric: QStyle::PM_LayoutHorizontalSpacing));
170 m_ui->gridLayout->setVerticalSpacing(q->style()->pixelMetric(metric: QStyle::PM_LayoutVerticalSpacing) * 2);
171 m_ui->gridLayout->setHorizontalSpacing(q->style()->pixelMetric(metric: QStyle::PM_LayoutHorizontalSpacing));
172
173 // Deprecated, we'll call show() if building with deprecated code
174 m_ui->sizeIsRelativeCheckBox->hide();
175
176 const bool isDiffMode = m_flags & KFontChooser::ShowDifferences;
177
178 QObject::connect(sender: m_ui->familyListWidget, signal: &QListWidget::currentTextChanged, slot: [this](const QString &family) {
179 slotFamilySelected(family);
180 });
181
182 if (isDiffMode) {
183 m_ui->familyLabel->hide();
184 m_ui->familyListWidget->setEnabled(false);
185 QObject::connect(sender: m_ui->familyCheckBox, signal: &QCheckBox::toggled, context: m_ui->familyListWidget, slot: &QWidget::setEnabled);
186 } else {
187 m_ui->familyCheckBox->hide();
188 }
189
190 setFamilyBoxItems();
191
192 // If the calling app sets FixedFontsOnly, don't show the "show fixed only" checkbox
193 m_ui->onlyFixedCheckBox->setVisible(!m_usingFixed);
194
195 if (!m_ui->onlyFixedCheckBox->isHidden()) {
196 QObject::connect(sender: m_ui->onlyFixedCheckBox, signal: &QCheckBox::toggled, context: q, slot: [this](const bool state) {
197 q->setFont(font: m_selectedFont, onlyFixed: state);
198 });
199
200 if (isDiffMode) { // In this mode follow the state of the m_ui->familyCheckBox
201 m_ui->onlyFixedCheckBox->setEnabled(false);
202 QObject::connect(sender: m_ui->familyCheckBox, signal: &QCheckBox::toggled, context: m_ui->onlyFixedCheckBox, slot: &QWidget::setEnabled);
203 }
204 }
205
206 // Populate usual styles, to determine minimum list width;
207 // will be replaced later with correct styles.
208 m_ui->styleListWidget->addItem(label: KFontChooser::tr(s: "Normal", c: "@item font"));
209 m_ui->styleListWidget->addItem(label: KFontChooser::tr(s: "Italic", c: "@item font"));
210 m_ui->styleListWidget->addItem(label: KFontChooser::tr(s: "Oblique", c: "@item font"));
211 m_ui->styleListWidget->addItem(label: KFontChooser::tr(s: "Bold", c: "@item font"));
212 m_ui->styleListWidget->addItem(label: KFontChooser::tr(s: "Bold Condensed Oblique", c: "@item font"));
213 m_ui->styleListWidget->setMinimumWidth(minimumListWidth(list: m_ui->styleListWidget));
214
215 QObject::connect(sender: m_ui->styleListWidget, signal: &QListWidget::currentTextChanged, slot: [this](const QString &style) {
216 slotStyleSelected(style);
217 });
218
219 if (isDiffMode) {
220 m_ui->styleLabel->hide();
221 m_ui->styleListWidget->setEnabled(false);
222 QObject::connect(sender: m_ui->styleCheckBox, signal: &QCheckBox::toggled, context: m_ui->styleListWidget, slot: &QWidget::setEnabled);
223 } else {
224 m_ui->styleCheckBox->hide();
225 }
226
227 // Populate with usual sizes, to determine minimum list width;
228 // will be replaced later with correct sizes.
229 fillSizeList();
230
231 QObject::connect(sender: m_ui->sizeSpinBox, signal: &QDoubleSpinBox::valueChanged, slot: [this](const double size) {
232 slotSizeValue(size);
233 });
234
235 QObject::connect(sender: m_ui->sizeListWidget, signal: &QListWidget::currentTextChanged, slot: [this](const QString &size) {
236 slotSizeSelected(size);
237 });
238
239 m_fontFeatureChangedTimer.setInterval(200);
240 m_fontFeatureChangedTimer.setSingleShot(true);
241 m_fontFeatureChangedTimer.callOnTimeout(args: [this]() {
242 slotFeaturesChanged(features: m_ui->fontFeaturesLineEdit->text());
243 });
244
245 QObject::connect(sender: m_ui->fontFeaturesLineEdit, signal: &QLineEdit::textChanged, slot: [this](const QString &) {
246 m_fontFeatureChangedTimer.start();
247 });
248
249 if (isDiffMode) {
250 m_ui->sizeLabel->hide();
251 m_ui->sizeListWidget->setEnabled(false);
252 m_ui->sizeSpinBox->setEnabled(false);
253 QObject::connect(sender: m_ui->sizeCheckBox, signal: &QCheckBox::toggled, context: m_ui->sizeListWidget, slot: &QWidget::setEnabled);
254 QObject::connect(sender: m_ui->sizeCheckBox, signal: &QCheckBox::toggled, context: m_ui->sizeSpinBox, slot: &QWidget::setEnabled);
255 } else {
256 m_ui->sizeCheckBox->hide();
257 }
258
259 QFont tmpFont(q->font().family(), 64, QFont::Black);
260 m_ui->sampleTextEdit->setFont(tmpFont);
261 m_ui->sampleTextEdit->setMinimumHeight(m_ui->sampleTextEdit->fontMetrics().lineSpacing());
262 // tr: A classical test phrase, with all letters of the English alphabet.
263 // Replace it with a sample text in your language, such that it is
264 // representative of language's writing system.
265 // If you wish, you can input several lines of text separated by \n.
266 q->setSampleText(KFontChooser::tr(s: "The Quick Brown Fox Jumps Over The Lazy Dog"));
267 m_ui->sampleTextEdit->setTextCursor(QTextCursor(m_ui->sampleTextEdit->document()));
268
269 QObject::connect(sender: q, signal: &KFontChooser::fontSelected, context: q, slot: [this](const QFont &font) {
270 displaySample(font);
271 });
272
273 // lets initialize the display if possible
274 if (m_usingFixed) {
275 q->setFont(font: QFontDatabase::systemFont(type: QFontDatabase::FixedFont), onlyFixed: true);
276 } else {
277 q->setFont(font: QGuiApplication::font(), onlyFixed: false);
278 }
279
280 // Set the minimum height for the list widgets
281 q->setMinVisibleItems(4);
282
283 // Set focus to the size list as this is the most commonly changed property
284 m_ui->sizeListWidget->setFocus();
285}
286
287void KFontChooser::setColor(const QColor &col)
288{
289 d->m_palette.setColor(acg: QPalette::Active, acr: QPalette::Text, acolor: col);
290 QPalette pal = d->m_ui->sampleTextEdit->palette();
291 pal.setColor(acg: QPalette::Active, acr: QPalette::Text, acolor: col);
292 d->m_ui->sampleTextEdit->setPalette(pal);
293 QTextCursor cursor = d->m_ui->sampleTextEdit->textCursor();
294 d->m_ui->sampleTextEdit->selectAll();
295 d->m_ui->sampleTextEdit->setTextColor(col);
296 d->m_ui->sampleTextEdit->setTextCursor(cursor);
297}
298
299QColor KFontChooser::color() const
300{
301 return d->m_palette.color(cg: QPalette::Active, cr: QPalette::Text);
302}
303
304void KFontChooser::setBackgroundColor(const QColor &col)
305{
306 d->m_palette.setColor(acg: QPalette::Active, acr: QPalette::Base, acolor: col);
307 QPalette pal = d->m_ui->sampleTextEdit->palette();
308 pal.setColor(acg: QPalette::Active, acr: QPalette::Base, acolor: col);
309 d->m_ui->sampleTextEdit->setPalette(pal);
310}
311
312QColor KFontChooser::backgroundColor() const
313{
314 return d->m_palette.color(cg: QPalette::Active, cr: QPalette::Base);
315}
316
317QString KFontChooser::sampleText() const
318{
319 return d->m_ui->sampleTextEdit->toPlainText();
320}
321
322void KFontChooser::setSampleText(const QString &text)
323{
324 d->m_ui->sampleTextEdit->setPlainText(text);
325}
326
327void KFontChooser::setSampleBoxVisible(bool visible)
328{
329 d->m_ui->sampleTextEdit->setVisible(visible);
330}
331
332QSize KFontChooser::sizeHint(void) const
333{
334 return minimumSizeHint();
335}
336
337void KFontChooser::enableColumn(int column, bool state)
338{
339 if (column & FamilyList) {
340 d->m_ui->familyListWidget->setEnabled(state);
341 }
342 if (column & StyleList) {
343 d->m_ui->styleListWidget->setEnabled(state);
344 }
345 if (column & SizeList) {
346 d->m_ui->sizeListWidget->setEnabled(state);
347 d->m_ui->sizeSpinBox->setEnabled(state);
348 }
349}
350
351void KFontChooser::setFont(const QFont &aFont, bool onlyFixed)
352{
353 d->m_selectedFont = aFont;
354 d->m_selectedSize = aFont.pointSizeF();
355 if (d->m_selectedSize == -1) {
356 d->m_selectedSize = QFontInfo(aFont).pointSizeF();
357 }
358
359 if (onlyFixed != d->m_usingFixed) {
360 d->m_usingFixed = onlyFixed;
361 d->setFamilyBoxItems();
362 }
363 d->setupDisplay();
364}
365
366KFontChooser::FontDiffFlags KFontChooser::fontDiffFlags() const
367{
368 FontDiffFlags diffFlags = NoFontDiffFlags;
369
370 if (d->m_ui->familyCheckBox->isChecked()) {
371 diffFlags |= FontDiffFamily;
372 }
373
374 if (d->m_ui->styleCheckBox->isChecked()) {
375 diffFlags |= FontDiffStyle;
376 }
377
378 if (d->m_ui->sizeCheckBox->isChecked()) {
379 diffFlags |= FontDiffSize;
380 }
381
382 return diffFlags;
383}
384
385QFont KFontChooser::font() const
386{
387 return d->m_selectedFont;
388}
389
390static bool isDefaultFontStyleName(const QString &style)
391{
392 /* clang-format off */
393 // Ordered by commonness, i.e. "Regular" is the most common
394 return style == QLatin1String("Regular")
395 || style == QLatin1String("Normal")
396 || style == QLatin1String("Book")
397 || style == QLatin1String("Roman");
398 /* clang-format on */
399}
400
401void KFontChooserPrivate::slotFamilySelected(const QString &family)
402{
403 if (!m_signalsAllowed) {
404 return;
405 }
406 m_signalsAllowed = false;
407
408 QString currentFamily;
409 if (family.isEmpty()) {
410 Q_ASSERT(m_ui->familyListWidget->currentItem());
411 if (m_ui->familyListWidget->currentItem()) {
412 currentFamily = m_qtFamilies[m_ui->familyListWidget->currentItem()->text()];
413 }
414 } else {
415 currentFamily = m_qtFamilies[family];
416 }
417
418 // Get the list of styles available in this family.
419 QStringList styles = QFontDatabase::styles(family: currentFamily);
420 if (styles.isEmpty()) {
421 // Avoid extraction, it is in kdeqt.po
422 styles.append(TR_NOX(sourceText: "Normal", disambiguation: "QFontDatabase"));
423 }
424
425 // Always prepend Regular, Normal, Book or Roman, this way if "m_selectedStyle"
426 // in the code below is empty, selecting index 0 should work better
427 std::sort(first: styles.begin(), last: styles.end(), comp: [](const QString &a, const QString &b) {
428 if (isDefaultFontStyleName(style: a)) {
429 return true;
430 } else if (isDefaultFontStyleName(style: b)) {
431 return false;
432 }
433 return false;
434 });
435
436 // Filter style strings and add to the listbox.
437 QString pureFamily;
438 splitFontString(name: family, family: &pureFamily);
439 QStringList filteredStyles;
440 m_qtStyles.clear();
441 m_styleIDs.clear();
442
443 const QStringList origStyles = styles;
444 for (const QString &style : origStyles) {
445 // Sometimes the font database will report an invalid style,
446 // that falls back back to another when set.
447 // Remove such styles, by checking set/get round-trip.
448 QFont testFont = QFontDatabase::font(family: currentFamily, style, pointSize: 10);
449 if (QFontDatabase::styleString(font: testFont) != style) {
450 styles.removeAll(t: style);
451 continue;
452 }
453
454 QString fstyle = tr(sourceText: "%1", disambiguation: "@item Font style").arg(a: style);
455 if (!filteredStyles.contains(str: fstyle)) {
456 filteredStyles.append(t: fstyle);
457 m_qtStyles.insert(x: {fstyle, style});
458 m_styleIDs.insert(x: {fstyle, styleIdentifier(font: testFont)});
459 }
460 }
461 m_ui->styleListWidget->clear();
462 m_ui->styleListWidget->addItems(labels: filteredStyles);
463
464 // Try to set the current style in the listbox to that previous.
465 int listPos = filteredStyles.indexOf(str: m_selectedStyle.isEmpty() ? TR_NOX(sourceText: "Normal", disambiguation: "QFontDatabase") : m_selectedStyle);
466 if (listPos < 0) {
467 // Make extra effort to have Italic selected when Oblique was chosen,
468 // and vice versa, as that is what the user would probably want.
469 QString styleIt = tr(sourceText: "Italic", disambiguation: "@item font");
470 QString styleOb = tr(sourceText: "Oblique", disambiguation: "@item font");
471 for (int i = 0; i < 2; ++i) {
472 int pos = m_selectedStyle.indexOf(s: styleIt);
473 if (pos >= 0) {
474 QString style = m_selectedStyle;
475 style.replace(i: pos, len: styleIt.length(), after: styleOb);
476 listPos = filteredStyles.indexOf(str: style);
477 if (listPos >= 0) {
478 break;
479 }
480 }
481 qSwap(value1&: styleIt, value2&: styleOb);
482 }
483 }
484 m_ui->styleListWidget->setCurrentRow(listPos >= 0 ? listPos : 0);
485 const QString currentStyle = m_qtStyles[m_ui->styleListWidget->currentItem()->text()];
486
487 // Recompute the size listbox for this family/style.
488 qreal currentSize = setupSizeListBox(family: currentFamily, style: currentStyle);
489 m_ui->sizeSpinBox->setValue(currentSize);
490
491 m_selectedFont = QFontDatabase::font(family: currentFamily, style: currentStyle, pointSize: static_cast<int>(currentSize));
492 if (QFontDatabase::isSmoothlyScalable(family: currentFamily, style: currentStyle) && m_selectedFont.pointSize() == floor(x: currentSize)) {
493 m_selectedFont.setPointSizeF(currentSize);
494 }
495 Q_EMIT q->fontSelected(font: m_selectedFont);
496
497 m_signalsAllowed = true;
498}
499
500void KFontChooserPrivate::slotStyleSelected(const QString &style)
501{
502 if (!m_signalsAllowed) {
503 return;
504 }
505 m_signalsAllowed = false;
506
507 const QString currentFamily = m_qtFamilies[m_ui->familyListWidget->currentItem()->text()];
508 const QString currentStyle = !style.isEmpty() ? m_qtStyles[style] : m_qtStyles[m_ui->styleListWidget->currentItem()->text()];
509
510 // Recompute the size listbox for this family/style.
511 qreal currentSize = setupSizeListBox(family: currentFamily, style: currentStyle);
512 m_ui->sizeSpinBox->setValue(currentSize);
513
514 m_selectedFont = QFontDatabase::font(family: currentFamily, style: currentStyle, pointSize: static_cast<int>(currentSize));
515 if (QFontDatabase::isSmoothlyScalable(family: currentFamily, style: currentStyle) && m_selectedFont.pointSize() == floor(x: currentSize)) {
516 m_selectedFont.setPointSizeF(currentSize);
517 }
518 Q_EMIT q->fontSelected(font: m_selectedFont);
519
520 if (!style.isEmpty()) {
521 m_selectedStyle = currentStyle;
522 }
523
524 m_signalsAllowed = true;
525}
526
527void KFontChooserPrivate::slotSizeSelected(const QString &size)
528{
529 if (!m_signalsAllowed) {
530 return;
531 }
532
533 m_signalsAllowed = false;
534
535 qreal currentSize = 0.0;
536 if (size.isEmpty()) {
537 currentSize = QLocale::system().toDouble(s: m_ui->sizeListWidget->currentItem()->text());
538 } else {
539 currentSize = QLocale::system().toDouble(s: size);
540 }
541
542 // Reset the customized size slot in the list if not needed.
543 if (m_customSizeRow >= 0 && m_selectedFont.pointSizeF() != currentSize) {
544 m_ui->sizeListWidget->item(row: m_customSizeRow)->setText(m_standardSizeAtCustom);
545 m_customSizeRow = -1;
546 }
547
548 m_ui->sizeSpinBox->setValue(currentSize);
549 m_selectedFont.setPointSizeF(currentSize);
550 Q_EMIT q->fontSelected(font: m_selectedFont);
551
552 if (!size.isEmpty()) {
553 m_selectedSize = currentSize;
554 }
555
556 m_signalsAllowed = true;
557}
558
559void KFontChooserPrivate::slotSizeValue(double dval)
560{
561 if (!m_signalsAllowed) {
562 return;
563 }
564 m_signalsAllowed = false;
565
566 // We compare with qreal, so convert for platforms where qreal != double.
567 qreal val = qreal(dval);
568
569 // Reset current size slot in list if it was customized.
570 if (m_customSizeRow >= 0 && m_ui->sizeListWidget->currentRow() == m_customSizeRow) {
571 m_ui->sizeListWidget->item(row: m_customSizeRow)->setText(m_standardSizeAtCustom);
572 m_customSizeRow = -1;
573 }
574
575 bool canCustomize = true;
576
577 const QString family = m_qtFamilies[m_ui->familyListWidget->currentItem()->text()];
578 const QString style = m_qtStyles[m_ui->styleListWidget->currentItem()->text()];
579
580 // For Qt-bad-sizes workaround: skip this block unconditionally
581 if (!QFontDatabase::isSmoothlyScalable(family, style)) {
582 // Bitmap font, allow only discrete sizes.
583 // Determine the nearest in the direction of change.
584 canCustomize = false;
585 int nrows = m_ui->sizeListWidget->count();
586 int row = m_ui->sizeListWidget->currentRow();
587 int nrow;
588 if (val - m_selectedFont.pointSizeF() > 0) {
589 for (nrow = row + 1; nrow < nrows; ++nrow) {
590 if (QLocale::system().toDouble(s: m_ui->sizeListWidget->item(row: nrow)->text()) >= val) {
591 break;
592 }
593 }
594 } else {
595 for (nrow = row - 1; nrow >= 0; --nrow) {
596 if (QLocale::system().toDouble(s: m_ui->sizeListWidget->item(row: nrow)->text()) <= val) {
597 break;
598 }
599 }
600 }
601 // Make sure the new row is not out of bounds.
602 nrow = nrow < 0 ? 0 : nrow >= nrows ? nrows - 1 : nrow;
603 // Get the size from the new row and set the spinbox to that size.
604 val = QLocale::system().toDouble(s: m_ui->sizeListWidget->item(row: nrow)->text());
605 m_ui->sizeSpinBox->setValue(val);
606 }
607
608 // Set the current size in the size listbox.
609 int row = nearestSizeRow(val, customize: canCustomize);
610 m_ui->sizeListWidget->setCurrentRow(row);
611
612 m_selectedSize = val;
613 m_selectedFont.setPointSizeF(val);
614 Q_EMIT q->fontSelected(font: m_selectedFont);
615
616 m_signalsAllowed = true;
617}
618
619void KFontChooserPrivate::slotFeaturesChanged(const QString &features)
620{
621 m_selectedFont.clearFeatures();
622
623 if (features.isEmpty()) {
624 return;
625 }
626
627 const QStringList rawFeaturesList = features.split(sep: QLatin1Char(','), behavior: Qt::SkipEmptyParts);
628 for (const QString &feature : rawFeaturesList) {
629 auto f = QStringView(feature).trimmed();
630 if (f.isEmpty()) {
631 continue;
632 }
633 QList<QStringView> parts = f.split(QStringLiteral("="), behavior: Qt::SkipEmptyParts);
634 if (parts.length() == 2) {
635 const auto tag = QFont::Tag::fromString(view: parts[0]);
636 bool ok = false;
637 const int number = parts[1].toInt(ok: &ok);
638 if (tag.has_value() && ok) {
639 m_selectedFont.setFeature(tag: tag.value(), value: number);
640 }
641 } else if (f.size() <= 4) {
642 const auto tag = QFont::Tag::fromString(view: feature);
643 if (tag.has_value()) {
644 m_selectedFont.setFeature(tag: tag.value(), value: 1);
645 }
646 }
647 }
648
649 Q_EMIT q->fontSelected(font: m_selectedFont);
650}
651
652void KFontChooserPrivate::displaySample(const QFont &font)
653{
654 m_ui->sampleTextEdit->setFont(font);
655}
656
657int KFontChooserPrivate::nearestSizeRow(qreal val, bool customize)
658{
659 qreal diff = 1000;
660 int row = 0;
661 for (int r = 0; r < m_ui->sizeListWidget->count(); ++r) {
662 qreal cval = QLocale::system().toDouble(s: m_ui->sizeListWidget->item(row: r)->text());
663 if (qAbs(t: cval - val) < diff) {
664 diff = qAbs(t: cval - val);
665 row = r;
666 }
667 }
668 // For Qt-bad-sizes workaround: ignore value of customize, use true
669 if (customize && diff > 0) {
670 m_customSizeRow = row;
671 m_standardSizeAtCustom = m_ui->sizeListWidget->item(row)->text();
672 m_ui->sizeListWidget->item(row)->setText(formatFontSize(size: val));
673 }
674 return row;
675}
676
677qreal KFontChooserPrivate::fillSizeList(const QList<qreal> &sizes_)
678{
679 if (m_ui->sizeListWidget->isHidden()) {
680 qCWarning(KWidgetsAddonsLog) << "Trying to fill the font size listwidget, but the widget is hidden.";
681 return 0.0;
682 }
683
684 QList<qreal> sizes = sizes_;
685 bool canCustomize = false;
686 if (sizes.isEmpty()) {
687 static const int c[] = {4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 22, 24, 26, 28, 32, 48, 64, 72, 80, 96, 128, 0};
688 for (int i = 0; c[i]; ++i) {
689 sizes.append(t: c[i]);
690 }
691 // Since sizes were not supplied, this is a vector font,
692 // and size slot customization is allowed.
693 canCustomize = true;
694 }
695
696 // Insert sizes into the listbox.
697 m_ui->sizeListWidget->clear();
698 std::sort(first: sizes.begin(), last: sizes.end());
699 for (qreal size : std::as_const(t&: sizes)) {
700 m_ui->sizeListWidget->addItem(label: formatFontSize(size));
701 }
702
703 // Return the nearest to selected size.
704 // If the font is vector, the nearest size is always same as selected,
705 // thus size slot customization is allowed.
706 // If the font is bitmap, the nearest size need not be same as selected,
707 // thus size slot customization is not allowed.
708 m_customSizeRow = -1;
709 int row = nearestSizeRow(val: m_selectedSize, customize: canCustomize);
710 return QLocale::system().toDouble(s: m_ui->sizeListWidget->item(row)->text());
711}
712
713qreal KFontChooserPrivate::setupSizeListBox(const QString &family, const QString &style)
714{
715 QList<qreal> sizes;
716 const bool smoothlyScalable = QFontDatabase::isSmoothlyScalable(family, style);
717 if (!smoothlyScalable) {
718 const QList<int> smoothSizes = QFontDatabase::smoothSizes(family, style);
719 for (int size : smoothSizes) {
720 sizes.append(t: size);
721 }
722 }
723
724 // Fill the listbox (uses default list of sizes if the given is empty).
725 // Collect the best fitting size to selected size, to use if not smooth.
726 qreal bestFitSize = fillSizeList(sizes_: sizes);
727
728 // Set the best fit size as current in the listbox if available.
729 const QList<QListWidgetItem *> selectedSizeList = m_ui->sizeListWidget->findItems(text: formatFontSize(size: bestFitSize), flags: Qt::MatchExactly);
730 if (!selectedSizeList.isEmpty()) {
731 m_ui->sizeListWidget->setCurrentItem(selectedSizeList.first());
732 }
733
734 return bestFitSize;
735}
736
737void KFontChooserPrivate::setupDisplay()
738{
739 qreal size = m_selectedFont.pointSizeF();
740 if (size == -1) {
741 size = QFontInfo(m_selectedFont).pointSizeF();
742 }
743
744 // Set font features
745 const auto tags = m_selectedFont.featureTags();
746 QStringList features;
747 for (const auto &tag : tags) {
748 const QString name = QString::fromUtf8(ba: tag.toString());
749 const quint32 value = m_selectedFont.featureValue(tag);
750 if (value == 1) {
751 features.push_back(t: name);
752 } else {
753 features.push_back(QStringLiteral("%1=%2").arg(args: name, args: QString::number(value)));
754 }
755 }
756 m_ui->fontFeaturesLineEdit->setText(features.join(QStringLiteral(",")));
757
758 int numEntries;
759 int i;
760
761 // Get the styleID here before familyListWidget->setCurrentRow() is called
762 // as it may change the font style
763 const QString styleID = styleIdentifier(font: m_selectedFont);
764
765 QString family = m_selectedFont.family().toLower();
766 // Direct family match.
767 numEntries = m_ui->familyListWidget->count();
768 for (i = 0; i < numEntries; ++i) {
769 if (family == m_qtFamilies[m_ui->familyListWidget->item(row: i)->text()].toLower()) {
770 m_ui->familyListWidget->setCurrentRow(i);
771 break;
772 }
773 }
774
775 // 1st family fallback.
776 if (i == numEntries) {
777 const int bracketPos = family.indexOf(ch: QLatin1Char('['));
778 if (bracketPos != -1) {
779 family = QStringView(family).left(n: bracketPos).trimmed().toString();
780 for (i = 0; i < numEntries; ++i) {
781 if (family == m_qtFamilies[m_ui->familyListWidget->item(row: i)->text()].toLower()) {
782 m_ui->familyListWidget->setCurrentRow(i);
783 break;
784 }
785 }
786 }
787 }
788
789 // 2nd family fallback.
790 if (i == numEntries) {
791 QString fallback = family + QLatin1String(" [");
792 for (i = 0; i < numEntries; ++i) {
793 if (m_qtFamilies[m_ui->familyListWidget->item(row: i)->text()].toLower().startsWith(s: fallback)) {
794 m_ui->familyListWidget->setCurrentRow(i);
795 break;
796 }
797 }
798 }
799
800 // 3rd family fallback.
801 if (i == numEntries) {
802 for (i = 0; i < numEntries; ++i) {
803 if (m_qtFamilies[m_ui->familyListWidget->item(row: i)->text()].toLower().startsWith(s: family)) {
804 m_ui->familyListWidget->setCurrentRow(i);
805 break;
806 }
807 }
808 }
809
810 // Family fallback in case nothing matched. Otherwise, diff doesn't work
811 if (i == numEntries) {
812 m_ui->familyListWidget->setCurrentRow(0);
813 }
814
815 // By setting the current item in the family box, the available
816 // styles and sizes for that family have been collected.
817 // Try now to set the current items in the style and size boxes.
818
819 // Set current style in the listbox.
820 numEntries = m_ui->styleListWidget->count();
821 for (i = 0; i < numEntries; ++i) {
822 if (styleID == m_styleIDs[m_ui->styleListWidget->item(row: i)->text()]) {
823 m_ui->styleListWidget->setCurrentRow(i);
824 break;
825 }
826 }
827 if (i == numEntries) {
828 // Style not found, fallback.
829 m_ui->styleListWidget->setCurrentRow(0);
830 }
831
832 // Set current size in the listbox.
833 // If smoothly scalable, allow customizing one of the standard size slots,
834 // otherwise just select the nearest available size.
835 const QString currentFamily = m_qtFamilies[m_ui->familyListWidget->currentItem()->text()];
836 const QString currentStyle = m_qtStyles[m_ui->styleListWidget->currentItem()->text()];
837 const bool canCustomize = QFontDatabase::isSmoothlyScalable(family: currentFamily, style: currentStyle);
838 m_ui->sizeListWidget->setCurrentRow(nearestSizeRow(val: size, customize: canCustomize));
839
840 // Set current size in the spinbox.
841 m_ui->sizeSpinBox->setValue(QLocale::system().toDouble(s: m_ui->sizeListWidget->currentItem()->text()));
842}
843
844// static
845QStringList KFontChooser::createFontList(uint fontListCriteria)
846{
847 QStringList lstSys(QFontDatabase::families());
848
849 // if we have criteria; then check fonts before adding
850 if (fontListCriteria) {
851 QStringList lstFonts;
852 for (const QString &family : std::as_const(t&: lstSys)) {
853 if ((fontListCriteria & FixedWidthFonts) > 0 && !QFontDatabase::isFixedPitch(family)) {
854 continue;
855 }
856 if (((fontListCriteria & (SmoothScalableFonts | ScalableFonts)) == ScalableFonts) && !QFontDatabase::isBitmapScalable(family)) {
857 continue;
858 }
859 if ((fontListCriteria & SmoothScalableFonts) > 0 && !QFontDatabase::isSmoothlyScalable(family)) {
860 continue;
861 }
862 lstFonts.append(t: family);
863 }
864
865 if ((fontListCriteria & FixedWidthFonts) > 0) {
866 // Fallback.. if there are no fixed fonts found, it's probably a
867 // bug in the font server or Qt. In this case, just use 'fixed'
868 if (lstFonts.isEmpty()) {
869 lstFonts.append(QStringLiteral("fixed"));
870 }
871 }
872
873 lstSys = lstFonts;
874 }
875
876 lstSys.sort();
877
878 return lstSys;
879}
880
881void KFontChooser::setFontListItems(const QStringList &fontList)
882{
883 d->setFamilyBoxItems(fontList);
884}
885
886void KFontChooserPrivate::setFamilyBoxItems(const QStringList &fonts)
887{
888 m_signalsAllowed = false;
889
890 m_ui->familyListWidget->clear();
891
892 m_qtFamilies = translateFontNameList(names: !fonts.isEmpty() ? fonts : KFontChooser::createFontList(fontListCriteria: m_usingFixed ? KFontChooser::FixedWidthFonts : 0));
893
894 QStringList list;
895 list.reserve(asize: m_qtFamilies.size());
896
897 // Generic font names
898 const QStringList genericTranslatedNames{
899 translateFontName(QStringLiteral("Sans Serif")),
900 translateFontName(QStringLiteral("Serif")),
901 translateFontName(QStringLiteral("Monospace")),
902 };
903
904 // Add generic family names to the top of the list
905 for (const QString &s : genericTranslatedNames) {
906 auto nIt = m_qtFamilies.find(x: s);
907 if (nIt != m_qtFamilies.cend()) {
908 list.push_back(t: s);
909 }
910 }
911
912 for (auto it = m_qtFamilies.cbegin(); it != m_qtFamilies.cend(); ++it) {
913 const QString &name = it->first;
914 if (genericTranslatedNames.contains(str: name)) {
915 continue;
916 }
917
918 list.push_back(t: name);
919 }
920
921 m_ui->familyListWidget->addItems(labels: list);
922 m_ui->familyListWidget->setMinimumWidth(minimumListWidth(list: m_ui->familyListWidget));
923
924 m_signalsAllowed = true;
925}
926
927void KFontChooser::setMinVisibleItems(int visibleItems)
928{
929 for (auto *widget : {d->m_ui->familyListWidget, d->m_ui->styleListWidget, d->m_ui->sizeListWidget}) {
930 widget->setMinimumHeight(minimumListHeight(list: widget, numVisibleEntry: visibleItems));
931 }
932}
933
934// Human-readable style identifiers returned by QFontDatabase::styleString()
935// do not always survive round trip of QFont serialization/deserialization,
936// causing wrong style in the style box to be highlighted when
937// the chooser dialog is opened. This will cause the style to be changed
938// when the dialog is closed and the user did not touch the style box.
939// Hence, construct custom style identifiers sufficient for the purpose.
940QString KFontChooserPrivate::styleIdentifier(const QFont &font)
941{
942 const int weight = font.weight();
943 QString styleName = font.styleName();
944 // If the styleName property is empty and the weight is QFont::Normal, that
945 // could mean it's a "Regular"-like style with the styleName part stripped
946 // so that subsequent calls to setBold(true) can work properly (i.e. selecting
947 // the "Bold" style provided by the font itself) without resorting to font
948 // "emboldening" which looks ugly.
949 // See also KConfigGroupGui::writeEntryGui().
950 if (styleName.isEmpty() && weight == QFont::Normal) {
951 const QStringList styles = QFontDatabase::styles(family: font.family());
952 for (const QString &style : styles) {
953 if (isDefaultFontStyleName(style)) {
954 styleName = style;
955 break;
956 } else {
957 // nothing more we can do
958 }
959 }
960 }
961
962 const QChar comma(QLatin1Char(','));
963 return QString::number(weight) + comma //
964 + QString::number((int)font.style()) + comma //
965 + QString::number(font.stretch()) + comma //
966 + styleName;
967}
968
969#include "moc_kfontchooser.cpp"
970

source code of kwidgetsaddons/src/kfontchooser.cpp