1 | /* vi: ts=8 sts=4 sw=4 |
2 | |
3 | This file is part of the KDE project, module kfile. |
4 | SPDX-FileCopyrightText: 2000 Geert Jansen <jansen@kde.org> |
5 | SPDX-FileCopyrightText: 2000 Kurt Granroth <granroth@kde.org> |
6 | SPDX-FileCopyrightText: 1997 Christoph Neerfeld <chris@kde.org> |
7 | SPDX-FileCopyrightText: 2002 Carsten Pfeiffer <pfeiffer@kde.org> |
8 | SPDX-FileCopyrightText: 2021 Kai Uwe Broulik <kde@broulik.de> |
9 | |
10 | SPDX-License-Identifier: LGPL-2.0-only |
11 | */ |
12 | |
13 | #include "kicondialog.h" |
14 | #include "kicondialog_p.h" |
15 | #include "kicondialogmodel_p.h" |
16 | |
17 | #include <KLazyLocalizedString> |
18 | #include <KLocalizedString> |
19 | #include <KStandardAction> |
20 | |
21 | #include <QAbstractListModel> |
22 | #include <QActionGroup> |
23 | #include <QApplication> |
24 | #include <QComboBox> |
25 | #include <QDialogButtonBox> |
26 | #include <QFileInfo> |
27 | #include <QGraphicsOpacityEffect> |
28 | #include <QLabel> |
29 | #include <QList> |
30 | #include <QMenu> |
31 | #include <QPainter> |
32 | #include <QScrollBar> |
33 | #include <QSortFilterProxyModel> |
34 | #include <QStandardItemModel> // for manipulatig QComboBox |
35 | #include <QStandardPaths> |
36 | #include <QSvgRenderer> |
37 | |
38 | #include <algorithm> |
39 | #include <math.h> |
40 | |
41 | static const int s_edgePad = 3; |
42 | |
43 | class KIconDialogSortFilterProxyModel : public QSortFilterProxyModel |
44 | { |
45 | Q_OBJECT |
46 | |
47 | public: |
48 | explicit KIconDialogSortFilterProxyModel(QObject *parent); |
49 | |
50 | enum SymbolicIcons { AllSymbolicIcons, OnlySymbolicIcons, NoSymbolicIcons }; |
51 | |
52 | void setSymbolicIcons(SymbolicIcons symbolicIcons); |
53 | void setHasSymbolicIcon(bool hasSymbolicIcon); |
54 | |
55 | protected: |
56 | bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; |
57 | |
58 | private: |
59 | SymbolicIcons m_symbolicIcons = AllSymbolicIcons; |
60 | bool m_hasSymbolicIcon = false; |
61 | }; |
62 | |
63 | KIconDialogSortFilterProxyModel::KIconDialogSortFilterProxyModel(QObject *parent) |
64 | : QSortFilterProxyModel(parent) |
65 | { |
66 | } |
67 | |
68 | void KIconDialogSortFilterProxyModel::setSymbolicIcons(SymbolicIcons symbolicIcons) |
69 | { |
70 | if (m_symbolicIcons == symbolicIcons) { |
71 | return; |
72 | } |
73 | |
74 | m_symbolicIcons = symbolicIcons; |
75 | invalidateFilter(); |
76 | } |
77 | |
78 | void KIconDialogSortFilterProxyModel::setHasSymbolicIcon(bool hasSymbolicIcon) |
79 | { |
80 | if (m_hasSymbolicIcon == hasSymbolicIcon) { |
81 | return; |
82 | } |
83 | |
84 | m_hasSymbolicIcon = hasSymbolicIcon; |
85 | invalidateFilter(); |
86 | } |
87 | |
88 | bool KIconDialogSortFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const |
89 | { |
90 | if (m_hasSymbolicIcon) { |
91 | if (m_symbolicIcons == OnlySymbolicIcons || m_symbolicIcons == NoSymbolicIcons) { |
92 | const QString display = sourceModel()->index(row: source_row, column: 0, parent: source_parent).data(arole: Qt::DisplayRole).toString(); |
93 | const bool isSymbolic = display.endsWith(s: KIconDialogModel::symbolicSuffix()); |
94 | if ((m_symbolicIcons == OnlySymbolicIcons && !isSymbolic) || (m_symbolicIcons == NoSymbolicIcons && isSymbolic)) { |
95 | return false; |
96 | } |
97 | } |
98 | } |
99 | |
100 | return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); |
101 | } |
102 | |
103 | KIconDialogModel::KIconDialogModel(KIconLoader *loader, QObject *parent) |
104 | : QAbstractListModel(parent) |
105 | , m_loader(loader) |
106 | { |
107 | } |
108 | |
109 | KIconDialogModel::~KIconDialogModel() = default; |
110 | |
111 | qreal KIconDialogModel::devicePixelRatio() const |
112 | { |
113 | return m_dpr; |
114 | } |
115 | |
116 | void KIconDialogModel::setDevicePixelRatio(qreal dpr) |
117 | { |
118 | m_dpr = dpr; |
119 | } |
120 | |
121 | QSize KIconDialogModel::iconSize() const |
122 | { |
123 | return m_iconSize; |
124 | } |
125 | |
126 | void KIconDialogModel::setIconSize(const QSize &iconSize) |
127 | { |
128 | m_iconSize = iconSize; |
129 | } |
130 | |
131 | QLatin1String KIconDialogModel::symbolicSuffix() |
132 | { |
133 | return QLatin1String("-symbolic" ); |
134 | } |
135 | |
136 | bool KIconDialogModel::hasSymbolicIcon() const |
137 | { |
138 | return m_hasSymbolicIcon; |
139 | } |
140 | |
141 | void KIconDialogModel::load(const QStringList &paths) |
142 | { |
143 | beginResetModel(); |
144 | |
145 | const bool oldSymbolic = m_hasSymbolicIcon; |
146 | m_hasSymbolicIcon = false; |
147 | |
148 | m_data.clear(); |
149 | m_data.reserve(asize: paths.count()); |
150 | |
151 | for (const QString &path : paths) { |
152 | const QFileInfo fi(path); |
153 | |
154 | KIconDialogModelData item; |
155 | item.name = fi.completeBaseName(); |
156 | item.path = path; |
157 | // pixmap is created on demand |
158 | |
159 | if (!m_hasSymbolicIcon && item.name.endsWith(s: symbolicSuffix())) { |
160 | m_hasSymbolicIcon = true; |
161 | } |
162 | |
163 | m_data.append(t: item); |
164 | } |
165 | |
166 | endResetModel(); |
167 | |
168 | if (oldSymbolic != m_hasSymbolicIcon) { |
169 | Q_EMIT hasSymbolicIconChanged(hasSymbolicIcon: m_hasSymbolicIcon); |
170 | } |
171 | } |
172 | |
173 | int KIconDialogModel::rowCount(const QModelIndex &parent) const |
174 | { |
175 | if (parent.isValid()) { |
176 | return 0; |
177 | } |
178 | return m_data.count(); |
179 | } |
180 | |
181 | QVariant KIconDialogModel::data(const QModelIndex &index, int role) const |
182 | { |
183 | if (!checkIndex(index, options: QAbstractItemModel::CheckIndexOption::IndexIsValid)) { |
184 | return QVariant(); |
185 | } |
186 | |
187 | const auto &item = m_data.at(i: index.row()); |
188 | |
189 | switch (role) { |
190 | case Qt::DisplayRole: |
191 | return item.name; |
192 | case Qt::DecorationRole: |
193 | if (item.pixmap.isNull()) { |
194 | const_cast<KIconDialogModel *>(this)->loadPixmap(index); |
195 | } |
196 | return item.pixmap; |
197 | case Qt::ToolTipRole: |
198 | return item.name; |
199 | case PathRole: |
200 | return item.path; |
201 | } |
202 | |
203 | return QVariant(); |
204 | } |
205 | |
206 | void KIconDialogModel::loadPixmap(const QModelIndex &index) |
207 | { |
208 | Q_ASSERT(index.isValid()); |
209 | |
210 | auto &item = m_data[index.row()]; |
211 | Q_ASSERT(item.pixmap.isNull()); |
212 | |
213 | const auto dpr = devicePixelRatio(); |
214 | |
215 | item.pixmap = m_loader->loadScaledIcon(name: item.path, group: KIconLoader::Desktop, scale: dpr, size: iconSize(), state: KIconLoader::DefaultState, overlays: {}, path_store: nullptr, canReturnNull: true); |
216 | item.pixmap.setDevicePixelRatio(dpr); |
217 | } |
218 | |
219 | /** |
220 | * Qt allocates very little horizontal space for the icon name, |
221 | * even if the gridSize width is large. This delegate allocates |
222 | * the gridSize width (minus some padding) for the icon and icon name. |
223 | */ |
224 | class KIconCanvasDelegate : public QAbstractItemDelegate |
225 | { |
226 | Q_OBJECT |
227 | public: |
228 | KIconCanvasDelegate(QListView *parent, QAbstractItemDelegate *defaultDelegate); |
229 | ~KIconCanvasDelegate() override |
230 | { |
231 | } |
232 | void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; |
233 | QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; |
234 | |
235 | private: |
236 | QAbstractItemDelegate *m_defaultDelegate = nullptr; |
237 | }; |
238 | |
239 | KIconCanvasDelegate::KIconCanvasDelegate(QListView *parent, QAbstractItemDelegate *defaultDelegate) |
240 | : QAbstractItemDelegate(parent) |
241 | { |
242 | m_defaultDelegate = defaultDelegate; |
243 | } |
244 | |
245 | void KIconCanvasDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const |
246 | { |
247 | auto *canvas = static_cast<QListView *>(parent()); |
248 | const int gridWidth = canvas->gridSize().width(); |
249 | QStyleOptionViewItem newOption = option; |
250 | newOption.displayAlignment = Qt::AlignHCenter | Qt::AlignTop; |
251 | newOption.features.setFlag(flag: QStyleOptionViewItem::WrapText); |
252 | // Manipulate the width available. |
253 | newOption.rect.setX((option.rect.x() / gridWidth) * gridWidth + s_edgePad); |
254 | newOption.rect.setY(option.rect.y() + s_edgePad); |
255 | newOption.rect.setWidth(gridWidth - 2 * s_edgePad); |
256 | newOption.rect.setHeight(option.rect.height() - 2 * s_edgePad); |
257 | m_defaultDelegate->paint(painter, option: newOption, index); |
258 | } |
259 | |
260 | QSize KIconCanvasDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const |
261 | { |
262 | auto *canvas = static_cast<QListView *>(parent()); |
263 | |
264 | // TODO can we set wrap text and display alignment somewhere globally? |
265 | QStyleOptionViewItem newOption = option; |
266 | newOption.displayAlignment = Qt::AlignHCenter | Qt::AlignTop; |
267 | newOption.features.setFlag(flag: QStyleOptionViewItem::WrapText); |
268 | |
269 | QSize size = m_defaultDelegate->sizeHint(option: newOption, index); |
270 | const int gridWidth = canvas->gridSize().width(); |
271 | const int gridHeight = canvas->gridSize().height(); |
272 | size.setWidth(gridWidth - 2 * s_edgePad); |
273 | size.setHeight(gridHeight - 2 * s_edgePad); |
274 | QFontMetrics metrics(option.font); |
275 | size.setHeight(gridHeight + metrics.height() * 3); |
276 | return size; |
277 | } |
278 | |
279 | KIconDialogPrivate::KIconDialogPrivate(KIconDialog *qq) |
280 | : q(qq) |
281 | , mpLoader(KIconLoader::global()) |
282 | , model(new KIconDialogModel(mpLoader, qq)) |
283 | , proxyModel(new KIconDialogSortFilterProxyModel(qq)) |
284 | , filterSymbolicAction(new QAction(qq)) |
285 | , filterSymbolicGroup(new QActionGroup(qq)) |
286 | { |
287 | proxyModel->setSourceModel(model); |
288 | proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); |
289 | |
290 | filterSymbolicGroup->setExclusive(true); |
291 | |
292 | QObject::connect(sender: model, signal: &KIconDialogModel::hasSymbolicIconChanged, context: filterSymbolicAction, slot: &QAction::setVisible); |
293 | QObject::connect(sender: model, signal: &KIconDialogModel::hasSymbolicIconChanged, context: proxyModel, slot: &KIconDialogSortFilterProxyModel::setHasSymbolicIcon); |
294 | } |
295 | |
296 | /* |
297 | * KIconDialog: Dialog for selecting icons. Both system and user |
298 | * specified icons can be chosen. |
299 | */ |
300 | |
301 | KIconDialog::KIconDialog(QWidget *parent) |
302 | : QDialog(parent) |
303 | , d(new KIconDialogPrivate(this)) |
304 | { |
305 | setModal(true); |
306 | |
307 | d->init(); |
308 | } |
309 | |
310 | void KIconDialogPrivate::init() |
311 | { |
312 | mGroupOrSize = KIconLoader::Desktop; |
313 | mContext = KIconLoader::Any; |
314 | |
315 | ui.setupUi(q); |
316 | |
317 | auto updatePlaceholder = [this] { |
318 | updatePlaceholderLabel(); |
319 | }; |
320 | QObject::connect(sender: proxyModel, signal: &QSortFilterProxyModel::modelReset, context: q, slot&: updatePlaceholder); |
321 | QObject::connect(sender: proxyModel, signal: &QSortFilterProxyModel::rowsInserted, context: q, slot&: updatePlaceholder); |
322 | QObject::connect(sender: proxyModel, signal: &QSortFilterProxyModel::rowsRemoved, context: q, slot&: updatePlaceholder); |
323 | |
324 | QAction *findAction = KStandardAction::find(recvr: ui.searchLine, slot: qOverload<>(&QWidget::setFocus), parent: q); |
325 | q->addAction(action: findAction); |
326 | |
327 | QMenu * = new QMenu(q); |
328 | |
329 | QAction *filterSymbolicAll = filterSymbolicMenu->addAction(i18nc("@item:inmenu All icons" , "All" )); |
330 | filterSymbolicAll->setData(KIconDialogSortFilterProxyModel::AllSymbolicIcons); |
331 | filterSymbolicAll->setChecked(true); // Start with "All" icons. |
332 | filterSymbolicAll->setCheckable(true); |
333 | |
334 | QAction *filterSymbolicOnly = filterSymbolicMenu->addAction(i18nc("@item:inmenu Show only symbolic icons" , "Only Symbolic" )); |
335 | filterSymbolicOnly->setData(KIconDialogSortFilterProxyModel::OnlySymbolicIcons); |
336 | filterSymbolicOnly->setCheckable(true); |
337 | |
338 | QAction *filterSymbolicNone = filterSymbolicMenu->addAction(i18nc("@item:inmenu Hide symbolic icons" , "No Symbolic" )); |
339 | filterSymbolicNone->setData(KIconDialogSortFilterProxyModel::NoSymbolicIcons); |
340 | filterSymbolicNone->setCheckable(true); |
341 | |
342 | filterSymbolicAction->setIcon(QIcon::fromTheme(QStringLiteral("view-filter" ))); |
343 | filterSymbolicAction->setCheckable(true); |
344 | filterSymbolicAction->setChecked(true); |
345 | filterSymbolicAction->setMenu(filterSymbolicMenu); |
346 | |
347 | filterSymbolicGroup->addAction(a: filterSymbolicAll); |
348 | filterSymbolicGroup->addAction(a: filterSymbolicOnly); |
349 | filterSymbolicGroup->addAction(a: filterSymbolicNone); |
350 | QObject::connect(sender: filterSymbolicGroup, signal: &QActionGroup::triggered, context: q, slot: [this](QAction *action) { |
351 | proxyModel->setSymbolicIcons(static_cast<KIconDialogSortFilterProxyModel::SymbolicIcons>(action->data().toInt())); |
352 | }); |
353 | |
354 | ui.searchLine->addAction(action: filterSymbolicAction, position: QLineEdit::TrailingPosition); |
355 | |
356 | QObject::connect(sender: ui.searchLine, signal: &QLineEdit::textChanged, context: proxyModel, slot: &QSortFilterProxyModel::setFilterFixedString); |
357 | |
358 | static const KLazyLocalizedString context_text[] = { |
359 | kli18n(text: "All" ), |
360 | kli18n(text: "Actions" ), |
361 | kli18n(text: "Applications" ), |
362 | kli18n(text: "Categories" ), |
363 | kli18n(text: "Devices" ), |
364 | kli18n(text: "Emblems" ), |
365 | kli18n(text: "Emotes" ), |
366 | kli18n(text: "Mimetypes" ), |
367 | kli18n(text: "Places" ), |
368 | kli18n(text: "Status" ), |
369 | }; |
370 | static const KIconLoader::Context context_id[] = { |
371 | KIconLoader::Any, |
372 | KIconLoader::Action, |
373 | KIconLoader::Application, |
374 | KIconLoader::Category, |
375 | KIconLoader::Device, |
376 | KIconLoader::Emblem, |
377 | KIconLoader::Emote, |
378 | KIconLoader::MimeType, |
379 | KIconLoader::Place, |
380 | KIconLoader::StatusIcon, |
381 | }; |
382 | const int cnt = sizeof(context_text) / sizeof(context_text[0]); |
383 | for (int i = 0; i < cnt; ++i) { |
384 | if (mpLoader->hasContext(context: context_id[i])) { |
385 | ui.contextCombo->addItem(atext: context_text[i].toString(), auserData: context_id[i]); |
386 | if (i == 0) { |
387 | ui.contextCombo->insertSeparator(index: i + 1); |
388 | } |
389 | } |
390 | } |
391 | ui.contextCombo->insertSeparator(index: ui.contextCombo->count()); |
392 | ui.contextCombo->addItem(i18nc("Other icons" , "Other" )); |
393 | ui.contextCombo->setMaxVisibleItems(ui.contextCombo->count()); |
394 | ui.contextCombo->setFixedSize(ui.contextCombo->sizeHint()); |
395 | |
396 | QObject::connect(sender: ui.contextCombo, signal: qOverload<int>(&QComboBox::activated), context: q, slot: [this]() { |
397 | const auto currentData = ui.contextCombo->currentData(); |
398 | if (currentData.isValid()) { |
399 | mContext = static_cast<KIconLoader::Context>(ui.contextCombo->currentData().toInt()); |
400 | } else { |
401 | mContext = static_cast<KIconLoader::Context>(-1); |
402 | } |
403 | showIcons(); |
404 | }); |
405 | |
406 | auto *delegate = new KIconCanvasDelegate(ui.canvas, ui.canvas->itemDelegate()); |
407 | ui.canvas->setItemDelegate(delegate); |
408 | |
409 | ui.canvas->setModel(proxyModel); |
410 | |
411 | QObject::connect(sender: ui.canvas, signal: &QAbstractItemView::activated, context: q, slot: [this]() { |
412 | custom.clear(); |
413 | q->slotOk(); |
414 | }); |
415 | |
416 | // You can't just stack widgets on top of each other in Qt Designer |
417 | auto *placeholderLayout = new QVBoxLayout(ui.canvas); |
418 | |
419 | placeholderLabel = new QLabel(); |
420 | QFont placeholderLabelFont; |
421 | // To match the size of a level 2 Heading/KTitleWidget |
422 | placeholderLabelFont.setPointSize(qRound(d: placeholderLabelFont.pointSize() * 1.3)); |
423 | placeholderLabel->setFont(placeholderLabelFont); |
424 | placeholderLabel->setTextInteractionFlags(Qt::NoTextInteraction); |
425 | placeholderLabel->setWordWrap(true); |
426 | placeholderLabel->setAlignment(Qt::AlignCenter); |
427 | |
428 | // Match opacity of QML placeholder label component |
429 | auto *effect = new QGraphicsOpacityEffect(placeholderLabel); |
430 | effect->setOpacity(0.5); |
431 | placeholderLabel->setGraphicsEffect(effect); |
432 | |
433 | updatePlaceholderLabel(); |
434 | |
435 | placeholderLayout->addWidget(placeholderLabel); |
436 | placeholderLayout->setAlignment(w: placeholderLabel, alignment: Qt::AlignCenter); |
437 | |
438 | // TODO I bet there is a KStandardAction for that? |
439 | browseButton = new QPushButton(QIcon::fromTheme(QStringLiteral("folder-open" )), i18n("Browse…" )); |
440 | // TODO does this have implicatons? I just want the "Browse" button on the left side :) |
441 | ui.buttonBox->addButton(button: browseButton, role: QDialogButtonBox::HelpRole); |
442 | QObject::connect(sender: browseButton, signal: &QPushButton::clicked, context: q, slot: [this] { |
443 | browse(); |
444 | }); |
445 | |
446 | QObject::connect(sender: ui.buttonBox, signal: &QDialogButtonBox::accepted, context: q, slot: &KIconDialog::slotOk); |
447 | QObject::connect(sender: ui.buttonBox, signal: &QDialogButtonBox::rejected, context: q, slot: &QDialog::reject); |
448 | |
449 | q->adjustSize(); |
450 | } |
451 | |
452 | KIconDialog::~KIconDialog() = default; |
453 | |
454 | static bool sortByFileName(const QString &path1, const QString &path2) |
455 | { |
456 | const QString fileName1 = path1.mid(position: path1.lastIndexOf(c: QLatin1Char('/')) + 1); |
457 | const QString fileName2 = path2.mid(position: path2.lastIndexOf(c: QLatin1Char('/')) + 1); |
458 | return QString::compare(s1: fileName1, s2: fileName2, cs: Qt::CaseInsensitive) < 0; |
459 | } |
460 | |
461 | void KIconDialogPrivate::showIcons() |
462 | { |
463 | QStringList filelist; |
464 | if (isSystemIconsContext()) { |
465 | if (m_bStrictIconSize) { |
466 | filelist = mpLoader->queryIcons(group_or_size: mGroupOrSize, context: mContext); |
467 | } else { |
468 | filelist = mpLoader->queryIconsByContext(group_or_size: mGroupOrSize, context: mContext); |
469 | } |
470 | } else if (!customLocation.isEmpty()) { |
471 | filelist = mpLoader->queryIconsByDir(iconsDir: customLocation); |
472 | } else { |
473 | // List PNG files found directly in the kiconload search paths. |
474 | const QStringList pngNameFilter(QStringLiteral("*.png" )); |
475 | for (const QString &relDir : KIconLoader::global()->searchPaths()) { |
476 | const QStringList dirs = QStandardPaths::locateAll(type: QStandardPaths::GenericDataLocation, fileName: relDir, options: QStandardPaths::LocateDirectory); |
477 | for (const QString &dir : dirs) { |
478 | const auto files = QDir(dir).entryList(nameFilters: pngNameFilter); |
479 | for (const QString &fileName : files) { |
480 | filelist << dir + QLatin1Char('/') + fileName; |
481 | } |
482 | } |
483 | } |
484 | } |
485 | |
486 | std::sort(first: filelist.begin(), last: filelist.end(), comp: sortByFileName); |
487 | |
488 | // The KIconCanvas has uniformItemSizes set which really expects |
489 | // all added icons to be the same size, otherwise weirdness ensues :) |
490 | // Ensure all SVGs are scaled to the desired size and that as few icons |
491 | // need to be padded as possible by specifying a sensible size. |
492 | if (mGroupOrSize < -1) { |
493 | // mGroupOrSize can be -1 if NoGroup is chosen. |
494 | // Explicit size. |
495 | ui.canvas->setIconSize(QSize(-mGroupOrSize, -mGroupOrSize)); |
496 | } else { |
497 | // Icon group. |
498 | int groupSize = mpLoader->currentSize(group: static_cast<KIconLoader::Group>(mGroupOrSize)); |
499 | ui.canvas->setIconSize(QSize(groupSize, groupSize)); |
500 | } |
501 | |
502 | // Try to make room for three lines of text... |
503 | QFontMetrics metrics(ui.canvas->font()); |
504 | const int frameHMargin = ui.canvas->style()->pixelMetric(metric: QStyle::PM_FocusFrameHMargin, option: nullptr, widget: ui.canvas) + 1; |
505 | const int lineCount = 3; |
506 | ui.canvas->setGridSize(QSize(100, ui.canvas->iconSize().height() + lineCount * metrics.height() + 3 * frameHMargin)); |
507 | |
508 | // Set a minimum size of 6x3 icons |
509 | const int columnCount = 6; |
510 | const int rowCount = 3; |
511 | QStyleOption opt; |
512 | opt.initFrom(w: ui.canvas); |
513 | int width = columnCount * ui.canvas->gridSize().width(); |
514 | width += ui.canvas->verticalScrollBar()->sizeHint().width() + 1; |
515 | width += 2 * ui.canvas->frameWidth(); |
516 | if (ui.canvas->style()->styleHint(stylehint: QStyle::SH_ScrollView_FrameOnlyAroundContents, opt: &opt, widget: ui.canvas)) { |
517 | width += ui.canvas->style()->pixelMetric(metric: QStyle::PM_ScrollView_ScrollBarSpacing, option: &opt, widget: ui.canvas); |
518 | } |
519 | int height = rowCount * ui.canvas->gridSize().height() + 1; |
520 | height += 2 * ui.canvas->frameWidth(); |
521 | |
522 | ui.canvas->setMinimumSize(QSize(width, height)); |
523 | |
524 | model->setIconSize(ui.canvas->iconSize()); |
525 | model->setDevicePixelRatio(q->devicePixelRatioF()); |
526 | model->load(paths: filelist); |
527 | |
528 | if (!pendingSelectedIcon.isEmpty()) { |
529 | selectIcon(iconName: pendingSelectedIcon); |
530 | pendingSelectedIcon.clear(); |
531 | } |
532 | } |
533 | |
534 | bool KIconDialogPrivate::selectIcon(const QString &iconName) |
535 | { |
536 | for (int i = 0; i < proxyModel->rowCount(); ++i) { |
537 | const QModelIndex idx = proxyModel->index(row: i, column: 0); |
538 | |
539 | QString name = idx.data(arole: KIconDialogModel::PathRole).toString(); |
540 | if (!name.isEmpty() && isSystemIconsContext()) { |
541 | const QFileInfo fi(name); |
542 | name = fi.completeBaseName(); |
543 | } |
544 | |
545 | if (iconName == name) { |
546 | ui.canvas->setCurrentIndex(idx); |
547 | return true; |
548 | } |
549 | } |
550 | |
551 | return false; |
552 | } |
553 | |
554 | void KIconDialog::setStrictIconSize(bool b) |
555 | { |
556 | d->m_bStrictIconSize = b; |
557 | } |
558 | |
559 | bool KIconDialog::strictIconSize() const |
560 | { |
561 | return d->m_bStrictIconSize; |
562 | } |
563 | |
564 | void KIconDialog::setIconSize(int size) |
565 | { |
566 | // see KIconLoader, if you think this is weird |
567 | if (size == 0) { |
568 | d->mGroupOrSize = KIconLoader::Desktop; // default Group |
569 | } else { |
570 | d->mGroupOrSize = -size; // yes, KIconLoader::queryIconsByContext is weird |
571 | } |
572 | } |
573 | |
574 | int KIconDialog::iconSize() const |
575 | { |
576 | // 0 or any other value ==> mGroupOrSize is a group, so we return 0 |
577 | return (d->mGroupOrSize < 0) ? -d->mGroupOrSize : 0; |
578 | } |
579 | |
580 | void KIconDialog::setSelectedIcon(const QString &iconName) |
581 | { |
582 | // TODO Update live when dialog is already open |
583 | d->pendingSelectedIcon = iconName; |
584 | } |
585 | |
586 | void KIconDialog::setup(KIconLoader::Group group, KIconLoader::Context context, bool strictIconSize, int iconSize, bool user, bool lockUser, bool lockCustomDir) |
587 | { |
588 | d->m_bStrictIconSize = strictIconSize; |
589 | d->m_bLockUser = lockUser; |
590 | d->m_bLockCustomDir = lockCustomDir; |
591 | if (iconSize == 0) { |
592 | if (group == KIconLoader::NoGroup) { |
593 | // NoGroup has numeric value -1, which should |
594 | // not really be used with KIconLoader::queryIcons*(...); |
595 | // pick a proper group. |
596 | d->mGroupOrSize = KIconLoader::Small; |
597 | } else { |
598 | d->mGroupOrSize = group; |
599 | } |
600 | } else { |
601 | d->mGroupOrSize = -iconSize; |
602 | } |
603 | |
604 | if (user) { |
605 | d->ui.contextCombo->setCurrentIndex(d->ui.contextCombo->count() - 1); |
606 | } else { |
607 | d->setContext(context); |
608 | } |
609 | |
610 | d->ui.contextCombo->setEnabled(!user || !lockUser); |
611 | |
612 | // Disable "Other" entry when user is locked |
613 | auto *model = qobject_cast<QStandardItemModel *>(object: d->ui.contextCombo->model()); |
614 | auto *otherItem = model->item(row: model->rowCount() - 1); |
615 | auto flags = otherItem->flags(); |
616 | flags.setFlag(flag: Qt::ItemIsEnabled, on: !lockUser); |
617 | otherItem->setFlags(flags); |
618 | |
619 | // Only allow browsing when explicitly allowed and user icons are allowed |
620 | // An app may not expect a path when asking only about system icons |
621 | d->browseButton->setVisible(!lockCustomDir && (!user || !lockUser)); |
622 | } |
623 | |
624 | void KIconDialogPrivate::setContext(KIconLoader::Context context) |
625 | { |
626 | mContext = context; |
627 | const int index = ui.contextCombo->findData(data: context); |
628 | if (index > -1) { |
629 | ui.contextCombo->setCurrentIndex(index); |
630 | } |
631 | } |
632 | |
633 | void KIconDialogPrivate::updatePlaceholderLabel() |
634 | { |
635 | if (proxyModel->rowCount() > 0) { |
636 | placeholderLabel->hide(); |
637 | return; |
638 | } |
639 | |
640 | if (!ui.searchLine->text().isEmpty()) { |
641 | placeholderLabel->setText(i18n("No icons matching the search" )); |
642 | } else { |
643 | placeholderLabel->setText(i18n("No icons in this category" )); |
644 | } |
645 | |
646 | placeholderLabel->show(); |
647 | } |
648 | |
649 | void KIconDialog::setCustomLocation(const QString &location) |
650 | { |
651 | d->customLocation = location; |
652 | } |
653 | |
654 | QString KIconDialog::openDialog() |
655 | { |
656 | if (exec() == Accepted) { |
657 | if (!d->custom.isEmpty()) { |
658 | return d->custom; |
659 | } |
660 | |
661 | const QString name = d->ui.canvas->currentIndex().data(arole: KIconDialogModel::PathRole).toString(); |
662 | if (name.isEmpty() || !d->ui.contextCombo->currentData().isValid()) { |
663 | return name; |
664 | } |
665 | |
666 | QFileInfo fi(name); |
667 | return fi.completeBaseName(); |
668 | } |
669 | |
670 | return QString(); |
671 | } |
672 | |
673 | void KIconDialog::showDialog() |
674 | { |
675 | setModal(false); |
676 | show(); |
677 | } |
678 | |
679 | void KIconDialog::slotOk() |
680 | { |
681 | QString name; |
682 | if (!d->custom.isEmpty()) { |
683 | name = d->custom; |
684 | } else { |
685 | name = d->ui.canvas->currentIndex().data(arole: KIconDialogModel::PathRole).toString(); |
686 | if (!name.isEmpty() && d->isSystemIconsContext()) { |
687 | const QFileInfo fi(name); |
688 | name = fi.completeBaseName(); |
689 | } |
690 | } |
691 | |
692 | Q_EMIT newIconName(iconName: name); |
693 | QDialog::accept(); |
694 | } |
695 | |
696 | void KIconDialog::showEvent(QShowEvent *event) |
697 | { |
698 | QDialog::showEvent(event); |
699 | d->showIcons(); |
700 | d->ui.searchLine->setFocus(); |
701 | } |
702 | |
703 | QString KIconDialog::getIcon(KIconLoader::Group group, |
704 | KIconLoader::Context context, |
705 | bool strictIconSize, |
706 | int iconSize, |
707 | bool user, |
708 | QWidget *parent, |
709 | const QString &title) |
710 | { |
711 | KIconDialog dlg(parent); |
712 | dlg.setup(group, context, strictIconSize, iconSize, user); |
713 | if (!title.isEmpty()) { |
714 | dlg.setWindowTitle(title); |
715 | } |
716 | |
717 | return dlg.openDialog(); |
718 | } |
719 | |
720 | void KIconDialogPrivate::browse() |
721 | { |
722 | if (browseDialog) { |
723 | browseDialog.data()->show(); |
724 | browseDialog.data()->raise(); |
725 | return; |
726 | } |
727 | |
728 | // Create a file dialog to select an ICO, PNG, XPM or SVG file, |
729 | // with the image previewer shown. |
730 | QFileDialog *dlg = new QFileDialog(q, i18n("Select Icon" ), QString(), i18n("*.ico *.png *.xpm *.svg *.svgz|Icon Files (*.ico *.png *.xpm *.svg *.svgz)" )); |
731 | // TODO This was deliberately modal before, why? Or just because "window modal" wasn't a thing? |
732 | dlg->setWindowModality(Qt::WindowModal); |
733 | dlg->setFileMode(QFileDialog::ExistingFile); |
734 | QObject::connect(sender: dlg, signal: &QFileDialog::fileSelected, context: q, slot: [this](const QString &path) { |
735 | if (!path.isEmpty()) { |
736 | custom = path; |
737 | if (isSystemIconsContext()) { |
738 | customLocation = QFileInfo(custom).absolutePath(); |
739 | } |
740 | q->slotOk(); |
741 | } |
742 | }); |
743 | browseDialog = dlg; |
744 | dlg->show(); |
745 | } |
746 | |
747 | bool KIconDialogPrivate::isSystemIconsContext() const |
748 | { |
749 | return ui.contextCombo->currentData().isValid(); |
750 | } |
751 | |
752 | #include "kicondialog.moc" |
753 | #include "moc_kicondialog.cpp" |
754 | #include "moc_kicondialogmodel_p.cpp" |
755 | |