1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the Qt Designer of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:GPL-EXCEPT$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU
19** General Public License version 3 as published by the Free Software
20** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
21** included in the packaging of this file. Please review the following
22** information to ensure the GNU General Public License requirements will
23** be met: https://www.gnu.org/licenses/gpl-3.0.html.
24**
25** $QT_END_LICENSE$
26**
27****************************************************************************/
28
29#include "appfontdialog.h"
30
31#include <iconloader_p.h>
32
33#include <QtDesigner/abstractsettings.h>
34
35#include <QtWidgets/qtreeview.h>
36#include <QtWidgets/qtoolbutton.h>
37#include <QtWidgets/qboxlayout.h>
38#include <QtWidgets/qlayoutitem.h>
39#include <QtWidgets/qfiledialog.h>
40#include <QtGui/qstandarditemmodel.h>
41#include <QtWidgets/qmessagebox.h>
42#include <QtGui/qfontdatabase.h>
43#include <QtWidgets/qdialogbuttonbox.h>
44
45#include <QtCore/qsettings.h>
46#include <QtCore/qcoreapplication.h>
47#include <QtCore/qstringlist.h>
48#include <QtCore/qfileinfo.h>
49#include <QtCore/qalgorithms.h>
50#include <QtCore/qvector.h>
51#include <QtCore/qdebug.h>
52
53#include <algorithm>
54
55QT_BEGIN_NAMESPACE
56
57enum {FileNameRole = Qt::UserRole + 1, IdRole = Qt::UserRole + 2 };
58enum { debugAppFontWidget = 0 };
59
60static const char fontFileKeyC[] = "fontFiles";
61
62// AppFontManager: Singleton that maintains the mapping of loaded application font
63// ids to the file names (which are not stored in QFontDatabase)
64// and provides API for loading/unloading fonts as well for saving/restoring settings.
65
66class AppFontManager
67{
68 Q_DISABLE_COPY_MOVE(AppFontManager)
69 AppFontManager();
70public:
71 static AppFontManager &instance();
72
73 void save(QDesignerSettingsInterface *s, const QString &prefix) const;
74 void restore(const QDesignerSettingsInterface *s, const QString &prefix);
75
76 // Return id or -1
77 int add(const QString &fontFile, QString *errorMessage);
78
79 bool remove(int id, QString *errorMessage);
80 bool remove(const QString &fontFile, QString *errorMessage);
81 bool removeAt(int index, QString *errorMessage);
82
83 // Store loaded fonts as pair of file name and Id
84 using FileNameFontIdPair = QPair<QString,int>;
85 using FileNameFontIdPairs = QVector<FileNameFontIdPair>;
86 const FileNameFontIdPairs &fonts() const;
87
88private:
89 FileNameFontIdPairs m_fonts;
90};
91
92AppFontManager::AppFontManager() = default;
93
94AppFontManager &AppFontManager::instance()
95{
96 static AppFontManager rc;
97 return rc;
98}
99
100void AppFontManager::save(QDesignerSettingsInterface *s, const QString &prefix) const
101{
102 // Store as list of file names
103 QStringList fontFiles;
104 const FileNameFontIdPairs::const_iterator cend = m_fonts.constEnd();
105 for (FileNameFontIdPairs::const_iterator it = m_fonts.constBegin(); it != cend; ++it)
106 fontFiles.push_back(t: it->first);
107
108 s->beginGroup(prefix);
109 s->setValue(key: QLatin1String(fontFileKeyC), value: fontFiles);
110 s->endGroup();
111
112 if (debugAppFontWidget)
113 qDebug() << "AppFontManager::saved" << fontFiles.size() << "fonts under " << prefix;
114}
115
116void AppFontManager::restore(const QDesignerSettingsInterface *s, const QString &prefix)
117{
118 QString key = prefix;
119 key += QLatin1Char('/');
120 key += QLatin1String(fontFileKeyC);
121 const QStringList fontFiles = s->value(key, defaultValue: QStringList()).toStringList();
122
123 if (debugAppFontWidget)
124 qDebug() << "AppFontManager::restoring" << fontFiles.size() << "fonts from " << prefix;
125 if (!fontFiles.isEmpty()) {
126 QString errorMessage;
127 const QStringList::const_iterator cend = fontFiles.constEnd();
128 for (QStringList::const_iterator it = fontFiles.constBegin(); it != cend; ++it)
129 if (add(fontFile: *it, errorMessage: &errorMessage) == -1)
130 qWarning(msg: "%s", qPrintable(errorMessage));
131 }
132}
133
134int AppFontManager::add(const QString &fontFile, QString *errorMessage)
135{
136 const QFileInfo inf(fontFile);
137 if (!inf.isFile()) {
138 *errorMessage = QCoreApplication::translate(context: "AppFontManager", key: "'%1' is not a file.").arg(a: fontFile);
139 return -1;
140 }
141 if (!inf.isReadable()) {
142 *errorMessage = QCoreApplication::translate(context: "AppFontManager", key: "The font file '%1' does not have read permissions.").arg(a: fontFile);
143 return -1;
144 }
145 const QString fullPath = inf.absoluteFilePath();
146 // Check if already loaded
147 const FileNameFontIdPairs::const_iterator cend = m_fonts.constEnd();
148 for (FileNameFontIdPairs::const_iterator it = m_fonts.constBegin(); it != cend; ++it) {
149 if (it->first == fullPath) {
150 *errorMessage = QCoreApplication::translate(context: "AppFontManager", key: "The font file '%1' is already loaded.").arg(a: fontFile);
151 return -1;
152 }
153 }
154
155 const int id = QFontDatabase::addApplicationFont(fileName: fullPath);
156 if (id == -1) {
157 *errorMessage = QCoreApplication::translate(context: "AppFontManager", key: "The font file '%1' could not be loaded.").arg(a: fontFile);
158 return -1;
159 }
160
161 if (debugAppFontWidget)
162 qDebug() << "AppFontManager::add" << fontFile << id;
163 m_fonts.push_back(t: FileNameFontIdPair(fullPath, id));
164 return id;
165}
166
167bool AppFontManager::remove(int id, QString *errorMessage)
168{
169 const int count = m_fonts.size();
170 for (int i = 0; i < count; i++)
171 if (m_fonts[i].second == id)
172 return removeAt(index: i, errorMessage);
173
174 *errorMessage = QCoreApplication::translate(context: "AppFontManager", key: "'%1' is not a valid font id.").arg(a: id);
175 return false;
176}
177
178bool AppFontManager::remove(const QString &fontFile, QString *errorMessage)
179{
180 const int count = m_fonts.size();
181 for (int i = 0; i < count; i++)
182 if (m_fonts[i].first == fontFile)
183 return removeAt(index: i, errorMessage);
184
185 *errorMessage = QCoreApplication::translate(context: "AppFontManager", key: "There is no loaded font matching the id '%1'.").arg(a: fontFile);
186 return false;
187}
188
189bool AppFontManager::removeAt(int index, QString *errorMessage)
190{
191 Q_ASSERT(index >= 0 && index < m_fonts.size());
192
193 const QString fontFile = m_fonts[index].first;
194 const int id = m_fonts[index].second;
195
196 if (debugAppFontWidget)
197 qDebug() << "AppFontManager::removeAt" << index << '(' << fontFile << id << ')';
198
199 if (!QFontDatabase::removeApplicationFont(id)) {
200 *errorMessage = QCoreApplication::translate(context: "AppFontManager", key: "The font '%1' (%2) could not be unloaded.").arg(a: fontFile).arg(a: id);
201 return false;
202 }
203 m_fonts.removeAt(i: index);
204 return true;
205}
206
207const AppFontManager::FileNameFontIdPairs &AppFontManager::fonts() const
208{
209 return m_fonts;
210}
211
212// ------------- AppFontModel
213class AppFontModel : public QStandardItemModel {
214 Q_DISABLE_COPY_MOVE(AppFontModel)
215public:
216 AppFontModel(QObject *parent = nullptr);
217
218 void init(const AppFontManager &mgr);
219 void add(const QString &fontFile, int id);
220 int idAt(const QModelIndex &idx) const;
221};
222
223AppFontModel::AppFontModel(QObject * parent) :
224 QStandardItemModel(parent)
225{
226 setHorizontalHeaderLabels(QStringList(AppFontWidget::tr(s: "Fonts")));
227}
228
229void AppFontModel::init(const AppFontManager &mgr)
230{
231 using FileNameFontIdPairs = AppFontManager::FileNameFontIdPairs;
232
233 const FileNameFontIdPairs &fonts = mgr.fonts();
234 const FileNameFontIdPairs::const_iterator cend = fonts.constEnd();
235 for (FileNameFontIdPairs::const_iterator it = fonts.constBegin(); it != cend; ++it)
236 add(fontFile: it->first, id: it->second);
237}
238
239void AppFontModel::add(const QString &fontFile, int id)
240{
241 const QFileInfo inf(fontFile);
242 // Root item with base name
243 QStandardItem *fileItem = new QStandardItem(inf.completeBaseName());
244 const QString fullPath = inf.absoluteFilePath();
245 fileItem->setData(value: fullPath, role: FileNameRole);
246 fileItem->setToolTip(fullPath);
247 fileItem->setData(value: id, role: IdRole);
248 fileItem->setFlags(Qt::ItemIsSelectable|Qt::ItemIsEnabled);
249
250 appendRow(aitem: fileItem);
251 const QStringList families = QFontDatabase::applicationFontFamilies(id);
252 const QStringList::const_iterator cend = families.constEnd();
253 for (QStringList::const_iterator it = families.constBegin(); it != cend; ++it) {
254 QStandardItem *familyItem = new QStandardItem(*it);
255 familyItem->setToolTip(fullPath);
256 familyItem->setFont(QFont(*it));
257 familyItem->setFlags(Qt::ItemIsEnabled);
258 fileItem->appendRow(aitem: familyItem);
259 }
260}
261
262int AppFontModel::idAt(const QModelIndex &idx) const
263{
264 if (const QStandardItem *item = itemFromIndex(index: idx))
265 return item->data(role: IdRole).toInt();
266 return -1;
267}
268
269// ------------- AppFontWidget
270AppFontWidget::AppFontWidget(QWidget *parent) :
271 QGroupBox(parent),
272 m_view(new QTreeView),
273 m_addButton(new QToolButton),
274 m_removeButton(new QToolButton),
275 m_removeAllButton(new QToolButton),
276 m_model(new AppFontModel(this))
277{
278 m_model->init(mgr: AppFontManager::instance());
279 m_view->setModel(m_model);
280 m_view->setSelectionMode(QAbstractItemView::ExtendedSelection);
281 m_view->expandAll();
282 connect(sender: m_view->selectionModel(), signal: &QItemSelectionModel::selectionChanged, receiver: this, slot: &AppFontWidget::selectionChanged);
283
284 m_addButton->setToolTip(tr(s: "Add font files"));
285 m_addButton->setIcon(qdesigner_internal::createIconSet(name: QString::fromUtf8(str: "plus.png")));
286 connect(sender: m_addButton, signal: &QAbstractButton::clicked, receiver: this, slot: &AppFontWidget::addFiles);
287
288 m_removeButton->setEnabled(false);
289 m_removeButton->setToolTip(tr(s: "Remove current font file"));
290 m_removeButton->setIcon(qdesigner_internal::createIconSet(name: QString::fromUtf8(str: "minus.png")));
291 connect(sender: m_removeButton, signal: &QAbstractButton::clicked, receiver: this, slot: &AppFontWidget::slotRemoveFiles);
292
293 m_removeAllButton->setToolTip(tr(s: "Remove all font files"));
294 m_removeAllButton->setIcon(qdesigner_internal::createIconSet(name: QString::fromUtf8(str: "editdelete.png")));
295 connect(sender: m_removeAllButton, signal: &QAbstractButton::clicked, receiver: this, slot: &AppFontWidget::slotRemoveAll);
296
297 QHBoxLayout *hLayout = new QHBoxLayout;
298 hLayout->addWidget(m_addButton);
299 hLayout->addWidget(m_removeButton);
300 hLayout->addWidget(m_removeAllButton);
301 hLayout->addItem(new QSpacerItem(0, 0,QSizePolicy::MinimumExpanding));
302
303 QVBoxLayout *vLayout = new QVBoxLayout;
304 vLayout->addWidget(m_view);
305 vLayout->addLayout(layout: hLayout);
306 setLayout(vLayout);
307}
308
309void AppFontWidget::addFiles()
310{
311 const QStringList files =
312 QFileDialog::getOpenFileNames(parent: this, caption: tr(s: "Add Font Files"), dir: QString(),
313 filter: tr(s: "Font files (*.ttf)"));
314 if (files.isEmpty())
315 return;
316
317 QString errorMessage;
318
319 AppFontManager &fmgr = AppFontManager::instance();
320 const QStringList::const_iterator cend = files.constEnd();
321 for (QStringList::const_iterator it = files.constBegin(); it != cend; ++it) {
322 const int id = fmgr.add(fontFile: *it, errorMessage: &errorMessage);
323 if (id != -1) {
324 m_model->add(fontFile: *it, id);
325 } else {
326 QMessageBox::critical(parent: this, title: tr(s: "Error Adding Fonts"), text: errorMessage);
327 }
328 }
329 m_view->expandAll();
330}
331
332static void removeFonts(const QModelIndexList &selectedIndexes, AppFontModel *model, QWidget *dialogParent)
333{
334 if (selectedIndexes.isEmpty())
335 return;
336
337 // Reverse sort top level rows and remove
338 AppFontManager &fmgr = AppFontManager::instance();
339 QVector<int> rows;
340 rows.reserve(asize: selectedIndexes.size());
341
342 QString errorMessage;
343 const QModelIndexList::const_iterator cend = selectedIndexes.constEnd();
344 for (QModelIndexList::const_iterator it = selectedIndexes.constBegin(); it != cend; ++it) {
345 const int id = model->idAt(idx: *it);
346 if (id != -1) {
347 if (fmgr.remove(id, errorMessage: &errorMessage)) {
348 rows.push_back(t: it->row());
349 } else {
350 QMessageBox::critical(parent: dialogParent, title: AppFontWidget::tr(s: "Error Removing Fonts"), text: errorMessage);
351 }
352 }
353 }
354
355 std::stable_sort(first: rows.begin(), last: rows.end());
356 for (int i = rows.size() - 1; i >= 0; i--)
357 model->removeRow(arow: rows[i]);
358}
359
360void AppFontWidget::slotRemoveFiles()
361{
362 removeFonts(selectedIndexes: m_view->selectionModel()->selectedIndexes(), model: m_model, dialogParent: this);
363}
364
365void AppFontWidget::slotRemoveAll()
366{
367 const int count = m_model->rowCount();
368 if (!count)
369 return;
370
371 const QMessageBox::StandardButton answer =
372 QMessageBox::question(parent: this, title: tr(s: "Remove Fonts"), text: tr(s: "Would you like to remove all fonts?"),
373 buttons: QMessageBox::Yes|QMessageBox::No, defaultButton: QMessageBox::No);
374 if (answer == QMessageBox::No)
375 return;
376
377 QModelIndexList topLevels;
378 for (int i = 0; i < count; i++)
379 topLevels.push_back(t: m_model->index(row: i, column: 0));
380 removeFonts(selectedIndexes: topLevels, model: m_model, dialogParent: this);
381}
382
383void AppFontWidget::selectionChanged(const QItemSelection &selected, const QItemSelection & /*deselected*/)
384{
385 m_removeButton->setEnabled(!selected.indexes().isEmpty());
386}
387
388void AppFontWidget::save(QDesignerSettingsInterface *s, const QString &prefix)
389{
390 AppFontManager::instance().save(s, prefix);
391}
392
393void AppFontWidget::restore(const QDesignerSettingsInterface *s, const QString &prefix)
394{
395 AppFontManager::instance().restore(s, prefix);
396}
397
398// ------------ AppFontDialog
399AppFontDialog::AppFontDialog(QWidget *parent) :
400 QDialog(parent),
401 m_appFontWidget(new AppFontWidget)
402{
403 setAttribute(Qt::WA_DeleteOnClose, on: true);
404 setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
405 setWindowTitle(tr(s: "Additional Fonts"));
406 setModal(false);
407 QVBoxLayout *vl = new QVBoxLayout;
408 vl->addWidget(m_appFontWidget);
409
410 QDialogButtonBox *bb = new QDialogButtonBox(QDialogButtonBox::Close);
411 QDialog::connect(sender: bb, signal: &QDialogButtonBox::rejected, receiver: this, slot: &AppFontDialog::reject);
412 vl->addWidget(bb);
413 setLayout(vl);
414}
415
416QT_END_NAMESPACE
417

source code of qttools/src/designer/src/designer/appfontdialog.cpp