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->setClearColor(Qt::transparent); | ||||
60 | m_dialog->quickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView); | ||||
61 | m_dialog->quickWidget->setSource(QUrl::fromLocalFile(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("xdg-desktop-portal-kde/qml/AppChooserDialog.qml")))); | ||||
62 | | ||||
63 | QObject *rootItem = m_dialog->quickWidget->rootObject(); | ||||
64 | connect(rootItem, SIGNAL(openDiscover()), this, SLOT(onOpenDiscover())); | ||||
65 | connect(rootItem, SIGNAL(applicationSelected(QString)), this, SLOT(onApplicationSelected(QString))); | ||||
66 | | ||||
67 | setWindowTitle(i18n("Open with...")); | ||||
68 | } | ||||
69 | | ||||
70 | AppChooserDialog::~AppChooserDialog() | ||||
71 | { | ||||
72 | delete m_dialog; | ||||
73 | } | ||||
74 | | ||||
75 | QString AppChooserDialog::selectedApplication() const | ||||
76 | { | ||||
77 | return m_selectedApplication; | ||||
78 | } | ||||
46 | 79 | | |||
47 | QVBoxLayout *vboxLayout = new QVBoxLayout(this); | 80 | void AppChooserDialog::onApplicationSelected(const QString& desktopFile) | ||
48 | vboxLayout->setSpacing(20); | 81 | { | ||
49 | vboxLayout->setContentsMargins(20, 20, 20, 20); | 82 | m_selectedApplication = desktopFile; | ||
50 | 83 | QDialog::accept(); | |||
51 | QLabel *label = new QLabel(this); | 84 | } | ||
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 | 85 | | |||
58 | connect(label, &QLabel::linkActivated, this, [] () { | 86 | void AppChooserDialog::onOpenDiscover() | ||
87 | { | ||||
59 | KProcess::startDetached(QStringLiteral("plasma-discover")); | 88 | KProcess::startDetached(QStringLiteral("plasma-discover")); | ||
60 | }); | 89 | } | ||
61 | 90 | | |||
62 | vboxLayout->addWidget(label); | 91 | void AppChooserDialog::updateChoices(const QStringList &choices) | ||
92 | { | ||||
93 | m_model->setPreferredApps(choices); | ||||
94 | } | ||||
63 | 95 | | |||
64 | QWidget *appsWidget = new QWidget(this); | 96 | ApplicationItem::ApplicationItem(const QString &name, const QString &icon, const QString &desktopFileName) | ||
65 | QScrollArea *scrollArea = new QScrollArea(this); | 97 | : m_applicationName(name) | ||
66 | scrollArea->setFrameShape(QFrame::NoFrame); | 98 | , m_applicationIcon(icon) | ||
67 | scrollArea->setWidget(appsWidget); | 99 | , m_applicationDesktopFile(desktopFileName) | ||
68 | scrollArea->setWidgetResizable(true); | 100 | , m_applicationCategory(AllApplications) | ||
101 | { | ||||
102 | } | ||||
69 | 103 | | |||
70 | // FIXME: workaround scrollarea sizing, set minimum height to make sure at least two rows are visible | 104 | QString ApplicationItem::applicationName() const | ||
71 | if (choices.count() > 3) { | 105 | { | ||
72 | scrollArea->setMinimumHeight(200); | 106 | return m_applicationName; | ||
73 | } | 107 | } | ||
74 | 108 | | |||
75 | m_gridLayout = new QGridLayout; | 109 | QString ApplicationItem::applicationIcon() const | ||
76 | appsWidget->setLayout(m_gridLayout); | 110 | { | ||
111 | return m_applicationIcon; | ||||
112 | } | ||||
77 | 113 | | |||
78 | QTimer::singleShot(0, this, &AppChooserDialog::addDialogItems); | 114 | QString ApplicationItem::applicationDesktopFile() const | ||
115 | { | ||||
116 | return m_applicationDesktopFile; | ||||
117 | } | ||||
79 | 118 | | |||
80 | vboxLayout->addWidget(scrollArea); | 119 | void ApplicationItem::setApplicationCategory(ApplicationItem::ApplicationCategory category) | ||
120 | { | ||||
121 | m_applicationCategory = category; | ||||
122 | } | ||||
81 | 123 | | |||
82 | setLayout(vboxLayout); | 124 | ApplicationItem::ApplicationCategory ApplicationItem::applicationCategory() const | ||
83 | setWindowTitle(i18n("Open with")); | 125 | { | ||
126 | return m_applicationCategory; | ||||
84 | } | 127 | } | ||
85 | 128 | | |||
86 | AppChooserDialog::~AppChooserDialog() | 129 | bool ApplicationItem::operator==(const ApplicationItem &item) const | ||
87 | { | 130 | { | ||
88 | delete m_gridLayout; | 131 | return item.applicationDesktopFile() == applicationDesktopFile(); | ||
89 | } | 132 | } | ||
90 | 133 | | |||
91 | void AppChooserDialog::updateChoices(const QStringList &choices) | 134 | AppFilterModel::AppFilterModel(QObject *parent) | ||
135 | : QSortFilterProxyModel(parent) | ||||
92 | { | 136 | { | ||
93 | bool changed = false; | 137 | setDynamicSortFilter(true); | ||
138 | setFilterCaseSensitivity(Qt::CaseInsensitive); | ||||
139 | sort(0, Qt::DescendingOrder); | ||||
140 | } | ||||
94 | 141 | | |||
95 | // Check if we will be adding something | 142 | AppFilterModel::~AppFilterModel() | ||
96 | for (const QString &choice : choices) { | 143 | { | ||
97 | if (!m_choices.contains(choice)) { | | |||
98 | changed = true; | | |||
99 | m_choices << choice; | | |||
100 | } | 144 | } | ||
145 | | ||||
146 | void AppFilterModel::setShowOnlyPrefferedApps(bool show) | ||||
147 | { | ||||
148 | m_showOnlyPreferredApps = show; | ||||
149 | | ||||
150 | invalidate(); | ||||
101 | } | 151 | } | ||
102 | 152 | | |||
103 | // Check if we will be removing something | 153 | bool AppFilterModel::showOnlyPreferredApps() const | ||
104 | for (const QString &choice : m_choices) { | 154 | { | ||
105 | if (!choices.contains(choice)) { | 155 | return m_showOnlyPreferredApps; | ||
156 | } | ||||
157 | | ||||
158 | void AppFilterModel::setFilter(const QString &text) | ||||
159 | { | ||||
160 | m_filter = text; | ||||
161 | | ||||
162 | invalidate(); | ||||
163 | } | ||||
164 | | ||||
165 | QString AppFilterModel::filter() const | ||||
166 | { | ||||
167 | return m_filter; | ||||
168 | } | ||||
169 | | ||||
170 | bool AppFilterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const | ||||
171 | { | ||||
172 | const QModelIndex index = sourceModel()->index(source_row, 0, source_parent); | ||||
173 | | ||||
174 | ApplicationItem::ApplicationCategory category = static_cast<ApplicationItem::ApplicationCategory>(sourceModel()->data(index, AppModel::ApplicationCategoryRole).toInt()); | ||||
175 | QString appName = sourceModel()->data(index, AppModel::ApplicationNameRole).toString(); | ||||
176 | | ||||
177 | if (m_showOnlyPreferredApps) | ||||
178 | return category == ApplicationItem::PreferredApplication; | ||||
179 | | ||||
180 | if (category == ApplicationItem::PreferredApplication) | ||||
181 | return true; | ||||
182 | | ||||
183 | if (m_filter.isEmpty()) | ||||
184 | return true; | ||||
185 | | ||||
186 | return appName.toLower().contains(m_filter); | ||||
187 | } | ||||
188 | | ||||
189 | bool AppFilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const | ||||
190 | { | ||||
191 | ApplicationItem::ApplicationCategory leftCategory = static_cast<ApplicationItem::ApplicationCategory>(sourceModel()->data(left, AppModel::ApplicationCategoryRole).toInt()); | ||||
192 | ApplicationItem::ApplicationCategory rightCategory = static_cast<ApplicationItem::ApplicationCategory>(sourceModel()->data(right, AppModel::ApplicationCategoryRole).toInt()); | ||||
193 | QString leftName = sourceModel()->data(left, AppModel::ApplicationNameRole).toString(); | ||||
194 | QString rightName = sourceModel()->data(right, AppModel::ApplicationNameRole).toString(); | ||||
195 | | ||||
196 | if (leftCategory < rightCategory) { | ||||
197 | return false; | ||||
198 | } else if (leftCategory > rightCategory) { | ||||
199 | return true; | ||||
200 | } | ||||
201 | | ||||
202 | return QString::localeAwareCompare(leftName, rightName) > 0; | ||||
203 | } | ||||
204 | | ||||
205 | AppModel::AppModel(QObject *parent) | ||||
206 | : QAbstractListModel(parent) | ||||
207 | { | ||||
208 | loadApplications(); | ||||
209 | } | ||||
210 | | ||||
211 | AppModel::~AppModel() | ||||
212 | { | ||||
213 | } | ||||
214 | | ||||
215 | void AppModel::setPreferredApps(const QStringList &list) | ||||
216 | { | ||||
217 | for (ApplicationItem &item : m_list) { | ||||
218 | bool changed = false; | ||||
219 | | ||||
220 | // First reset to initial type | ||||
221 | if (item.applicationCategory() != ApplicationItem::AllApplications) { | ||||
222 | item.setApplicationCategory(ApplicationItem::AllApplications); | ||||
106 | changed = true; | 223 | changed = true; | ||
107 | m_choices.removeAll(choice); | | |||
108 | } | 224 | } | ||
225 | | ||||
226 | if (list.contains(item.applicationDesktopFile())) { | ||||
227 | item.setApplicationCategory(ApplicationItem::PreferredApplication); | ||||
228 | changed = true; | ||||
109 | } | 229 | } | ||
110 | 230 | | |||
111 | // If something changed, clear the layout and add the items again | | |||
112 | if (changed) { | 231 | if (changed) { | ||
113 | int rowCount = m_gridLayout->rowCount(); | 232 | const int row = m_list.indexOf(item); | ||
114 | int columnCount = m_gridLayout->columnCount(); | 233 | if (row >= 0) { | ||
115 | 234 | QModelIndex index = createIndex(row, 0, AppModel::ApplicationCategoryRole); | |||
116 | for (int i = 0; i < rowCount; ++i) { | 235 | 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 | } | 236 | } | ||
125 | } | 237 | } | ||
126 | } | 238 | } | ||
127 | } | 239 | } | ||
128 | 240 | | |||
129 | addDialogItems(); | 241 | QVariant AppModel::data(const QModelIndex &index, int role) const | ||
242 | { | ||||
243 | const int row = index.row(); | ||||
244 | | ||||
245 | if (row >= 0 && row < m_list.count()) { | ||||
246 | ApplicationItem item = m_list.at(row); | ||||
247 | | ||||
248 | switch (role) { | ||||
249 | case ApplicationNameRole: | ||||
250 | return item.applicationName(); | ||||
251 | case ApplicationIconRole: | ||||
252 | return item.applicationIcon(); | ||||
253 | case ApplicationDesktopFileRole: | ||||
254 | return item.applicationDesktopFile(); | ||||
255 | case ApplicationCategoryRole: | ||||
256 | return static_cast<int>(item.applicationCategory()); | ||||
257 | default: | ||||
258 | break; | ||||
130 | } | 259 | } | ||
131 | } | 260 | } | ||
132 | 261 | | |||
133 | QString AppChooserDialog::selectedApplication() const | 262 | return QVariant(); | ||
263 | } | ||||
264 | | ||||
265 | int AppModel::rowCount(const QModelIndex &parent) const | ||||
134 | { | 266 | { | ||
135 | if (m_selectedApplication.isEmpty()) { | 267 | return parent.isValid() ? 0 : m_list.count(); | ||
136 | return m_defaultApp; | | |||
137 | } | 268 | } | ||
138 | 269 | | |||
139 | return m_selectedApplication; | 270 | QHash<int, QByteArray> AppModel::roleNames() const | ||
271 | { | ||||
272 | QHash<int, QByteArray> roles = QAbstractListModel::roleNames(); | ||||
273 | roles[ApplicationNameRole] = "ApplicationName"; | ||||
274 | roles[ApplicationIconRole] = "ApplicationIcon"; | ||||
275 | roles[ApplicationDesktopFileRole] = "ApplicationDesktopFile"; | ||||
276 | roles[ApplicationCategoryRole] = "ApplicationCategory"; | ||||
277 | | ||||
278 | return roles; | ||||
140 | } | 279 | } | ||
141 | 280 | | |||
142 | void AppChooserDialog::addDialogItems() | 281 | void AppModel::loadApplications() | ||
143 | { | 282 | { | ||
144 | int i = 0, j = 0; | 283 | for (const QString &location : QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation)) { | ||
145 | for (const QString &choice : m_choices) { | 284 | QDir dir(location); | ||
146 | const QString desktopFile = choice + QStringLiteral(".desktop"); | 285 | 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; | 286 | QString applicationIcon; | ||
150 | QString applicationName; | 287 | QString applicationName; | ||
151 | QSettings settings(desktopFile, QSettings::IniFormat); | 288 | | ||
289 | QSettings settings(QStringLiteral("%1/%2").arg(dir.path()).arg(entry), QSettings::IniFormat); | ||||
152 | settings.beginGroup(QStringLiteral("Desktop Entry")); | 290 | settings.beginGroup(QStringLiteral("Desktop Entry")); | ||
153 | if (settings.contains(QStringLiteral("X-GNOME-FullName"))) { | 291 | if (settings.contains(QStringLiteral("X-GNOME-FullName"))) { | ||
154 | applicationName = settings.value(QStringLiteral("X-GNOME-FullName")).toString(); | 292 | applicationName = settings.value(QStringLiteral("X-GNOME-FullName")).toString(); | ||
155 | } else { | 293 | } else { | ||
156 | applicationName = settings.value(QStringLiteral("Name")).toString(); | 294 | applicationName = settings.value(QStringLiteral("Name")).toString(); | ||
157 | } | 295 | } | ||
158 | applicationIcon = settings.value(QStringLiteral("Icon")).toString(); | 296 | applicationIcon = settings.value(QStringLiteral("Icon")).toString(); | ||
159 | 297 | | |||
160 | AppChooserDialogItem *item = new AppChooserDialogItem(applicationName, applicationIcon, choice, this); | 298 | const QString desktopFileWithoutSuffix = entry.remove(QStringLiteral(".desktop")); | ||
161 | m_gridLayout->addWidget(item, i, j++, Qt::AlignHCenter); | 299 | if (applicationName.isEmpty() || applicationIcon.isEmpty()) | ||
300 | continue; | ||||
162 | 301 | | |||
163 | connect(item, &AppChooserDialogItem::clicked, this, [this] (const QString &selectedApplication) { | 302 | ApplicationItem appItem(applicationName, applicationIcon, desktopFileWithoutSuffix); | ||
164 | m_selectedApplication = selectedApplication; | | |||
165 | QDialog::accept(); | | |||
166 | }); | | |||
167 | 303 | | |||
168 | if (choice == m_defaultApp) { | 304 | if (!m_list.contains(appItem)) | ||
169 | item->setDown(true); | 305 | m_list.append(appItem); | ||
170 | item->setChecked(true); | | |||
171 | } | | |||
172 | | ||||
173 | if (j == 3) { | | |||
174 | i++; | | |||
175 | j = 0; | | |||
176 | } | | |||
177 | } | 306 | } | ||
178 | } | 307 | } | ||
179 | } | 308 | } |