1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qfontcombobox.h"
5
6#include <qaccessible.h>
7#include <qstringlistmodel.h>
8#include <qitemdelegate.h>
9#include <qlistview.h>
10#include <qpainter.h>
11#include <qevent.h>
12#include <qapplication.h>
13#include <private/qcombobox_p.h>
14#include <qdebug.h>
15
16QT_BEGIN_NAMESPACE
17
18using namespace Qt::StringLiterals;
19
20static QFontDatabase::WritingSystem writingSystemFromScript(QLocale::Script script)
21{
22 switch (script) {
23 case QLocale::ArabicScript:
24 return QFontDatabase::Arabic;
25 case QLocale::CyrillicScript:
26 return QFontDatabase::Cyrillic;
27 case QLocale::GurmukhiScript:
28 return QFontDatabase::Gurmukhi;
29 case QLocale::SimplifiedHanScript:
30 return QFontDatabase::SimplifiedChinese;
31 case QLocale::TraditionalHanScript:
32 return QFontDatabase::TraditionalChinese;
33 case QLocale::LatinScript:
34 return QFontDatabase::Latin;
35 case QLocale::ArmenianScript:
36 return QFontDatabase::Armenian;
37 case QLocale::BengaliScript:
38 return QFontDatabase::Bengali;
39 case QLocale::DevanagariScript:
40 return QFontDatabase::Devanagari;
41 case QLocale::GeorgianScript:
42 return QFontDatabase::Georgian;
43 case QLocale::GreekScript:
44 return QFontDatabase::Greek;
45 case QLocale::GujaratiScript:
46 return QFontDatabase::Gujarati;
47 case QLocale::HebrewScript:
48 return QFontDatabase::Hebrew;
49 case QLocale::JapaneseScript:
50 return QFontDatabase::Japanese;
51 case QLocale::KhmerScript:
52 return QFontDatabase::Khmer;
53 case QLocale::KannadaScript:
54 return QFontDatabase::Kannada;
55 case QLocale::KoreanScript:
56 return QFontDatabase::Korean;
57 case QLocale::LaoScript:
58 return QFontDatabase::Lao;
59 case QLocale::MalayalamScript:
60 return QFontDatabase::Malayalam;
61 case QLocale::MyanmarScript:
62 return QFontDatabase::Myanmar;
63 case QLocale::TamilScript:
64 return QFontDatabase::Tamil;
65 case QLocale::TeluguScript:
66 return QFontDatabase::Telugu;
67 case QLocale::ThaanaScript:
68 return QFontDatabase::Thaana;
69 case QLocale::ThaiScript:
70 return QFontDatabase::Thai;
71 case QLocale::TibetanScript:
72 return QFontDatabase::Tibetan;
73 case QLocale::SinhalaScript:
74 return QFontDatabase::Sinhala;
75 case QLocale::SyriacScript:
76 return QFontDatabase::Syriac;
77 case QLocale::OriyaScript:
78 return QFontDatabase::Oriya;
79 case QLocale::OghamScript:
80 return QFontDatabase::Ogham;
81 case QLocale::RunicScript:
82 return QFontDatabase::Runic;
83 case QLocale::NkoScript:
84 return QFontDatabase::Nko;
85 default:
86 return QFontDatabase::Any;
87 }
88}
89
90static QFontDatabase::WritingSystem writingSystemFromLocale()
91{
92 QStringList uiLanguages = QLocale::system().uiLanguages();
93 QLocale::Script script;
94 if (!uiLanguages.isEmpty())
95 script = QLocale(uiLanguages.at(i: 0)).script();
96 else
97 script = QLocale::system().script();
98
99 return writingSystemFromScript(script);
100}
101
102static QFontDatabase::WritingSystem writingSystemForFont(const QFont &font, bool *hasLatin)
103{
104 QList<QFontDatabase::WritingSystem> writingSystems = QFontDatabase::writingSystems(family: font.families().first());
105// qDebug() << font.families().first() << writingSystems;
106
107 // this just confuses the algorithm below. Vietnamese is Latin with lots of special chars
108 writingSystems.removeOne(t: QFontDatabase::Vietnamese);
109 *hasLatin = writingSystems.removeOne(t: QFontDatabase::Latin);
110
111 if (writingSystems.isEmpty())
112 return QFontDatabase::Any;
113
114 QFontDatabase::WritingSystem system = writingSystemFromLocale();
115
116 if (writingSystems.contains(t: system))
117 return system;
118
119 if (system == QFontDatabase::TraditionalChinese
120 && writingSystems.contains(t: QFontDatabase::SimplifiedChinese)) {
121 return QFontDatabase::SimplifiedChinese;
122 }
123
124 if (system == QFontDatabase::SimplifiedChinese
125 && writingSystems.contains(t: QFontDatabase::TraditionalChinese)) {
126 return QFontDatabase::TraditionalChinese;
127 }
128
129 system = writingSystems.constLast();
130
131 if (!*hasLatin) {
132 // we need to show something
133 return system;
134 }
135
136 if (writingSystems.size() == 1 && system > QFontDatabase::Cyrillic)
137 return system;
138
139 if (writingSystems.size() <= 2 && system > QFontDatabase::Armenian && system < QFontDatabase::Vietnamese)
140 return system;
141
142 if (writingSystems.size() <= 5 && system >= QFontDatabase::SimplifiedChinese && system <= QFontDatabase::Korean)
143 return system;
144
145 return QFontDatabase::Any;
146}
147
148class QFontComboBoxPrivate : public QComboBoxPrivate
149{
150public:
151 inline QFontComboBoxPrivate() { filters = QFontComboBox::AllFonts; }
152
153 QFontComboBox::FontFilters filters;
154 QFont currentFont;
155 QHash<QFontDatabase::WritingSystem, QString> sampleTextForWritingSystem;
156 QHash<QString, QString> sampleTextForFontFamily;
157 QHash<QString, QFont> displayFontForFontFamily;
158
159 void _q_updateModel();
160 void _q_currentChanged(const QString &);
161
162 Q_DECLARE_PUBLIC(QFontComboBox)
163};
164
165class QFontFamilyDelegate : public QAbstractItemDelegate
166{
167 Q_OBJECT
168public:
169 explicit QFontFamilyDelegate(QObject *parent, QFontComboBoxPrivate *comboP);
170
171 // painting
172 void paint(QPainter *painter,
173 const QStyleOptionViewItem &option,
174 const QModelIndex &index) const override;
175
176 QSize sizeHint(const QStyleOptionViewItem &option,
177 const QModelIndex &index) const override;
178
179 const QIcon truetype;
180 const QIcon bitmap;
181 QFontDatabase::WritingSystem writingSystem;
182 QFontComboBoxPrivate *comboPrivate;
183};
184
185QFontFamilyDelegate::QFontFamilyDelegate(QObject *parent, QFontComboBoxPrivate *comboP)
186 : QAbstractItemDelegate(parent),
187 truetype(QStringLiteral(":/qt-project.org/styles/commonstyle/images/fonttruetype-16.png")),
188 bitmap(QStringLiteral(":/qt-project.org/styles/commonstyle/images/fontbitmap-16.png")),
189 writingSystem(QFontDatabase::Any),
190 comboPrivate(comboP)
191{
192}
193
194void QFontFamilyDelegate::paint(QPainter *painter,
195 const QStyleOptionViewItem &option,
196 const QModelIndex &index) const
197{
198 QString text = index.data(arole: Qt::DisplayRole).toString();
199 QFont font(option.font);
200 font.setPointSize(QFontInfo(font).pointSize() * 3 / 2);
201 QFont font2 = font;
202 font2.setFamilies(QStringList{text});
203
204 bool hasLatin;
205 QFontDatabase::WritingSystem system = writingSystemForFont(font: font2, hasLatin: &hasLatin);
206 if (hasLatin)
207 font = font2;
208
209 font = comboPrivate->displayFontForFontFamily.value(key: text, defaultValue: font);
210
211 QRect r = option.rect;
212
213 if (option.state & QStyle::State_Selected) {
214 painter->save();
215 painter->setBrush(option.palette.highlight());
216 painter->setPen(Qt::NoPen);
217 painter->drawRect(r: option.rect);
218 painter->setPen(QPen(option.palette.highlightedText(), 0));
219 }
220
221 const QIcon *icon = &bitmap;
222 if (QFontDatabase::isSmoothlyScalable(family: text)) {
223 icon = &truetype;
224 }
225 const QSize actualSize = icon->actualSize(size: r.size());
226 const QRect iconRect = QStyle::alignedRect(direction: option.direction, alignment: option.displayAlignment,
227 size: actualSize, rectangle: r);
228 icon->paint(painter, rect: iconRect, alignment: Qt::AlignLeft|Qt::AlignVCenter);
229 if (option.direction == Qt::RightToLeft)
230 r.setRight(r.right() - actualSize.width() - 4);
231 else
232 r.setLeft(r.left() + actualSize.width() + 4);
233
234 QFont old = painter->font();
235 painter->setFont(font);
236
237 const Qt::Alignment textAlign = QStyle::visualAlignment(direction: option.direction, alignment: option.displayAlignment);
238 // If the ascent of the font is larger than the height of the rect,
239 // we will clip the text, so it's better to align the tight bounding rect in this case
240 // This is specifically for fonts where the ascent is very large compared to
241 // the descent, like certain of the Stix family.
242 QFontMetricsF fontMetrics(font);
243 if (fontMetrics.ascent() > r.height()) {
244 QRectF tbr = fontMetrics.tightBoundingRect(text);
245 QRect textRect(r);
246 textRect.setHeight(textRect.height() + (r.height() - tbr.height()));
247 painter->drawText(r: textRect, flags: Qt::AlignBottom|Qt::TextSingleLine|textAlign, text);
248 } else {
249 painter->drawText(r, flags: Qt::AlignVCenter|Qt::TextSingleLine|textAlign, text);
250 }
251
252 if (writingSystem != QFontDatabase::Any)
253 system = writingSystem;
254
255 const QString sampleText = comboPrivate->sampleTextForFontFamily.value(key: text, defaultValue: comboPrivate->sampleTextForWritingSystem.value(key: system));
256 if (system != QFontDatabase::Any || !sampleText.isEmpty()) {
257 int w = painter->fontMetrics().horizontalAdvance(text + " "_L1);
258 painter->setFont(font2);
259 const QString sample = !sampleText.isEmpty() ? sampleText : QFontDatabase::writingSystemSample(writingSystem: system);
260 if (option.direction == Qt::RightToLeft)
261 r.setRight(r.right() - w);
262 else
263 r.setLeft(r.left() + w);
264 painter->drawText(r, flags: Qt::AlignVCenter|Qt::TextSingleLine|textAlign, text: sample);
265 }
266 painter->setFont(old);
267
268 if (option.state & QStyle::State_Selected)
269 painter->restore();
270
271}
272
273QSize QFontFamilyDelegate::sizeHint(const QStyleOptionViewItem &option,
274 const QModelIndex &index) const
275{
276 QString text = index.data(arole: Qt::DisplayRole).toString();
277 QFont font(option.font);
278// font.setFamilies(QStringList{text});
279 font.setPointSize(QFontInfo(font).pointSize() * 3/2);
280 QFontMetrics fontMetrics(font);
281 return QSize(fontMetrics.horizontalAdvance(text), fontMetrics.height());
282}
283
284
285void QFontComboBoxPrivate::_q_updateModel()
286{
287 Q_Q(QFontComboBox);
288
289 if (QCoreApplication::closingDown())
290 return;
291
292 const int scalableMask = (QFontComboBox::ScalableFonts | QFontComboBox::NonScalableFonts);
293 const int spacingMask = (QFontComboBox::ProportionalFonts | QFontComboBox::MonospacedFonts);
294
295 QStringListModel *m = qobject_cast<QStringListModel *>(object: q->model());
296 if (!m)
297 return;
298 QFontFamilyDelegate *delegate = qobject_cast<QFontFamilyDelegate *>(object: q->view()->itemDelegate());
299 QFontDatabase::WritingSystem system = delegate ? delegate->writingSystem : QFontDatabase::Any;
300
301 QStringList list = QFontDatabase::families(writingSystem: system);
302 QStringList result;
303
304 int offset = 0;
305 QFontInfo fi(currentFont);
306
307 for (int i = 0; i < list.size(); ++i) {
308 if (QFontDatabase::isPrivateFamily(family: list.at(i)))
309 continue;
310
311 if ((filters & scalableMask) && (filters & scalableMask) != scalableMask) {
312 if (bool(filters & QFontComboBox::ScalableFonts) != QFontDatabase::isSmoothlyScalable(family: list.at(i)))
313 continue;
314 }
315 if ((filters & spacingMask) && (filters & spacingMask) != spacingMask) {
316 if (bool(filters & QFontComboBox::MonospacedFonts) != QFontDatabase::isFixedPitch(family: list.at(i)))
317 continue;
318 }
319 result += list.at(i);
320 if (list.at(i) == fi.family() || list.at(i).startsWith(s: fi.family() + " ["_L1))
321 offset = result.size() - 1;
322 }
323 list = result;
324
325 //we need to block the signals so that the model doesn't emit reset
326 //this prevents the current index from changing
327 //it will be updated just after this
328 ///TODO: we should finda way to avoid blocking signals and have a real update of the model
329 {
330 const QSignalBlocker blocker(m);
331 m->setStringList(list);
332 // Since the modelReset signal is blocked the view will not emit an accessibility event
333 #if QT_CONFIG(accessibility)
334 if (QAccessible::isActive()) {
335 QAccessibleTableModelChangeEvent accessibleEvent(q->view(), QAccessibleTableModelChangeEvent::ModelReset);
336 QAccessible::updateAccessibility(event: &accessibleEvent);
337 }
338 #endif
339 }
340
341 if (list.isEmpty()) {
342 if (currentFont != QFont()) {
343 currentFont = QFont();
344 emit q->currentFontChanged(f: currentFont);
345 }
346 } else {
347 q->setCurrentIndex(offset);
348 }
349}
350
351
352void QFontComboBoxPrivate::_q_currentChanged(const QString &text)
353{
354 Q_Q(QFontComboBox);
355 QStringList families = currentFont.families();
356 if (families.isEmpty() || families.first() != text) {
357 currentFont.setFamilies(QStringList{text});
358 emit q->currentFontChanged(f: currentFont);
359 }
360}
361
362/*!
363 \class QFontComboBox
364 \brief The QFontComboBox widget is a combobox that lets the user
365 select a font family.
366
367 \since 4.2
368 \ingroup basicwidgets
369 \inmodule QtWidgets
370
371 The combobox is populated with an alphabetized list of font
372 family names, such as Arial, Helvetica, and Times New Roman.
373 Family names are displayed using the actual font when possible.
374 For fonts such as Symbol, where the name is not representable in
375 the font itself, a sample of the font is displayed next to the
376 family name.
377
378 QFontComboBox is often used in toolbars, in conjunction with a
379 QComboBox for controlling the font size and two \l{QToolButton}s
380 for bold and italic.
381
382 When the user selects a new font, the currentFontChanged() signal
383 is emitted in addition to currentIndexChanged().
384
385 Call setWritingSystem() to tell QFontComboBox to show only fonts
386 that support a given writing system, and setFontFilters() to
387 filter out certain types of fonts as e.g. non scalable fonts or
388 monospaced fonts.
389
390 \image windowsvista-fontcombobox.png Screenshot of QFontComboBox on Windows Vista
391
392 \sa QComboBox, QFont, QFontInfo, QFontMetrics, QFontDatabase
393*/
394
395/*!
396 Constructs a font combobox with the given \a parent.
397*/
398QFontComboBox::QFontComboBox(QWidget *parent)
399 : QComboBox(*new QFontComboBoxPrivate, parent)
400{
401 Q_D(QFontComboBox);
402 d->currentFont = font();
403 setEditable(true);
404
405 QStringListModel *m = new QStringListModel(this);
406 setModel(m);
407 setItemDelegate(new QFontFamilyDelegate(this, d));
408 QListView *lview = qobject_cast<QListView*>(object: view());
409 if (lview)
410 lview->setUniformItemSizes(true);
411 setWritingSystem(QFontDatabase::Any);
412
413 connect(sender: this, SIGNAL(currentTextChanged(QString)),
414 receiver: this, SLOT(_q_currentChanged(QString)));
415
416 connect(qApp, SIGNAL(fontDatabaseChanged()),
417 receiver: this, SLOT(_q_updateModel()));
418}
419
420
421/*!
422 Destroys the combobox.
423*/
424QFontComboBox::~QFontComboBox()
425{
426}
427
428/*!
429 \property QFontComboBox::writingSystem
430 \brief the writing system that serves as a filter for the combobox
431
432 If \a script is QFontDatabase::Any (the default), all fonts are
433 listed.
434
435 \sa fontFilters
436*/
437
438void QFontComboBox::setWritingSystem(QFontDatabase::WritingSystem script)
439{
440 Q_D(QFontComboBox);
441 QFontFamilyDelegate *delegate = qobject_cast<QFontFamilyDelegate *>(object: view()->itemDelegate());
442 if (delegate)
443 delegate->writingSystem = script;
444 d->_q_updateModel();
445}
446
447QFontDatabase::WritingSystem QFontComboBox::writingSystem() const
448{
449 QFontFamilyDelegate *delegate = qobject_cast<QFontFamilyDelegate *>(object: view()->itemDelegate());
450 if (delegate)
451 return delegate->writingSystem;
452 return QFontDatabase::Any;
453}
454
455
456/*!
457 \enum QFontComboBox::FontFilter
458
459 This enum can be used to only show certain types of fonts in the font combo box.
460
461 \value AllFonts Show all fonts
462 \value ScalableFonts Show scalable fonts
463 \value NonScalableFonts Show non scalable fonts
464 \value MonospacedFonts Show monospaced fonts
465 \value ProportionalFonts Show proportional fonts
466*/
467
468/*!
469 \property QFontComboBox::fontFilters
470 \brief the filter for the combobox
471
472 By default, all fonts are listed.
473
474 \sa writingSystem
475*/
476void QFontComboBox::setFontFilters(FontFilters filters)
477{
478 Q_D(QFontComboBox);
479 d->filters = filters;
480 d->_q_updateModel();
481}
482
483QFontComboBox::FontFilters QFontComboBox::fontFilters() const
484{
485 Q_D(const QFontComboBox);
486 return d->filters;
487}
488
489/*!
490 \property QFontComboBox::currentFont
491 \brief the currently selected font
492
493 \sa currentIndex, currentText
494*/
495QFont QFontComboBox::currentFont() const
496{
497 Q_D(const QFontComboBox);
498 return d->currentFont;
499}
500
501void QFontComboBox::setCurrentFont(const QFont &font)
502{
503 Q_D(QFontComboBox);
504 if (font != d->currentFont) {
505 d->currentFont = font;
506 d->_q_updateModel();
507 if (d->currentFont == font) { //else the signal has already be emitted by _q_updateModel
508 emit currentFontChanged(f: d->currentFont);
509 }
510 }
511}
512
513/*!
514 \fn void QFontComboBox::currentFontChanged(const QFont &font)
515
516 This signal is emitted whenever the current font changes, with
517 the new \a font.
518
519 \sa currentFont
520*/
521
522/*!
523 \reimp
524*/
525bool QFontComboBox::event(QEvent *e)
526{
527 if (e->type() == QEvent::Resize) {
528 QListView *lview = qobject_cast<QListView*>(object: view());
529 if (lview) {
530 lview->window()->setFixedWidth(qMin(a: width() * 5 / 3,
531 b: QWidgetPrivate::availableScreenGeometry(widget: lview).width()));
532 }
533 }
534 return QComboBox::event(event: e);
535}
536
537/*!
538 \reimp
539*/
540QSize QFontComboBox::sizeHint() const
541{
542 QSize sz = QComboBox::sizeHint();
543 QFontMetrics fm(font());
544 sz.setWidth(fm.horizontalAdvance(u'm') * 14);
545 return sz;
546}
547
548/*!
549 Sets the \a sampleText to show after the font name (when the combo is open) for a given \a writingSystem.
550
551 The sample text given with setSampleTextForFont() has priority.
552
553 \since 6.3
554*/
555void QFontComboBox::setSampleTextForSystem(QFontDatabase::WritingSystem writingSystem, const QString &sampleText)
556{
557 Q_D(QFontComboBox);
558 d->sampleTextForWritingSystem[writingSystem] = sampleText;
559}
560
561
562/*!
563 Returns the sample text to show after the font name (when the combo is open) for a given \a writingSystem.
564
565 \since 6.3
566*/
567QString QFontComboBox::sampleTextForSystem(QFontDatabase::WritingSystem writingSystem) const
568{
569 Q_D(const QFontComboBox);
570 return d->sampleTextForWritingSystem.value(key: writingSystem);
571}
572
573/*!
574 Sets the \a sampleText to show after the font name (when the combo is open) for a given \a fontFamily.
575
576 The sample text given with this function has priority over the one set with setSampleTextForSystem().
577
578 \since 6.3
579*/
580void QFontComboBox::setSampleTextForFont(const QString &fontFamily, const QString &sampleText)
581{
582 Q_D(QFontComboBox);
583 d->sampleTextForFontFamily[fontFamily] = sampleText;
584}
585
586/*!
587 Returns the sample text to show after the font name (when the combo is open) for a given \a fontFamily.
588
589 \since 6.3
590*/
591QString QFontComboBox::sampleTextForFont(const QString &fontFamily) const
592{
593 Q_D(const QFontComboBox);
594 return d->sampleTextForFontFamily.value(key: fontFamily);
595}
596
597/*!
598 Sets the \a font to be used to display a given \a fontFamily (when the combo is open).
599
600 \since 6.3
601*/
602void QFontComboBox::setDisplayFont(const QString &fontFamily, const QFont &font)
603{
604 Q_D(QFontComboBox);
605 d->displayFontForFontFamily[fontFamily] = font;
606}
607
608/*!
609 Returns the font (if set) to be used to display a given \a fontFamily (when the combo is open).
610
611 \since 6.3
612*/
613std::optional<QFont> QFontComboBox::displayFont(const QString &fontFamily) const
614{
615 Q_D(const QFontComboBox);
616 return d->displayFontForFontFamily.value(key: fontFamily, defaultValue: {});
617}
618
619QT_END_NAMESPACE
620
621#include "qfontcombobox.moc"
622#include "moc_qfontcombobox.cpp"
623

source code of qtbase/src/widgets/widgets/qfontcombobox.cpp