]> cloud.milkyroute.net Git - dolphin.git/blob - src/selectionmode/selectionmodebottombar.cpp
Add Selection Mode
[dolphin.git] / src / selectionmode / selectionmodebottombar.cpp
1 /*
2 This file is part of the KDE project
3 SPDX-FileCopyrightText: 2022 Felix Ernst <fe.a.ernst@gmail.com>
4
5 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
6 */
7
8 #include "selectionmodebottombar.h"
9
10 #include "backgroundcolorhelper.h"
11 #include "dolphin_generalsettings.h"
12 #include "dolphincontextmenu.h"
13 #include "dolphinmainwindow.h"
14 #include "dolphinremoveaction.h"
15 #include "global.h"
16 #include "kitemviews/kfileitemlisttostring.h"
17
18 #include <KActionCollection>
19 #include <KColorScheme>
20 #include <KFileItem>
21 #include <KFileItemListProperties>
22 #include <KLocalizedString>
23 #include <KStandardAction>
24
25 #include <QFontMetrics>
26 #include <QGuiApplication>
27 #include <QHBoxLayout>
28 #include <QLabel>
29 #include <QLayout>
30 #include <QMenu>
31 #include <QPushButton>
32 #include <QResizeEvent>
33 #include <QScrollArea>
34 #include <QStyle>
35 #include <QToolButton>
36 #include <QtGlobal>
37 #include <QVBoxLayout>
38
39 #include <unordered_set>
40 #include <iostream>
41 SelectionModeBottomBar::SelectionModeBottomBar(KActionCollection *actionCollection, QWidget *parent) :
42 QWidget{parent},
43 m_actionCollection{actionCollection}
44 {
45 // Showing of this widget is normally animated. We hide it for now and make it small.
46 hide();
47 setMaximumHeight(0);
48
49 setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed);
50 setMinimumWidth(0);
51
52 auto fillParentLayout = new QGridLayout(this);
53 fillParentLayout->setContentsMargins(0, 0, 0, 0);
54
55 // Put the contents into a QScrollArea. This prevents increasing the view width
56 // in case that not enough width for the contents is available. (this trick is also used in dolphinsearchbox.cpp.)
57 auto scrollArea = new QScrollArea(this);
58 fillParentLayout->addWidget(scrollArea);
59 scrollArea->setFrameShape(QFrame::NoFrame);
60 scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
61 scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
62 scrollArea->setWidgetResizable(true);
63
64 auto contentsContainer = new QWidget(scrollArea);
65 scrollArea->setWidget(contentsContainer);
66 contentsContainer->installEventFilter(this); // Adjusts the height of this bar to the height of the contentsContainer
67
68 BackgroundColorHelper::instance()->controlBackgroundColor(this);
69
70 // We will mostly interact with m_layout when changing the contents and not care about the other internal hierarchy.
71 m_layout = new QHBoxLayout(contentsContainer);
72 }
73
74 void SelectionModeBottomBar::setVisible(bool visible, Animated animated)
75 {
76 Q_ASSERT_X(animated == WithAnimation, "SelectionModeBottomBar::setVisible", "This wasn't implemented.");
77
78 if (!visible && m_contents == PasteContents) {
79 return; // The bar with PasteContents should not be hidden or users might not know how to paste what they just copied.
80 // Set m_contents to anything else to circumvent this prevention mechanism.
81 }
82
83 if (!m_heightAnimation) {
84 m_heightAnimation = new QPropertyAnimation(this, "maximumHeight");
85 }
86 disconnect(m_heightAnimation, &QAbstractAnimation::finished,
87 this, nullptr);
88 m_heightAnimation->setDuration(2 *
89 style()->styleHint(QStyle::SH_Widget_Animation_Duration, nullptr, this) *
90 GlobalConfig::animationDurationFactor());
91
92 if (visible) {
93 show();
94 m_heightAnimation->setStartValue(0);
95 m_heightAnimation->setEndValue(sizeHint().height());
96 m_heightAnimation->setEasingCurve(QEasingCurve::OutCubic);
97 connect(m_heightAnimation, &QAbstractAnimation::finished,
98 this, [this](){ setMaximumHeight(sizeHint().height()); });
99 } else {
100 m_heightAnimation->setStartValue(height());
101 m_heightAnimation->setEndValue(0);
102 m_heightAnimation->setEasingCurve(QEasingCurve::OutCubic);
103 connect(m_heightAnimation, &QAbstractAnimation::finished,
104 this, &QWidget::hide);
105 }
106
107 m_heightAnimation->start();
108 }
109
110 QSize SelectionModeBottomBar::sizeHint() const
111 {
112 // 1 as width because this widget should never be the reason the DolphinViewContainer is made wider.
113 return QSize{1, m_layout->parentWidget()->sizeHint().height()};
114 }
115
116 void SelectionModeBottomBar::slotSelectionChanged(const KFileItemList &selection, const QUrl &baseUrl)
117 {
118 if (m_contents == GeneralContents) {
119 auto contextActions = contextActionsFor(selection, baseUrl);
120 m_generalBarActions.clear();
121 for (auto i = contextActions.begin(); i != contextActions.end(); ++i) {
122 m_generalBarActions.emplace_back(ActionWithWidget{*i});
123 }
124 resetContents(GeneralContents);
125 }
126 updateMainActionButton(selection);
127 }
128
129 void SelectionModeBottomBar::slotSplitTabDisabled()
130 {
131 switch (m_contents) {
132 case CopyToOtherViewContents:
133 case MoveToOtherViewContents:
134 Q_EMIT leaveSelectionModeRequested();
135 default:
136 return;
137 }
138 }
139
140 void SelectionModeBottomBar::resetContents(SelectionModeBottomBar::Contents contents)
141 {
142 emptyBarContents();
143
144 // A label is added in many of the methods below. We only know its size a bit later and if it should be hidden.
145 QTimer::singleShot(10, this, [this](){ updateExplanatoryLabelVisibility(); });
146
147 Q_CHECK_PTR(m_actionCollection);
148 m_contents = contents;
149 switch (contents) {
150 case CopyContents:
151 return addCopyContents();
152 case CopyLocationContents:
153 return addCopyLocationContents();
154 case CopyToOtherViewContents:
155 return addCopyToOtherViewContents();
156 case CutContents:
157 return addCutContents();
158 case DeleteContents:
159 return addDeleteContents();
160 case DuplicateContents:
161 return addDuplicateContents();
162 case GeneralContents:
163 return addGeneralContents();
164 case PasteContents:
165 return addPasteContents();
166 case MoveToOtherViewContents:
167 return addMoveToOtherViewContents();
168 case MoveToTrashContents:
169 return addMoveToTrashContents();
170 case RenameContents:
171 return addRenameContents();
172 }
173 }
174
175 bool SelectionModeBottomBar::eventFilter(QObject *watched, QEvent *event)
176 {
177 Q_ASSERT(qobject_cast<QWidget *>(watched)); // This evenfFilter is only implemented for QWidgets.
178
179 switch (event->type()) {
180 case QEvent::ChildAdded:
181 case QEvent::ChildRemoved:
182 QTimer::singleShot(0, this, [this](){ setMaximumHeight(sizeHint().height()); });
183 // Fall through.
184 default:
185 return false;
186 }
187 }
188
189 void SelectionModeBottomBar::resizeEvent(QResizeEvent *resizeEvent)
190 {
191 if (resizeEvent->oldSize().width() == resizeEvent->size().width()) {
192 // The width() didn't change so our custom override isn't needed.
193 return QWidget::resizeEvent(resizeEvent);
194 }
195 m_layout->parentWidget()->setFixedWidth(resizeEvent->size().width());
196
197 if (m_contents == GeneralContents) {
198 Q_ASSERT(m_overflowButton);
199 if (unusedSpace() < 0) {
200 // The bottom bar is overflowing! We need to hide some of the widgets.
201 for (auto i = m_generalBarActions.rbegin(); i != m_generalBarActions.rend(); ++i) {
202 if (!i->isWidgetVisible()) {
203 continue;
204 }
205 i->widget()->setVisible(false);
206
207 // Add the action to the overflow.
208 std::cout << "An Action is added to the m_overflowButton because of a resize: " << qPrintable(i->action()->text()) << "\n";
209 auto overflowMenu = m_overflowButton->menu();
210 if (overflowMenu->actions().isEmpty()) {
211 overflowMenu->addAction(i->action());
212 } else {
213 overflowMenu->insertAction(overflowMenu->actions().at(0), i->action());
214 }
215 std::cout << "The number of actions in the menu is now " << m_overflowButton->menu()->actions().count() << "\n.";
216 m_overflowButton->setVisible(true);
217 if (unusedSpace() >= 0) {
218 break; // All widgets fit now.
219 }
220 }
221 } else {
222 // We have some unusedSpace(). Let's check if we can maybe add more of the contextual action's widgets.
223 for (auto i = m_generalBarActions.begin(); i != m_generalBarActions.end(); ++i) {
224 if (i->isWidgetVisible()) {
225 continue;
226 }
227 if (!i->widget()) {
228 i->newWidget(this);
229 i->widget()->setVisible(false);
230 m_layout->insertWidget(m_layout->count() - 1, i->widget()); // Insert before m_overflowButton
231 }
232 if (unusedSpace() < i->widget()->sizeHint().width()) {
233 // It doesn't fit. We keep it invisible.
234 break;
235 }
236 i->widget()->setVisible(true);
237
238 // Remove the action from the overflow.
239 std::cout << "An Action is removed from the m_overflowButton because of a resize: " << qPrintable(i->action()->text()) << "\n";
240 auto overflowMenu = m_overflowButton->menu();
241 overflowMenu->removeAction(i->action());
242 std::cout << "The number of actions in the menu is now " << m_overflowButton->menu()->actions().count() << "\n.";
243 if (overflowMenu->isEmpty()) {
244 m_overflowButton->setVisible(false);
245 }
246 }
247 }
248 }
249
250 // Hide the leading explanation if it doesn't fit. The buttons are labeled clear enough that this shouldn't be a big UX problem.
251 updateExplanatoryLabelVisibility();
252 return QWidget::resizeEvent(resizeEvent);
253 }
254
255 void SelectionModeBottomBar::addCopyContents()
256 {
257 m_explanatoryLabel = new QLabel(i18nc("@info explaining the next step in a process", "Select the files and folders that should be copied."), this);
258 m_explanatoryLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
259 m_explanatoryLabel->setWordWrap(true);
260 m_layout->addWidget(m_explanatoryLabel);
261
262 // i18n: Aborts the current step-by-step process to copy files by leaving the selection mode.
263 auto *cancelButton = new QPushButton(i18nc("@action:button", "Abort Copying"), this);
264 connect(cancelButton, &QAbstractButton::clicked, this, &SelectionModeBottomBar::leaveSelectionModeRequested);
265 m_layout->addWidget(cancelButton);
266
267 auto *copyButton = new QPushButton(this);
268 // We claim to have PasteContents already so triggering the copy action next won't instantly hide the bottom bar.
269 connect(copyButton, &QAbstractButton::clicked, [this]() {
270 if (GeneralSettings::showPasteBarAfterCopying()) {
271 m_contents = Contents::PasteContents;
272 }
273 });
274 // Connect the copy action as a second step.
275 m_mainAction = ActionWithWidget(m_actionCollection->action(KStandardAction::name(KStandardAction::Copy)), copyButton);
276 // Finally connect the lambda that actually changes the contents to the PasteContents.
277 connect(copyButton, &QAbstractButton::clicked, [this]() {
278 if (GeneralSettings::showPasteBarAfterCopying()) {
279 resetContents(Contents::PasteContents); // resetContents() needs to be connected last because
280 // it instantly deletes the button and then the other slots won't be called.
281 }
282 Q_EMIT leaveSelectionModeRequested();
283 });
284 updateMainActionButton(KFileItemList());
285 m_layout->addWidget(copyButton);
286 }
287
288 void SelectionModeBottomBar::addCopyLocationContents()
289 {
290 m_explanatoryLabel = new QLabel(i18nc("@info explaining the next step in a process", "Select one file or folder whose location should be copied."), this);
291 m_explanatoryLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
292 m_explanatoryLabel->setWordWrap(true);
293 m_layout->addWidget(m_explanatoryLabel);
294
295 // i18n: Aborts the current step-by-step process to copy the location of files by leaving the selection mode.
296 auto *cancelButton = new QPushButton(i18nc("@action:button", "Abort Copying"), this);
297 connect(cancelButton, &QAbstractButton::clicked, this, &SelectionModeBottomBar::leaveSelectionModeRequested);
298 m_layout->addWidget(cancelButton);
299
300 auto *copyLocationButton = new QPushButton(this);
301 m_mainAction = ActionWithWidget(m_actionCollection->action(QStringLiteral("copy_location")), copyLocationButton);
302 updateMainActionButton(KFileItemList());
303 m_layout->addWidget(copyLocationButton);
304 }
305
306 void SelectionModeBottomBar::addCopyToOtherViewContents()
307 {
308 // i18n: "Copy over" refers to copying to the other split view area that is currently visible to the user.
309 m_explanatoryLabel = new QLabel(i18nc("@info explaining the next step in a process", "Select the files and folders that should be copied over."), this);
310 m_explanatoryLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
311 m_explanatoryLabel->setWordWrap(true);
312 m_layout->addWidget(m_explanatoryLabel);
313
314 // i18n: Aborts the current step-by-step process to copy the location of files by leaving the selection mode.
315 auto *cancelButton = new QPushButton(i18nc("@action:button", "Abort Copying"), this);
316 connect(cancelButton, &QAbstractButton::clicked, this, &SelectionModeBottomBar::leaveSelectionModeRequested);
317 m_layout->addWidget(cancelButton);
318
319 auto *copyToOtherViewButton = new QPushButton(this);
320 m_mainAction = ActionWithWidget(m_actionCollection->action(QStringLiteral("copy_to_inactive_split_view")), copyToOtherViewButton);
321 updateMainActionButton(KFileItemList());
322 m_layout->addWidget(copyToOtherViewButton);
323 }
324
325 void SelectionModeBottomBar::addCutContents()
326 {
327 m_explanatoryLabel = new QLabel(i18nc("@info explaining the next step in a process", "Select the files and folders that should be cut."), this);
328 m_explanatoryLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
329 m_explanatoryLabel->setWordWrap(true);
330 m_layout->addWidget(m_explanatoryLabel);
331
332 // i18n: Aborts the current step-by-step process to cut files by leaving the selection mode.
333 auto *cancelButton = new QPushButton(i18nc("@action:button", "Abort Cutting"), this);
334 connect(cancelButton, &QAbstractButton::clicked, this, &SelectionModeBottomBar::leaveSelectionModeRequested);
335 m_layout->addWidget(cancelButton);
336
337 auto *cutButton = new QPushButton(this);
338 // We claim to have PasteContents already so triggering the cut action next won't instantly hide the bottom bar.
339 connect(cutButton, &QAbstractButton::clicked, [this]() {
340 if (GeneralSettings::showPasteBarAfterCopying()) {
341 m_contents = Contents::PasteContents;
342 }
343 });
344 // Connect the cut action as a second step.
345 m_mainAction = ActionWithWidget(m_actionCollection->action(KStandardAction::name(KStandardAction::Cut)), cutButton);
346 // Finally connect the lambda that actually changes the contents to the PasteContents.
347 connect(cutButton, &QAbstractButton::clicked, [this](){
348 if (GeneralSettings::showPasteBarAfterCopying()) {
349 resetContents(Contents::PasteContents); // resetContents() needs to be connected last because
350 // it instantly deletes the button and then the other slots won't be called.
351 }
352 Q_EMIT leaveSelectionModeRequested();
353 });
354 updateMainActionButton(KFileItemList());
355 m_layout->addWidget(cutButton);
356 }
357
358 void SelectionModeBottomBar::addDeleteContents()
359 {
360 m_explanatoryLabel = new QLabel(i18nc("@info explaining the next step in a process", "Select the files and folders that should be permanently deleted."), this);
361 m_explanatoryLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
362 m_explanatoryLabel->setWordWrap(true);
363 m_layout->addWidget(m_explanatoryLabel);
364
365 // i18n: Aborts the current step-by-step process to delete files by leaving the selection mode.
366 auto *cancelButton = new QPushButton(i18nc("@action:button", "Abort"), this);
367 connect(cancelButton, &QAbstractButton::clicked, this, &SelectionModeBottomBar::leaveSelectionModeRequested);
368 m_layout->addWidget(cancelButton);
369
370 auto *deleteButton = new QPushButton(this);
371 m_mainAction = ActionWithWidget(m_actionCollection->action(KStandardAction::name(KStandardAction::DeleteFile)), deleteButton);
372 updateMainActionButton(KFileItemList());
373 m_layout->addWidget(deleteButton);
374 }
375
376 void SelectionModeBottomBar::addDuplicateContents()
377 {
378 m_explanatoryLabel = new QLabel(i18nc("@info explaining the next step in a process", "Select the files and folders that should be duplicated here."), this);
379 m_explanatoryLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
380 m_explanatoryLabel->setWordWrap(true);
381 m_layout->addWidget(m_explanatoryLabel);
382
383 // i18n: Aborts the current step-by-step process to duplicate files by leaving the selection mode.
384 auto *cancelButton = new QPushButton(i18nc("@action:button", "Abort Duplicating"), this);
385 connect(cancelButton, &QAbstractButton::clicked, this, &SelectionModeBottomBar::leaveSelectionModeRequested);
386 m_layout->addWidget(cancelButton);
387
388 auto *duplicateButton = new QPushButton(this);
389 m_mainAction = ActionWithWidget(m_actionCollection->action(QStringLiteral("duplicate")), duplicateButton);
390 updateMainActionButton(KFileItemList());
391 m_layout->addWidget(duplicateButton);
392 }
393
394 void SelectionModeBottomBar::addGeneralContents()
395 {
396 if (!m_overflowButton) {
397 m_overflowButton = new QToolButton{this};
398 // i18n: This button appears in a bar if there isn't enough horizontal space to fit all the other buttons.
399 // The small icon-only button opens a menu that contains the actions that didn't fit on the bar.
400 // Since this is an icon-only button this text will only appear as a tooltip and as accessibility text.
401 m_overflowButton->setToolTip(i18nc("@action", "More"));
402 m_overflowButton->setAccessibleName(m_overflowButton->toolTip());
403 m_overflowButton->setIcon(QIcon::fromTheme(QStringLiteral("view-more-horizontal-symbolic")));
404 m_overflowButton->setMenu(new QMenu{m_overflowButton});
405 m_overflowButton->setPopupMode(QToolButton::ToolButtonPopupMode::InstantPopup);
406 m_overflowButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::MinimumExpanding); // Makes sure it has the same height as the labeled buttons.
407 m_layout->addWidget(m_overflowButton);
408 } else {
409 m_overflowButton->menu()->actions().clear();
410 // The overflowButton should be part of the calculation for needed space so we set it visible in regards to unusedSpace().
411 m_overflowButton->setVisible(true);
412 }
413
414 // We first add all the m_generalBarActions to the bar until the bar is full.
415 auto i = m_generalBarActions.begin();
416 for (; i != m_generalBarActions.end(); ++i) {
417 if (i->action()->isVisible()) {
418 if (i->widget()) {
419 i->widget()->setEnabled(i->action()->isEnabled());
420 } else {
421 i->newWidget(this);
422 i->widget()->setVisible(false);
423 m_layout->insertWidget(m_layout->count() - 1, i->widget()); // Insert before m_overflowButton
424 }
425 if (unusedSpace() < i->widget()->sizeHint().width()) {
426 std::cout << "The " << unusedSpace() << " is smaller than the button->sizeHint().width() of " << i->widget()->sizeHint().width() << " plus the m_layout->spacing() of " << m_layout->spacing() << " so the action " << qPrintable(i->action()->text()) << " doesn't get its own button.\n";
427 break; // The bar is too full already. We keep it invisible.
428 } else {
429 std::cout << "The " << unusedSpace() << " is bigger than the button->sizeHint().width() of " << i->widget()->sizeHint().width() << " plus the m_layout->spacing() of " << m_layout->spacing() << " so the action " << qPrintable(i->action()->text()) << " was added as its own button/widget.\n";
430 i->widget()->setVisible(true);
431 }
432 }
433 }
434 // We are done adding widgets to the bar so either we were able to fit all the actions in there
435 m_overflowButton->setVisible(false);
436 // …or there are more actions left which need to be put into m_overflowButton.
437 for (; i != m_generalBarActions.end(); ++i) {
438 m_overflowButton->menu()->addAction(i->action());
439
440 // The overflowButton is set visible if there is actually an action in it.
441 if (!m_overflowButton->isVisible() && i->action()->isVisible() && !i->action()->isSeparator()) {
442 m_overflowButton->setVisible(true);
443 }
444 }
445 }
446
447 void SelectionModeBottomBar::addMoveToOtherViewContents()
448 {
449 // i18n: "Move over" refers to moving to the other split view area that is currently visible to the user.
450 m_explanatoryLabel = new QLabel(i18nc("@info explaining the next step in a process", "Select the files and folders that should be moved over."), this);
451 m_explanatoryLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
452 m_explanatoryLabel->setWordWrap(true);
453 m_layout->addWidget(m_explanatoryLabel);
454
455 // i18n: Aborts the current step-by-step process to copy the location of files by leaving the selection mode.
456 auto *cancelButton = new QPushButton(i18nc("@action:button", "Abort Moving"), this);
457 connect(cancelButton, &QAbstractButton::clicked, this, &SelectionModeBottomBar::leaveSelectionModeRequested);
458 m_layout->addWidget(cancelButton);
459
460 auto *moveToOtherViewButton = new QPushButton(this);
461 m_mainAction = ActionWithWidget(m_actionCollection->action(QStringLiteral("move_to_inactive_split_view")), moveToOtherViewButton);
462 updateMainActionButton(KFileItemList());
463 m_layout->addWidget(moveToOtherViewButton);
464 }
465
466 void SelectionModeBottomBar::addMoveToTrashContents()
467 {
468 m_explanatoryLabel = new QLabel(i18nc("@info explaining the next step in a process", "Select the files and folders that should be moved to the Trash."), this);
469 m_explanatoryLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
470 m_explanatoryLabel->setWordWrap(true);
471 m_layout->addWidget(m_explanatoryLabel);
472
473 // i18n: Aborts the current step-by-step process of moving files to the trash by leaving the selection mode.
474 auto *cancelButton = new QPushButton(i18nc("@action:button", "Abort"), this);
475 connect(cancelButton, &QAbstractButton::clicked, this, &SelectionModeBottomBar::leaveSelectionModeRequested);
476 m_layout->addWidget(cancelButton);
477
478 auto *moveToTrashButton = new QPushButton(this);
479 m_mainAction = ActionWithWidget(m_actionCollection->action(KStandardAction::name(KStandardAction::MoveToTrash)), moveToTrashButton);
480 updateMainActionButton(KFileItemList());
481 m_layout->addWidget(moveToTrashButton);
482 }
483
484 void SelectionModeBottomBar::addPasteContents()
485 {
486 m_explanatoryLabel = new QLabel(xi18n("<para>The selected files and folders were added to the Clipboard. "
487 "Now the <emphasis>Paste</emphasis> action can be used to transfer them from the Clipboard "
488 "to any other location. They can even be transferred to other applications by using their "
489 "respective <emphasis>Paste</emphasis> actions.</para>"), this);
490 m_explanatoryLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
491 m_explanatoryLabel->setWordWrap(true);
492 m_layout->addWidget(m_explanatoryLabel);
493
494 auto *vBoxLayout = new QVBoxLayout(this);
495 m_layout->addLayout(vBoxLayout);
496
497 /** We are in "PasteContents" mode which means hiding the bottom bar is impossible.
498 * So we first have to claim that we have different contents before requesting to leave selection mode. */
499 auto actuallyLeaveSelectionMode = [this]() {
500 m_contents = Contents::CopyLocationContents;
501 Q_EMIT leaveSelectionModeRequested();
502 };
503
504 auto *pasteButton = new QPushButton(this);
505 copyActionDataToButton(pasteButton, m_actionCollection->action(KStandardAction::name(KStandardAction::Paste)));
506 pasteButton->setText(i18nc("@action A more elaborate and clearly worded version of the Paste action", "Paste from Clipboard"));
507 connect(pasteButton, &QAbstractButton::clicked, this, actuallyLeaveSelectionMode);
508 vBoxLayout->addWidget(pasteButton);
509
510 auto *dismissButton = new QToolButton(this);
511 dismissButton->setText(i18nc("@action Dismisses a bar explaining how to use the Paste action", "Dismiss this Reminder"));
512 connect(dismissButton, &QAbstractButton::clicked, this, actuallyLeaveSelectionMode);
513 auto *dontRemindAgainAction = new QAction(i18nc("@action Dismisses an explanatory area and never shows it again", "Don't remind me again"), this);
514 connect(dontRemindAgainAction, &QAction::triggered, this, []() {
515 GeneralSettings::setShowPasteBarAfterCopying(false);
516 });
517 connect(dontRemindAgainAction, &QAction::triggered, this, actuallyLeaveSelectionMode);
518 auto *dismissButtonMenu = new QMenu(dismissButton);
519 dismissButtonMenu->addAction(dontRemindAgainAction);
520 dismissButton->setMenu(dismissButtonMenu);
521 dismissButton->setPopupMode(QToolButton::MenuButtonPopup);
522 vBoxLayout->addWidget(dismissButton);
523
524 m_explanatoryLabel->setMaximumHeight(pasteButton->sizeHint().height() + dismissButton->sizeHint().height() + m_explanatoryLabel->fontMetrics().height());
525 }
526
527 void SelectionModeBottomBar::addRenameContents()
528 {
529 m_explanatoryLabel = new QLabel(i18nc("@info explains the next step in a process", "Select the file or folder that should be renamed.\nBulk renaming is possible when multiple items are selected."), this);
530 m_explanatoryLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
531 m_explanatoryLabel->setWordWrap(true);
532 m_layout->addWidget(m_explanatoryLabel);
533
534 // i18n: Aborts the current step-by-step process to delete files by leaving the selection mode.
535 auto *cancelButton = new QPushButton(i18nc("@action:button", "Stop Renaming"), this);
536 connect(cancelButton, &QAbstractButton::clicked, this, &SelectionModeBottomBar::leaveSelectionModeRequested);
537 m_layout->addWidget(cancelButton);
538
539 auto *renameButton = new QPushButton(this);
540 m_mainAction = ActionWithWidget(m_actionCollection->action(KStandardAction::name(KStandardAction::RenameFile)), renameButton);
541 updateMainActionButton(KFileItemList());
542 m_layout->addWidget(renameButton);
543 }
544
545 void SelectionModeBottomBar::emptyBarContents()
546 {
547 QLayoutItem *child;
548 while ((child = m_layout->takeAt(0)) != nullptr) {
549 if (auto *childLayout = child->layout()) {
550 QLayoutItem *grandChild;
551 while ((grandChild = childLayout->takeAt(0)) != nullptr) {
552 delete grandChild->widget(); // delete the widget
553 delete grandChild; // delete the layout item
554 }
555 }
556 delete child->widget(); // delete the widget
557 delete child; // delete the layout item
558 }
559 }
560
561 std::vector<QAction *> SelectionModeBottomBar::contextActionsFor(const KFileItemList& selectedItems, const QUrl& baseUrl)
562 {
563 std::vector<QAction *> contextActions;
564 contextActions.emplace_back(m_actionCollection->action(KStandardAction::name(KStandardAction::Copy)));
565 contextActions.emplace_back(m_actionCollection->action(KStandardAction::name(KStandardAction::Cut)));
566 contextActions.emplace_back(m_actionCollection->action(KStandardAction::name(KStandardAction::RenameFile)));
567 contextActions.emplace_back(m_actionCollection->action(KStandardAction::name(KStandardAction::MoveToTrash)));
568
569 if (!selectedItems.isEmpty()) {
570 // We are going to add the actions from the right-click context menu for the selected items.
571 auto *dolphinMainWindow = qobject_cast<DolphinMainWindow *>(window());
572 Q_CHECK_PTR(dolphinMainWindow);
573 if (!m_fileItemActions) {
574 m_fileItemActions = new KFileItemActions(this);
575 m_fileItemActions->setParentWidget(dolphinMainWindow);
576 connect(m_fileItemActions, &KFileItemActions::error, this, &SelectionModeBottomBar::error);
577 }
578 m_internalContextMenu = std::make_unique<DolphinContextMenu>(dolphinMainWindow, selectedItems.constFirst(), selectedItems, baseUrl, m_fileItemActions);
579 auto internalContextMenuActions = m_internalContextMenu->actions();
580
581 // There are some actions which we wouldn't want to add. We remember them in the actionsThatShouldntBeAdded set.
582 // We don't want to add the four basic actions again which were already added to the top.
583 std::unordered_set<QAction *> actionsThatShouldntBeAdded{contextActions.begin(), contextActions.end()};
584 // "Delete" isn't really necessary to add because we have "Move to Trash" already. It is also more dangerous so let's exclude it.
585 actionsThatShouldntBeAdded.insert(m_actionCollection->action(KStandardAction::name(KStandardAction::DeleteFile)));
586 // "Open Terminal" isn't really context dependent and can therefore be opened from elsewhere instead.
587 actionsThatShouldntBeAdded.insert(m_actionCollection->action(QStringLiteral("open_terminal")));
588
589 // KHamburgerMenu would only be visible if there is no menu available anywhere on the user interface. This might be useful for recovery from
590 // such a situation in theory but a bar with context dependent actions doesn't really seem like the right place for it.
591 Q_ASSERT(internalContextMenuActions.first()->icon().name() == m_actionCollection->action(KStandardAction::name(KStandardAction::HamburgerMenu))->icon().name());
592 internalContextMenuActions.removeFirst();
593
594 for (auto it = internalContextMenuActions.constBegin(); it != internalContextMenuActions.constEnd(); ++it) {
595 if (actionsThatShouldntBeAdded.count(*it)) {
596 continue; // Skip this action.
597 }
598 if (!qobject_cast<DolphinRemoveAction *>(*it)) { // We already have a "Move to Trash" action so we don't want a DolphinRemoveAction.
599 // We filter duplicate separators here so we won't have to deal with them later.
600 if (!contextActions.back()->isSeparator() || !(*it)->isSeparator()) {
601 contextActions.emplace_back((*it));
602 }
603 }
604 }
605 }
606 return contextActions;
607 }
608
609 int SelectionModeBottomBar::unusedSpace() const
610 {
611 int sumOfPreferredWidths = m_layout->contentsMargins().left() + m_layout->contentsMargins().right();
612 if (m_overflowButton) {
613 sumOfPreferredWidths += m_overflowButton->sizeHint().width();
614 }
615 std::cout << "These layout items should have sane width: ";
616 for (int i = 0; i < m_layout->count(); ++i) {
617 auto widget = m_layout->itemAt(i)->widget();
618 if (widget && !widget->isVisibleTo(widget->parentWidget())) {
619 continue; // We don't count invisible widgets.
620 }
621 std::cout << m_layout->itemAt(i)->sizeHint().width() << ", ";
622 if (m_layout->itemAt(i)->sizeHint().width() == 0) {
623 // One of the items reports an invalid width. We can't work with this so we report an unused space of 0 which should lead to as few changes to the
624 // layout as possible until the next resize event happens at a later point in time.
625 //return 0;
626 }
627 sumOfPreferredWidths += m_layout->itemAt(i)->sizeHint().width() + m_layout->spacing();
628 }
629 std::cout << "leads to unusedSpace = " << width() << " - " << sumOfPreferredWidths - 20 << " = " << width() - sumOfPreferredWidths - 20 << "\n";
630 return width() - sumOfPreferredWidths - 20; // We consider all space used when there are only 20 pixels left
631 // so there is some room to breath and not too much wonkyness while resizing.
632 }
633
634 void SelectionModeBottomBar::updateExplanatoryLabelVisibility()
635 {
636 if (!m_explanatoryLabel) {
637 return;
638 }
639 std::cout << "label minimumSizeHint compared to width() :" << m_explanatoryLabel->sizeHint().width() << "/" << m_explanatoryLabel->width() << "; unusedSpace: " << unusedSpace() << "\n";
640 if (m_explanatoryLabel->isVisible()) {
641 m_explanatoryLabel->setVisible(unusedSpace() > 0);
642 } else {
643 // We only want to re-show the label when it fits comfortably so the computation below adds another "+20".
644 m_explanatoryLabel->setVisible(unusedSpace() > m_explanatoryLabel->sizeHint().width() + 20);
645 }
646 }
647
648 void SelectionModeBottomBar::updateMainActionButton(const KFileItemList& selection)
649 {
650 if (!m_mainAction.widget()) {
651 return;
652 }
653 Q_ASSERT(qobject_cast<QAbstractButton *>(m_mainAction.widget()));
654
655 // Users are nudged towards selecting items by having the button disabled when nothing is selected.
656 m_mainAction.widget()->setEnabled(selection.count() > 0 && m_mainAction.action()->isEnabled());
657 QFontMetrics fontMetrics = m_mainAction.widget()->fontMetrics();
658
659 QString buttonText;
660 switch (m_contents) {
661 case CopyContents:
662 buttonText = i18ncp("@action A more elaborate and clearly worded version of the Copy action",
663 "Copy %2 to the Clipboard", "Copy %2 to the Clipboard", selection.count(),
664 fileItemListToString(selection, fontMetrics.averageCharWidth() * 20, fontMetrics));
665 break;
666 case CopyLocationContents:
667 buttonText = i18ncp("@action A more elaborate and clearly worded version of the Copy Location action",
668 "Copy the Location of %2 to the Clipboard", "Copy the Location of %2 to the Clipboard", selection.count(),
669 fileItemListToString(selection, fontMetrics.averageCharWidth() * 20, fontMetrics));
670 break;
671 case CutContents:
672 buttonText = i18ncp("@action A more elaborate and clearly worded version of the Cut action",
673 "Cut %2 to the Clipboard", "Cut %2 to the Clipboard", selection.count(),
674 fileItemListToString(selection, fontMetrics.averageCharWidth() * 20, fontMetrics));
675 break;
676 case DeleteContents:
677 buttonText = i18ncp("@action A more elaborate and clearly worded version of the Delete action",
678 "Permanently Delete %2", "Permanently Delete %2", selection.count(),
679 fileItemListToString(selection, fontMetrics.averageCharWidth() * 20, fontMetrics));
680 break;
681 case DuplicateContents:
682 buttonText = i18ncp("@action A more elaborate and clearly worded version of the Duplicate action",
683 "Duplicate %2", "Duplicate %2", selection.count(),
684 fileItemListToString(selection, fontMetrics.averageCharWidth() * 20, fontMetrics));
685 break;
686 case MoveToTrashContents:
687 buttonText = i18ncp("@action A more elaborate and clearly worded version of the Trash action",
688 "Move %2 to the Trash", "Move %2 to the Trash", selection.count(),
689 fileItemListToString(selection, fontMetrics.averageCharWidth() * 20, fontMetrics));
690 break;
691 case RenameContents:
692 buttonText = i18ncp("@action A more elaborate and clearly worded version of the Rename action",
693 "Rename %2", "Rename %2", selection.count(),
694 fileItemListToString(selection, fontMetrics.averageCharWidth() * 20, fontMetrics));
695 break;
696 default:
697 return;
698 }
699 if (buttonText != QStringLiteral("NULL")) {
700 static_cast<QAbstractButton *>(m_mainAction.widget())->setText(buttonText);
701
702 // The width of the button has changed. We might want to hide the label so the full button text fits on the bar.
703 updateExplanatoryLabelVisibility();
704 }
705 }