Changeset View
Changeset View
Standalone View
Standalone View
src/appchooserdialog.cpp
1 | /* | 1 | /* | ||
---|---|---|---|---|---|
2 | * Copyright © 2017-2018 Red Hat, Inc | 2 | * Copyright © 2017-2019 Red Hat, Inc | ||
3 | * | 3 | * | ||
4 | * This program is free software; you can redistribute it and/or | 4 | * This program is free software; you can redistribute it and/or | ||
5 | * modify it under the terms of the GNU Lesser General Public | 5 | * modify it under the terms of the GNU Lesser General Public | ||
6 | * License as published by the Free Software Foundation; either | 6 | * License as published by the Free Software Foundation; either | ||
7 | * version 2 of the License, or (at your option) any later version. | 7 | * version 2 of the License, or (at your option) any later version. | ||
8 | * | 8 | * | ||
9 | * This library is distributed in the hope that it will be useful, | 9 | * This library is distributed in the hope that it will be useful, | ||
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
12 | * Lesser General Public License for more details. | 12 | * Lesser General Public License for more details. | ||
13 | * | 13 | * | ||
14 | * You should have received a copy of the GNU Lesser General Public | 14 | * You should have received a copy of the GNU Lesser General Public | ||
15 | * License along with this library. If not, see <http://www.gnu.org/licenses/>. | 15 | * License along with this library. If not, see <http://www.gnu.org/licenses/>. | ||
16 | * | 16 | * | ||
17 | * Authors: | 17 | * Authors: | ||
18 | * Jan Grulich <jgrulich@redhat.com> | 18 | * Jan Grulich <jgrulich@redhat.com> | ||
19 | */ | 19 | */ | ||
20 | 20 | | |||
21 | #include "appchooserdialog.h" | 21 | #include "appchooserdialog.h" | ||
22 | #include "appchooserdialogitem.h" | 22 | #include "ui_appchooserdialog.h" | ||
23 | 23 | | |||
24 | #include <QGridLayout> | 24 | #include <QQmlContext> | ||
25 | #include <QVBoxLayout> | 25 | #include <QQmlEngine> | ||
26 | #include <QLabel> | 26 | #include <QQuickWidget> | ||
27 | #include <QLayoutItem> | 27 | #include <QQuickItem> | ||
28 | #include <QLoggingCategory> | 28 | | ||
29 | #include <KLocalizedString> | 29 | #include <QDir> | ||
30 | #include <QSettings> | | |||
31 | #include <QStandardPaths> | 30 | #include <QStandardPaths> | ||
32 | #include <QScrollArea> | 31 | #include <QSettings> | ||
33 | #include <QTimer> | | |||
34 | 32 | | |||
35 | #include <KProcess> | 33 | #include <KProcess> | ||
36 | 34 | #include <kdeclarative/kdeclarative.h> | |||
37 | Q_LOGGING_CATEGORY(XdgDesktopPortalKdeAppChooserDialog, "xdp-kde-app-chooser-dialog") | | |||
38 | 35 | | |||
39 | AppChooserDialog::AppChooserDialog(const QStringList &choices, const QString &defaultApp, const QString &fileName, QDialog *parent, Qt::WindowFlags flags) | 36 | AppChooserDialog::AppChooserDialog(const QStringList &choices, const QString &defaultApp, const QString &fileName, QDialog *parent, Qt::WindowFlags flags) | ||
40 | : QDialog(parent, flags) | 37 | : QDialog(parent, flags) | ||
41 | , m_choices(choices) | 38 | , m_dialog(new Ui::AppChooserDialog) | ||
39 | , m_defaultChoices(choices) | ||||
42 | , m_defaultApp(defaultApp) | 40 | , m_defaultApp(defaultApp) | ||
43 | { | 41 | { | ||
44 | setMinimumWidth(640); | 42 | m_dialog->setupUi(this); | ||
45 | setMaximumHeight(480); | 43 | | ||
44 | KDeclarative::KDeclarative kdeclarative; | ||||
45 | kdeclarative.setDeclarativeEngine(m_dialog->quickWidget->engine()); | ||||
46 | kdeclarative.setTranslationDomain(QStringLiteral(TRANSLATION_DOMAIN)); | ||||
47 | kdeclarative.setupEngine(m_dialog->quickWidget->engine()); | ||||
48 | kdeclarative.setupContext(); | ||||
49 | | ||||
50 | m_model = new AppModel(this); | ||||
51 | m_model->setPreferredApps(choices); | ||||
52 | | ||||
53 | AppFilterModel *filterModel = new AppFilterModel(this); | ||||
54 | filterModel->setSourceModel(m_model); | ||||
55 | | ||||
56 | m_dialog->quickWidget->rootContext()->setContextProperty(QStringLiteral("myModel"), filterModel); | ||||
57 | m_dialog->quickWidget->rootContext()->setContextProperty(QStringLiteral("fileName"), fileName); | ||||
58 | m_dialog->quickWidget->rootContext()->setContextProperty(QStringLiteral("defaultApp"), defaultApp); | ||||
59 | m_dialog->quickWidget->rootContext()->setContextProperty(QStringLiteral("backgroundColor"), palette().color(QPalette::Active, QPalette::Window)); | ||||
60 | m_dialog->quickWidget->rootContext()->setContextProperty(QStringLiteral("highlightColor"), palette().color(QPalette::Active, QPalette::Highlight)); | ||||
61 | m_dialog->quickWidget->setClearColor(Qt::transparent); | ||||
62 | m_dialog->quickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView); | ||||
63 | m_dialog->quickWidget->setSource(QUrl::fromLocalFile(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("xdg-desktop-portal-kde/qml/AppChooserDialog.qml")))); | ||||
64 | | ||||
65 | QObject *rootItem = m_dialog->quickWidget->rootObject(); | ||||
66 | connect(rootItem, SIGNAL(openDiscover()), this, SLOT(onOpenDiscover())); | ||||
67 | connect(rootItem, SIGNAL(applicationSelected(QString)), this, SLOT(onApplicationSelected(QString))); | ||||
68 | | ||||
69 | setWindowTitle(i18n("Open with...")); | ||||
70 | } | ||||
71 | | ||||
72 | AppChooserDialog::~AppChooserDialog() | ||||
73 | { | ||||
74 | delete m_dialog; | ||||
75 | } | ||||
76 | | ||||
77 | QString AppChooserDialog::selectedApplication() const | ||||
78 | { | ||||
79 | return m_selectedApplication; | ||||
80 | } | ||||
46 | 81 | | |||
47 | QVBoxLayout *vboxLayout = new QVBoxLayout(this); | 82 | void AppChooserDialog::onApplicationSelected(const QString& desktopFile) | ||
48 | vboxLayout->setSpacing(20); | 83 | { | ||
49 | vboxLayout->setContentsMargins(20, 20, 20, 20); | 84 | m_selectedApplication = desktopFile; | ||
50 | 85 | QDialog::accept(); | |||
51 | QLabel *label = new QLabel(this); | 86 | } | ||
52 | label->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter); | | |||
53 | label->setScaledContents(true); | | |||
54 | label->setWordWrap(true); | | |||
55 | label->setText(i18n("Select application to open \"%1\". Other applications are available in <a href=#discover><span style=\"text-decoration: underline\">Discover</span></a>.", fileName)); | | |||
56 | label->setOpenExternalLinks(false); | | |||
57 | 87 | | |||
58 | connect(label, &QLabel::linkActivated, this, [] () { | 88 | void AppChooserDialog::onOpenDiscover() | ||
89 | { | ||||
59 | KProcess::startDetached(QStringLiteral("plasma-discover")); | 90 | KProcess::startDetached(QStringLiteral("plasma-discover")); | ||
60 | }); | 91 | } | ||
61 | 92 | | |||
62 | vboxLayout->addWidget(label); | 93 | void AppChooserDialog::updateChoices(const QStringList &choices) | ||
94 | { | ||||
95 | m_model->setPreferredApps(choices); | ||||
96 | } | ||||
63 | 97 | | |||
64 | QWidget *appsWidget = new QWidget(this); | 98 | ApplicationItem::ApplicationItem(const QString &name, const QString &icon, const QString &desktopFileName) | ||
65 | QScrollArea *scrollArea = new QScrollArea(this); | 99 | : m_applicationName(name) | ||
66 | scrollArea->setFrameShape(QFrame::NoFrame); | 100 | , m_applicationIcon(icon) | ||
67 | scrollArea->setWidget(appsWidget); | 101 | , m_applicationDesktopFile(desktopFileName) | ||
68 | scrollArea->setWidgetResizable(true); | 102 | , m_applicationCategory(AllApplications) | ||
103 | { | ||||
104 | } | ||||
69 | 105 | | |||
70 | // FIXME: workaround scrollarea sizing, set minimum height to make sure at least two rows are visible | 106 | QString ApplicationItem::applicationName() const | ||
71 | if (choices.count() > 3) { | 107 | { | ||
72 | scrollArea->setMinimumHeight(200); | 108 | return m_applicationName; | ||
73 | } | 109 | } | ||
74 | 110 | | |||
75 | m_gridLayout = new QGridLayout; | 111 | QString ApplicationItem::applicationIcon() const | ||
76 | appsWidget->setLayout(m_gridLayout); | 112 | { | ||
113 | return m_applicationIcon; | ||||
114 | } | ||||
77 | 115 | | |||
78 | QTimer::singleShot(0, this, &AppChooserDialog::addDialogItems); | 116 | QString ApplicationItem::applicationDesktopFile() const | ||
117 | { | ||||
118 | return m_applicationDesktopFile; | ||||
119 | } | ||||
79 | 120 | | |||
80 | vboxLayout->addWidget(scrollArea); | 121 | void ApplicationItem::setApplicationCategory(ApplicationItem::ApplicationCategory category) | ||
122 | { | ||||
123 | m_applicationCategory = category; | ||||
124 | } | ||||
81 | 125 | | |||
82 | setLayout(vboxLayout); | 126 | ApplicationItem::ApplicationCategory ApplicationItem::applicationCategory() const | ||
83 | setWindowTitle(i18n("Open with")); | 127 | { | ||
128 | return m_applicationCategory; | ||||
84 | } | 129 | } | ||
85 | 130 | | |||
86 | AppChooserDialog::~AppChooserDialog() | 131 | bool ApplicationItem::operator==(const ApplicationItem &item) const | ||
87 | { | 132 | { | ||
88 | delete m_gridLayout; | 133 | return item.applicationDesktopFile() == applicationDesktopFile(); | ||
89 | } | 134 | } | ||
90 | 135 | | |||
91 | void AppChooserDialog::updateChoices(const QStringList &choices) | 136 | AppFilterModel::AppFilterModel(QObject *parent) | ||
137 | : QSortFilterProxyModel(parent) | ||||
92 | { | 138 | { | ||
93 | bool changed = false; | 139 | setDynamicSortFilter(true); | ||
140 | setFilterCaseSensitivity(Qt::CaseInsensitive); | ||||
141 | sort(0, Qt::DescendingOrder); | ||||
142 | } | ||||
94 | 143 | | |||
95 | // Check if we will be adding something | 144 | AppFilterModel::~AppFilterModel() | ||
96 | for (const QString &choice : choices) { | 145 | { | ||
97 | if (!m_choices.contains(choice)) { | | |||
98 | changed = true; | | |||
99 | m_choices << choice; | | |||
100 | } | 146 | } | ||
147 | | ||||
148 | void AppFilterModel::setShowOnlyPrefferedApps(bool show) | ||||
149 | { | ||||
150 | m_showOnlyPreferredApps = show; | ||||
151 | | ||||
152 | invalidate(); | ||||
101 | } | 153 | } | ||
102 | 154 | | |||
103 | // Check if we will be removing something | 155 | bool AppFilterModel::showOnlyPreferredApps() const | ||
104 | for (const QString &choice : m_choices) { | 156 | { | ||
105 | if (!choices.contains(choice)) { | 157 | return m_showOnlyPreferredApps; | ||
158 | } | ||||
159 | | ||||
160 | void AppFilterModel::setFilter(const QString &text) | ||||
161 | { | ||||
162 | m_filter = text; | ||||
163 | | ||||
164 | invalidate(); | ||||
165 | } | ||||
166 | | ||||
167 | QString AppFilterModel::filter() const | ||||
168 | { | ||||
169 | return m_filter; | ||||
170 | } | ||||
171 | | ||||
172 | bool AppFilterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const | ||||
173 | { | ||||
174 | const QModelIndex index = sourceModel()->index(source_row, 0, source_parent); | ||||
175 | | ||||
176 | ApplicationItem::ApplicationCategory category = static_cast<ApplicationItem::ApplicationCategory>(sourceModel()->data(index, AppModel::ApplicationCategoryRole).toInt()); | ||||
177 | QString appName = sourceModel()->data(index, AppModel::ApplicationNameRole).toString(); | ||||
178 | | ||||
179 | if (m_showOnlyPreferredApps) | ||||
180 | return category == ApplicationItem::PreferredApplication; | ||||
181 | | ||||
182 | if (category == ApplicationItem::PreferredApplication) | ||||
183 | return true; | ||||
184 | | ||||
185 | if (m_filter.isEmpty()) | ||||
186 | return true; | ||||
187 | | ||||
188 | return appName.toLower().contains(m_filter); | ||||
189 | } | ||||
190 | | ||||
191 | bool AppFilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const | ||||
192 | { | ||||
193 | ApplicationItem::ApplicationCategory leftCategory = static_cast<ApplicationItem::ApplicationCategory>(sourceModel()->data(left, AppModel::ApplicationCategoryRole).toInt()); | ||||
194 | ApplicationItem::ApplicationCategory rightCategory = static_cast<ApplicationItem::ApplicationCategory>(sourceModel()->data(right, AppModel::ApplicationCategoryRole).toInt()); | ||||
195 | QString leftName = sourceModel()->data(left, AppModel::ApplicationNameRole).toString(); | ||||
196 | QString rightName = sourceModel()->data(right, AppModel::ApplicationNameRole).toString(); | ||||
197 | | ||||
198 | if (leftCategory < rightCategory) { | ||||
199 | return false; | ||||
200 | } else if (leftCategory > rightCategory) { | ||||
201 | return true; | ||||
202 | } | ||||
203 | | ||||
204 | return QString::localeAwareCompare(leftName, rightName) > 0; | ||||
205 | } | ||||
206 | | ||||
207 | AppModel::AppModel(QObject *parent) | ||||
208 | : QAbstractListModel(parent) | ||||
209 | { | ||||
210 | loadApplications(); | ||||
211 | } | ||||
212 | | ||||
213 | AppModel::~AppModel() | ||||
214 | { | ||||
215 | } | ||||
216 | | ||||
217 | void AppModel::setPreferredApps(const QStringList &list) | ||||
218 | { | ||||
219 | for (ApplicationItem &item : m_list) { | ||||
220 | bool changed = false; | ||||
221 | | ||||
222 | // First reset to initial type | ||||
223 | if (item.applicationCategory() != ApplicationItem::AllApplications) { | ||||
224 | item.setApplicationCategory(ApplicationItem::AllApplications); | ||||
106 | changed = true; | 225 | changed = true; | ||
107 | m_choices.removeAll(choice); | | |||
108 | } | 226 | } | ||
227 | | ||||
228 | if (list.contains(item.applicationDesktopFile())) { | ||||
229 | item.setApplicationCategory(ApplicationItem::PreferredApplication); | ||||
230 | changed = true; | ||||
109 | } | 231 | } | ||
110 | 232 | | |||
111 | // If something changed, clear the layout and add the items again | | |||
112 | if (changed) { | 233 | if (changed) { | ||
113 | int rowCount = m_gridLayout->rowCount(); | 234 | const int row = m_list.indexOf(item); | ||
114 | int columnCount = m_gridLayout->columnCount(); | 235 | if (row >= 0) { | ||
115 | 236 | QModelIndex index = createIndex(row, 0, AppModel::ApplicationCategoryRole); | |||
116 | for (int i = 0; i < rowCount; ++i) { | 237 | Q_EMIT dataChanged(index, index); | ||
117 | for (int j = 0; j < columnCount; ++j) { | | |||
118 | QLayoutItem *item = m_gridLayout->itemAtPosition(i, j); | | |||
119 | if (item) { | | |||
120 | QWidget *widget = item->widget(); | | |||
121 | if (widget) { | | |||
122 | m_gridLayout->removeWidget(widget); | | |||
123 | widget->deleteLater(); | | |||
124 | } | 238 | } | ||
125 | } | 239 | } | ||
126 | } | 240 | } | ||
127 | } | 241 | } | ||
128 | 242 | | |||
129 | addDialogItems(); | 243 | QVariant AppModel::data(const QModelIndex &index, int role) const | ||
244 | { | ||||
245 | const int row = index.row(); | ||||
246 | | ||||
247 | if (row >= 0 && row < m_list.count()) { | ||||
248 | ApplicationItem item = m_list.at(row); | ||||
249 | | ||||
250 | switch (role) { | ||||
251 | case ApplicationNameRole: | ||||
252 | return item.applicationName(); | ||||
253 | case ApplicationIconRole: | ||||
254 | return item.applicationIcon(); | ||||
255 | case ApplicationDesktopFileRole: | ||||
256 | return item.applicationDesktopFile(); | ||||
257 | case ApplicationCategoryRole: | ||||
258 | return static_cast<int>(item.applicationCategory()); | ||||
259 | default: | ||||
260 | break; | ||||
130 | } | 261 | } | ||
131 | } | 262 | } | ||
132 | 263 | | |||
133 | QString AppChooserDialog::selectedApplication() const | 264 | return QVariant(); | ||
265 | } | ||||
266 | | ||||
267 | int AppModel::rowCount(const QModelIndex &parent) const | ||||
134 | { | 268 | { | ||
135 | if (m_selectedApplication.isEmpty()) { | 269 | return parent.isValid() ? 0 : m_list.count(); | ||
136 | return m_defaultApp; | | |||
137 | } | 270 | } | ||
138 | 271 | | |||
139 | return m_selectedApplication; | 272 | QHash<int, QByteArray> AppModel::roleNames() const | ||
273 | { | ||||
274 | QHash<int, QByteArray> roles = QAbstractListModel::roleNames(); | ||||
275 | roles[ApplicationNameRole] = "ApplicationName"; | ||||
276 | roles[ApplicationIconRole] = "ApplicationIcon"; | ||||
277 | roles[ApplicationDesktopFileRole] = "ApplicationDesktopFile"; | ||||
278 | roles[ApplicationCategoryRole] = "ApplicationCategory"; | ||||
279 | | ||||
280 | return roles; | ||||
140 | } | 281 | } | ||
141 | 282 | | |||
142 | void AppChooserDialog::addDialogItems() | 283 | void AppModel::loadApplications() | ||
143 | { | 284 | { | ||
144 | int i = 0, j = 0; | 285 | for (const QString &location : QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation)) { | ||
145 | for (const QString &choice : m_choices) { | 286 | QDir dir(location); | ||
146 | const QString desktopFile = choice + QStringLiteral(".desktop"); | 287 | for (QString &entry : dir.entryList(QStringList({QStringLiteral("*.desktop")}), QDir::Files, QDir::Name)) { | ||
147 | const QStringList desktopFilesLocations = QStandardPaths::locateAll(QStandardPaths::ApplicationsLocation, desktopFile, QStandardPaths::LocateFile); | | |||
148 | for (const QString &desktopFile : desktopFilesLocations) { | | |||
149 | QString applicationIcon; | 288 | QString applicationIcon; | ||
150 | QString applicationName; | 289 | QString applicationName; | ||
151 | QSettings settings(desktopFile, QSettings::IniFormat); | 290 | | ||
291 | QSettings settings(QStringLiteral("%1/%2").arg(dir.path()).arg(entry), QSettings::IniFormat); | ||||
152 | settings.beginGroup(QStringLiteral("Desktop Entry")); | 292 | settings.beginGroup(QStringLiteral("Desktop Entry")); | ||
153 | if (settings.contains(QStringLiteral("X-GNOME-FullName"))) { | 293 | if (settings.contains(QStringLiteral("X-GNOME-FullName"))) { | ||
154 | applicationName = settings.value(QStringLiteral("X-GNOME-FullName")).toString(); | 294 | applicationName = settings.value(QStringLiteral("X-GNOME-FullName")).toString(); | ||
155 | } else { | 295 | } else { | ||
156 | applicationName = settings.value(QStringLiteral("Name")).toString(); | 296 | applicationName = settings.value(QStringLiteral("Name")).toString(); | ||
157 | } | 297 | } | ||
158 | applicationIcon = settings.value(QStringLiteral("Icon")).toString(); | 298 | applicationIcon = settings.value(QStringLiteral("Icon")).toString(); | ||
159 | 299 | | |||
160 | AppChooserDialogItem *item = new AppChooserDialogItem(applicationName, applicationIcon, choice, this); | 300 | const QString desktopFileWithoutSuffix = entry.remove(QStringLiteral(".desktop")); | ||
161 | m_gridLayout->addWidget(item, i, j++, Qt::AlignHCenter); | 301 | if (applicationName.isEmpty() || applicationIcon.isEmpty()) | ||
302 | continue; | ||||
162 | 303 | | |||
163 | connect(item, &AppChooserDialogItem::clicked, this, [this] (const QString &selectedApplication) { | 304 | ApplicationItem appItem(applicationName, applicationIcon, desktopFileWithoutSuffix); | ||
164 | m_selectedApplication = selectedApplication; | | |||
165 | QDialog::accept(); | | |||
166 | }); | | |||
167 | 305 | | |||
168 | if (choice == m_defaultApp) { | 306 | if (!m_list.contains(appItem)) | ||
169 | item->setDown(true); | 307 | m_list.append(appItem); | ||
170 | item->setChecked(true); | | |||
171 | } | | |||
172 | | ||||
173 | if (j == 3) { | | |||
174 | i++; | | |||
175 | j = 0; | | |||
176 | } | | |||
177 | } | 308 | } | ||
178 | } | 309 | } | ||
179 | } | 310 | } |