| 1 | /* |
| 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 <KStandardActions> |
| 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 = KStandardActions::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 | placeholderLayout->addWidget(placeholderLabel); |
| 434 | placeholderLayout->setAlignment(w: placeholderLabel, alignment: Qt::AlignCenter); |
| 435 | |
| 436 | updatePlaceholderLabel(); |
| 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 | |