]> cloud.milkyroute.net Git - dolphin.git/blob - src/panels/places/placespanel.cpp
[Places Panel] Reject drops on unwritable locations
[dolphin.git] / src / panels / places / placespanel.cpp
1 /*
2 * SPDX-FileCopyrightText: 2008-2012 Peter Penz <peter.penz19@gmail.com>
3 * SPDX-FileCopyrightText: 2021 Kai Uwe Broulik <kde@broulik.de>
4 *
5 * Based on KFilePlacesView from kdelibs:
6 * SPDX-FileCopyrightText: 2007 Kevin Ottens <ervin@kde.org>
7 * SPDX-FileCopyrightText: 2007 David Faure <faure@kde.org>
8 *
9 * SPDX-License-Identifier: GPL-2.0-or-later
10 */
11
12 #include "placespanel.h"
13
14 #include "dolphinplacesmodelsingleton.h"
15 #include "dolphin_generalsettings.h"
16 #include "dolphin_placespanelsettings.h"
17 #include "global.h"
18 #include "views/draganddrophelper.h"
19 #include "settings/dolphinsettingsdialog.h"
20
21 #include <KFilePlacesModel>
22 #include <KIO/DropJob>
23 #include <KIO/Job>
24 #include <KListOpenFilesJob>
25 #include <KLocalizedString>
26 #include <KProtocolManager>
27
28 #include <QIcon>
29 #include <QMenu>
30 #include <QMimeData>
31 #include <QShowEvent>
32 #include <QTimer>
33
34 #include <Solid/StorageAccess>
35
36 PlacesPanel::PlacesPanel(QWidget* parent)
37 : KFilePlacesView(parent)
38 {
39 setDropOnPlaceEnabled(true);
40 connect(this, &PlacesPanel::urlsDropped,
41 this, &PlacesPanel::slotUrlsDropped);
42
43 setAutoResizeItemsEnabled(false);
44
45 setTeardownFunction([this](const QModelIndex &index) {
46 slotTearDownRequested(index);
47 });
48
49 m_configureTrashAction = new QAction(QIcon::fromTheme(QStringLiteral("configure")), i18nc("@action:inmenu", "Configure Trash…"));
50 m_configureTrashAction->setPriority(QAction::HighPriority);
51 connect(m_configureTrashAction, &QAction::triggered, this, &PlacesPanel::slotConfigureTrash);
52 addAction(m_configureTrashAction);
53
54 connect(this, &PlacesPanel::contextMenuAboutToShow, this, &PlacesPanel::slotContextMenuAboutToShow);
55
56 connect(this, &PlacesPanel::iconSizeChanged, this, [this](const QSize &newSize) {
57 int iconSize = qMin(newSize.width(), newSize.height());
58 if (iconSize == 0) {
59 // Don't store 0 size, let's keep -1 for default/small/automatic
60 iconSize = -1;
61 }
62 PlacesPanelSettings* settings = PlacesPanelSettings::self();
63 settings->setIconSize(iconSize);
64 settings->save();
65 });
66 }
67
68 PlacesPanel::~PlacesPanel() = default;
69
70 void PlacesPanel::setUrl(const QUrl &url)
71 {
72 // KFilePlacesView::setUrl no-ops when no model is set but we only set it in showEvent()
73 // Remember the URL and set it in showEvent
74 m_url = url;
75 KFilePlacesView::setUrl(url);
76 }
77
78 QList<QAction*> PlacesPanel::customContextMenuActions() const
79 {
80 return m_customContextMenuActions;
81 }
82
83 void PlacesPanel::setCustomContextMenuActions(const QList<QAction *> &actions)
84 {
85 m_customContextMenuActions = actions;
86 }
87
88 void PlacesPanel::proceedWithTearDown()
89 {
90 Q_ASSERT(m_deviceToTearDown);
91
92 connect(m_deviceToTearDown, &Solid::StorageAccess::teardownDone,
93 this, &PlacesPanel::slotTearDownDone);
94 m_deviceToTearDown->teardown();
95 }
96
97 void PlacesPanel::readSettings()
98 {
99 if (GeneralSettings::autoExpandFolders()) {
100 setDragAutoActivationDelay(750);
101 } else {
102 setDragAutoActivationDelay(0);
103 }
104
105 const int iconSize = qMax(0, PlacesPanelSettings::iconSize());
106 setIconSize(QSize(iconSize, iconSize));
107 }
108
109 void PlacesPanel::showEvent(QShowEvent* event)
110 {
111 if (!event->spontaneous() && !model()) {
112 readSettings();
113
114 auto *placesModel = DolphinPlacesModelSingleton::instance().placesModel();
115 setModel(placesModel);
116
117 connect(placesModel, &KFilePlacesModel::errorMessage, this, &PlacesPanel::errorMessage);
118
119 connect(placesModel, &QAbstractItemModel::rowsInserted, this, &PlacesPanel::slotRowsInserted);
120 connect(placesModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, &PlacesPanel::slotRowsAboutToBeRemoved);
121
122 for (int i = 0; i < model()->rowCount(); ++i) {
123 connectDeviceSignals(model()->index(i, 0, QModelIndex()));
124 }
125
126 setUrl(m_url);
127 }
128
129 KFilePlacesView::showEvent(event);
130 }
131
132 static bool isInternalDrag(const QMimeData *mimeData)
133 {
134 const auto formats = mimeData->formats();
135 for (const auto &format : formats) {
136 // from KFilePlacesModel::_k_internalMimetype
137 if (format.startsWith(QLatin1String("application/x-kfileplacesmodel-"))) {
138 return true;
139 }
140 }
141 return false;
142 }
143
144 void PlacesPanel::dragMoveEvent(QDragMoveEvent *event)
145 {
146 const QModelIndex index = indexAt(event->pos());
147 if (index.isValid()) {
148 auto *placesModel = static_cast<KFilePlacesModel *>(model());
149
150 // Reject drag ontop of a non-writable protocol
151 // We don't know whether we're dropping inbetween or ontop of a place
152 // so still allow internal drag events so that re-arranging still works.
153 const QUrl url = placesModel->url(index);
154 if (url.isValid() && !isInternalDrag(event->mimeData()) && !KProtocolManager::supportsWriting(url)) {
155 event->setDropAction(Qt::IgnoreAction);
156 }
157 }
158
159 KFilePlacesView::dragMoveEvent(event);
160 }
161
162 void PlacesPanel::slotConfigureTrash()
163 {
164 const QUrl url = currentIndex().data(KFilePlacesModel::UrlRole).toUrl();
165
166 DolphinSettingsDialog* settingsDialog = new DolphinSettingsDialog(url, this);
167 settingsDialog->setCurrentPage(settingsDialog->trashSettings);
168 settingsDialog->setAttribute(Qt::WA_DeleteOnClose);
169 settingsDialog->show();
170 }
171
172 void PlacesPanel::slotUrlsDropped(const QUrl& dest, QDropEvent* event, QWidget* parent)
173 {
174 KIO::DropJob *job = DragAndDropHelper::dropUrls(dest, event, parent);
175 if (job) {
176 connect(job, &KIO::DropJob::result, this, [this](KJob *job) { if (job->error()) Q_EMIT errorMessage(job->errorString()); });
177 }
178 }
179
180 void PlacesPanel::slotContextMenuAboutToShow(const QModelIndex &index, QMenu *menu)
181 {
182 Q_UNUSED(menu);
183
184 auto *placesModel = static_cast<KFilePlacesModel *>(model());
185 const QUrl url = placesModel->url(index);
186 const Solid::Device device = placesModel->deviceForIndex(index);
187
188 m_configureTrashAction->setVisible(url.scheme() == QLatin1String("trash"));
189
190 // show customContextMenuActions only on the view's context menu
191 if (!url.isValid() && !device.isValid()) {
192 addActions(m_customContextMenuActions);
193 } else {
194 const auto actions = this->actions();
195 for (QAction *action : actions) {
196 if (m_customContextMenuActions.contains(action)) {
197 removeAction(action);
198 }
199 }
200 }
201 }
202
203 void PlacesPanel::slotTearDownRequested(const QModelIndex &index)
204 {
205 auto *placesModel = static_cast<KFilePlacesModel *>(model());
206
207 Solid::StorageAccess *storageAccess = placesModel->deviceForIndex(index).as<Solid::StorageAccess>();
208 if (!storageAccess) {
209 return;
210 }
211
212 m_deviceToTearDown = storageAccess;
213
214 // disconnect the Solid::StorageAccess::teardownRequested
215 // to prevent emitting PlacesPanel::storageTearDownExternallyRequested
216 // after we have emitted PlacesPanel::storageTearDownRequested
217 disconnect(storageAccess, &Solid::StorageAccess::teardownRequested, this, &PlacesPanel::slotTearDownRequestedExternally);
218 Q_EMIT storageTearDownRequested(storageAccess->filePath());
219 }
220
221 void PlacesPanel::slotTearDownRequestedExternally(const QString &udi)
222 {
223 Q_UNUSED(udi);
224 auto *storageAccess = static_cast<Solid::StorageAccess*>(sender());
225
226 Q_EMIT storageTearDownExternallyRequested(storageAccess->filePath());
227 }
228
229 void PlacesPanel::slotTearDownDone(Solid::ErrorType error, const QVariant& errorData)
230 {
231 if (error && errorData.isValid()) {
232 if (error == Solid::ErrorType::DeviceBusy) {
233 KListOpenFilesJob* listOpenFilesJob = new KListOpenFilesJob(m_deviceToTearDown->filePath());
234 connect(listOpenFilesJob, &KIO::Job::result, this, [this, listOpenFilesJob](KJob*) {
235 const KProcessList::KProcessInfoList blockingProcesses = listOpenFilesJob->processInfoList();
236 QString errorString;
237 if (blockingProcesses.isEmpty()) {
238 errorString = i18n("One or more files on this device are open within an application.");
239 } else {
240 QStringList blockingApps;
241 for (const auto& process : blockingProcesses) {
242 blockingApps << process.name();
243 }
244 blockingApps.removeDuplicates();
245 errorString = xi18np("One or more files on this device are opened in application <application>\"%2\"</application>.",
246 "One or more files on this device are opened in following applications: <application>%2</application>.",
247 blockingApps.count(), blockingApps.join(i18nc("separator in list of apps blocking device unmount", ", ")));
248 }
249 Q_EMIT errorMessage(errorString);
250 });
251 listOpenFilesJob->start();
252 } else {
253 Q_EMIT errorMessage(errorData.toString());
254 }
255 } else {
256 // No error; it must have been unmounted successfully
257 Q_EMIT storageTearDownSuccessful();
258 }
259 disconnect(m_deviceToTearDown, &Solid::StorageAccess::teardownDone,
260 this, &PlacesPanel::slotTearDownDone);
261 m_deviceToTearDown = nullptr;
262 }
263
264 void PlacesPanel::slotRowsInserted(const QModelIndex &parent, int first, int last)
265 {
266 for (int i = first; i <= last; ++i) {
267 connectDeviceSignals(model()->index(first, 0, parent));
268 }
269 }
270
271 void PlacesPanel::slotRowsAboutToBeRemoved(const QModelIndex &parent, int first, int last)
272 {
273 auto *placesModel = static_cast<KFilePlacesModel *>(model());
274
275 for (int i = first; i <= last; ++i) {
276 const QModelIndex index = placesModel->index(i, 0, parent);
277
278 Solid::StorageAccess *storageAccess = placesModel->deviceForIndex(index).as<Solid::StorageAccess>();
279 if (!storageAccess) {
280 continue;
281 }
282
283 disconnect(storageAccess, &Solid::StorageAccess::teardownRequested, this, nullptr);
284 }
285 }
286
287 void PlacesPanel::connectDeviceSignals(const QModelIndex &index)
288 {
289 auto *placesModel = static_cast<KFilePlacesModel *>(model());
290
291 Solid::StorageAccess *storageAccess = placesModel->deviceForIndex(index).as<Solid::StorageAccess>();
292 if (!storageAccess) {
293 return;
294 }
295
296 connect(storageAccess, &Solid::StorageAccess::teardownRequested, this, &PlacesPanel::slotTearDownRequestedExternally);
297 }