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 | |
55 | QT_BEGIN_NAMESPACE |
56 | |
57 | enum {FileNameRole = Qt::UserRole + 1, IdRole = Qt::UserRole + 2 }; |
58 | enum { debugAppFontWidget = 0 }; |
59 | |
60 | static 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 | |
66 | class AppFontManager |
67 | { |
68 | Q_DISABLE_COPY_MOVE(AppFontManager) |
69 | AppFontManager(); |
70 | public: |
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 | |
88 | private: |
89 | FileNameFontIdPairs m_fonts; |
90 | }; |
91 | |
92 | AppFontManager::AppFontManager() = default; |
93 | |
94 | AppFontManager &AppFontManager::instance() |
95 | { |
96 | static AppFontManager rc; |
97 | return rc; |
98 | } |
99 | |
100 | void 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 | |
116 | void 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 | |
134 | int 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 | |
167 | bool 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 | |
178 | bool 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 | |
189 | bool 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 | |
207 | const AppFontManager::FileNameFontIdPairs &AppFontManager::fonts() const |
208 | { |
209 | return m_fonts; |
210 | } |
211 | |
212 | // ------------- AppFontModel |
213 | class AppFontModel : public QStandardItemModel { |
214 | Q_DISABLE_COPY_MOVE(AppFontModel) |
215 | public: |
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 | |
223 | AppFontModel::AppFontModel(QObject * parent) : |
224 | QStandardItemModel(parent) |
225 | { |
226 | setHorizontalHeaderLabels(QStringList(AppFontWidget::tr(s: "Fonts" ))); |
227 | } |
228 | |
229 | void 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 | |
239 | void 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 | |
262 | int 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 |
270 | AppFontWidget::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 | |
309 | void 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 | |
332 | static 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 | |
360 | void AppFontWidget::slotRemoveFiles() |
361 | { |
362 | removeFonts(selectedIndexes: m_view->selectionModel()->selectedIndexes(), model: m_model, dialogParent: this); |
363 | } |
364 | |
365 | void 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 | |
383 | void AppFontWidget::selectionChanged(const QItemSelection &selected, const QItemSelection & /*deselected*/) |
384 | { |
385 | m_removeButton->setEnabled(!selected.indexes().isEmpty()); |
386 | } |
387 | |
388 | void AppFontWidget::save(QDesignerSettingsInterface *s, const QString &prefix) |
389 | { |
390 | AppFontManager::instance().save(s, prefix); |
391 | } |
392 | |
393 | void AppFontWidget::restore(const QDesignerSettingsInterface *s, const QString &prefix) |
394 | { |
395 | AppFontManager::instance().restore(s, prefix); |
396 | } |
397 | |
398 | // ------------ AppFontDialog |
399 | AppFontDialog::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 | |
416 | QT_END_NAMESPACE |
417 | |