]> cloud.milkyroute.net Git - dolphin.git/blob - src/dolphintabwidget.cpp
Fix item highlighting through DBus
[dolphin.git] / src / dolphintabwidget.cpp
1 /*
2 * SPDX-FileCopyrightText: 2014 Emmanuel Pescosta <emmanuelpescosta099@gmail.com>
3 *
4 * SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
7 #include "dolphintabwidget.h"
8
9 #include "dolphin_generalsettings.h"
10 #include "dolphintabbar.h"
11 #include "dolphinviewcontainer.h"
12
13 #include <KConfigGroup>
14 #include <KShell>
15 #include <kio/global.h>
16 #include <KIO/CommandLauncherJob>
17 #include <KAcceleratorManager>
18
19 #include <QApplication>
20 #include <QDropEvent>
21
22 DolphinTabWidget::DolphinTabWidget(DolphinNavigatorsWidgetAction *navigatorsWidget, QWidget* parent) :
23 QTabWidget(parent),
24 m_lastViewedTab(nullptr),
25 m_navigatorsWidget{navigatorsWidget}
26 {
27 KAcceleratorManager::setNoAccel(this);
28
29 connect(this, &DolphinTabWidget::tabCloseRequested,
30 this, QOverload<int>::of(&DolphinTabWidget::closeTab));
31 connect(this, &DolphinTabWidget::currentChanged,
32 this, &DolphinTabWidget::currentTabChanged);
33
34 DolphinTabBar* tabBar = new DolphinTabBar(this);
35 connect(tabBar, &DolphinTabBar::openNewActivatedTab,
36 this, QOverload<int>::of(&DolphinTabWidget::openNewActivatedTab));
37 connect(tabBar, &DolphinTabBar::tabDropEvent,
38 this, &DolphinTabWidget::tabDropEvent);
39 connect(tabBar, &DolphinTabBar::tabDetachRequested,
40 this, &DolphinTabWidget::detachTab);
41 tabBar->hide();
42
43 setTabBar(tabBar);
44 setDocumentMode(true);
45 setElideMode(Qt::ElideRight);
46 setUsesScrollButtons(true);
47 }
48
49 DolphinTabPage* DolphinTabWidget::currentTabPage() const
50 {
51 return tabPageAt(currentIndex());
52 }
53
54 DolphinTabPage* DolphinTabWidget::nextTabPage() const
55 {
56 const int index = currentIndex() + 1;
57 return tabPageAt(index < count() ? index : 0);
58 }
59
60 DolphinTabPage* DolphinTabWidget::prevTabPage() const
61 {
62 const int index = currentIndex() - 1;
63 return tabPageAt(index >= 0 ? index : (count() - 1));
64 }
65
66 DolphinTabPage* DolphinTabWidget::tabPageAt(const int index) const
67 {
68 return static_cast<DolphinTabPage*>(widget(index));
69 }
70
71 void DolphinTabWidget::saveProperties(KConfigGroup& group) const
72 {
73 const int tabCount = count();
74 group.writeEntry("Tab Count", tabCount);
75 group.writeEntry("Active Tab Index", currentIndex());
76
77 for (int i = 0; i < tabCount; ++i) {
78 const DolphinTabPage* tabPage = tabPageAt(i);
79 group.writeEntry("Tab Data " % QString::number(i), tabPage->saveState());
80 }
81 }
82
83 void DolphinTabWidget::readProperties(const KConfigGroup& group)
84 {
85 const int tabCount = group.readEntry("Tab Count", 0);
86 for (int i = 0; i < tabCount; ++i) {
87 if (i >= count()) {
88 openNewActivatedTab();
89 }
90 const QByteArray state = group.readEntry("Tab Data " % QString::number(i), QByteArray());
91 tabPageAt(i)->restoreState(state);
92 }
93
94 const int index = group.readEntry("Active Tab Index", 0);
95 setCurrentIndex(index);
96 }
97
98 void DolphinTabWidget::refreshViews()
99 {
100 // Left-elision is better when showing full paths, since you care most
101 // about the current directory which is on the right
102 if (GeneralSettings::showFullPathInTitlebar()) {
103 setElideMode(Qt::ElideLeft);
104 } else {
105 setElideMode(Qt::ElideRight);
106 }
107
108 const int tabCount = count();
109 for (int i = 0; i < tabCount; ++i) {
110 tabBar()->setTabText(i, tabName(tabPageAt(i)));
111 tabPageAt(i)->refreshViews();
112 }
113 }
114
115 bool DolphinTabWidget::isUrlOpen(const QUrl &url) const
116 {
117 return viewOpenAtDirectory(url).has_value();
118 }
119
120 bool DolphinTabWidget::isItemVisibleInAnyView(const QUrl &urlOfItem) const
121 {
122 return viewShowingItem(urlOfItem).has_value();
123 }
124
125 void DolphinTabWidget::openNewActivatedTab()
126 {
127 std::unique_ptr<DolphinUrlNavigator::VisualState> oldNavigatorState;
128 if (currentTabPage()->primaryViewActive() || !m_navigatorsWidget->secondaryUrlNavigator()) {
129 oldNavigatorState = m_navigatorsWidget->primaryUrlNavigator()->visualState();
130 } else {
131 oldNavigatorState = m_navigatorsWidget->secondaryUrlNavigator()->visualState();
132 }
133
134 const DolphinViewContainer* oldActiveViewContainer = currentTabPage()->activeViewContainer();
135 Q_ASSERT(oldActiveViewContainer);
136
137 openNewActivatedTab(oldActiveViewContainer->url());
138
139 DolphinViewContainer* newActiveViewContainer = currentTabPage()->activeViewContainer();
140 Q_ASSERT(newActiveViewContainer);
141
142 // The URL navigator of the new tab should have the same editable state
143 // as the current tab
144 newActiveViewContainer->urlNavigator()->setVisualState(*oldNavigatorState.get());
145
146 // Always focus the new tab's view
147 newActiveViewContainer->view()->setFocus();
148 }
149
150 void DolphinTabWidget::openNewActivatedTab(const QUrl& primaryUrl, const QUrl& secondaryUrl)
151 {
152 openNewTab(primaryUrl, secondaryUrl);
153 if (GeneralSettings::openNewTabAfterLastTab()) {
154 setCurrentIndex(count() - 1);
155 } else {
156 setCurrentIndex(currentIndex() + 1);
157 }
158 }
159
160 void DolphinTabWidget::openNewTab(const QUrl& primaryUrl, const QUrl& secondaryUrl, DolphinTabWidget::NewTabPosition position)
161 {
162 QWidget* focusWidget = QApplication::focusWidget();
163
164 DolphinTabPage* tabPage = new DolphinTabPage(primaryUrl, secondaryUrl, this);
165 tabPage->setActive(false);
166 connect(tabPage, &DolphinTabPage::activeViewChanged,
167 this, &DolphinTabWidget::activeViewChanged);
168 connect(tabPage, &DolphinTabPage::activeViewUrlChanged,
169 this, &DolphinTabWidget::tabUrlChanged);
170 connect(tabPage->activeViewContainer(), &DolphinViewContainer::captionChanged, this, [this, tabPage]() {
171 const int tabIndex = indexOf(tabPage);
172 Q_ASSERT(tabIndex >= 0);
173 tabBar()->setTabText(tabIndex, tabName(tabPage));
174 });
175
176 if (position == NewTabPosition::FollowSetting) {
177 if (GeneralSettings::openNewTabAfterLastTab()) {
178 position = NewTabPosition::AtEnd;
179 } else {
180 position = NewTabPosition::AfterCurrent;
181 }
182 }
183
184 int newTabIndex = -1;
185 if (position == NewTabPosition::AfterCurrent || (position == NewTabPosition::FollowSetting && !GeneralSettings::openNewTabAfterLastTab())) {
186 newTabIndex = currentIndex() + 1;
187 }
188
189 insertTab(newTabIndex, tabPage, QIcon() /* loaded in tabInserted */, tabName(tabPage));
190
191 if (focusWidget) {
192 // The DolphinViewContainer grabbed the keyboard focus. As the tab is opened
193 // in background, assure that the previous focused widget gets the focus back.
194 focusWidget->setFocus();
195 }
196 }
197
198 void DolphinTabWidget::openDirectories(const QList<QUrl>& dirs, bool splitView)
199 {
200 Q_ASSERT(dirs.size() > 0);
201
202 bool somethingWasAlreadyOpen = false;
203
204 QList<QUrl>::const_iterator it = dirs.constBegin();
205 while (it != dirs.constEnd()) {
206 const QUrl& primaryUrl = *(it++);
207 const std::optional<ViewIndex> alreadyOpenDirectory = viewOpenAtDirectory(primaryUrl);
208
209 // When the user asks for a URL that's already open,
210 // activate it instead of opening a new tab
211 if (alreadyOpenDirectory.has_value()) {
212 somethingWasAlreadyOpen = true;
213 activateTab(alreadyOpenDirectory->tabIndex);
214 const auto tabPage = tabPageAt(alreadyOpenDirectory->tabIndex);
215 if (alreadyOpenDirectory->isInPrimaryView) {
216 tabPage->primaryViewContainer()->setActive(true);
217 } else {
218 tabPage->secondaryViewContainer()->setActive(true);
219 }
220 } else if (splitView && (it != dirs.constEnd())) {
221 const QUrl& secondaryUrl = *(it++);
222 if (somethingWasAlreadyOpen) {
223 openNewTab(primaryUrl, secondaryUrl);
224 } else {
225 openNewActivatedTab(primaryUrl, secondaryUrl);
226 }
227 } else {
228 if (somethingWasAlreadyOpen) {
229 openNewTab(primaryUrl);
230 } else {
231 openNewActivatedTab(primaryUrl);
232 }
233 }
234 }
235 }
236
237 void DolphinTabWidget::openFiles(const QList<QUrl>& files, bool splitView)
238 {
239 Q_ASSERT(files.size() > 0);
240
241 // Get all distinct directories from 'files'.
242 QList<QUrl> dirsThatNeedToBeOpened;
243 QList<QUrl> dirsThatWereAlreadyOpen;
244 for (const QUrl& file : files) {
245 const QUrl dir(file.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash));
246 if (dirsThatNeedToBeOpened.contains(dir) || dirsThatWereAlreadyOpen.contains(dir)) {
247 continue;
248 }
249
250 // The selecting of files that we do later will not work in views that already have items selected.
251 // So we check if dir is already open and clear the selection if it is. BUG: 417230
252 // We also make sure the view will be activated.
253 auto viewIndex = viewShowingItem(file);
254 if (viewIndex.has_value()) {
255 activateTab(viewIndex->tabIndex);
256 if (viewIndex->isInPrimaryView) {
257 tabPageAt(viewIndex->tabIndex)->primaryViewContainer()->view()->clearSelection();
258 tabPageAt(viewIndex->tabIndex)->primaryViewContainer()->setActive(true);
259 } else {
260 tabPageAt(viewIndex->tabIndex)->secondaryViewContainer()->view()->clearSelection();
261 tabPageAt(viewIndex->tabIndex)->secondaryViewContainer()->setActive(true);
262 }
263 dirsThatWereAlreadyOpen.append(dir);
264 } else {
265 dirsThatNeedToBeOpened.append(dir);
266 }
267 }
268
269 const int oldTabCount = count();
270 // Open a tab for each directory. If the "split view" option is enabled,
271 // two directories are shown inside one tab (see openDirectories()).
272 if (dirsThatNeedToBeOpened.size() > 0) {
273 openDirectories(dirsThatNeedToBeOpened, splitView);
274 }
275 const int tabCount = count();
276
277 // Select the files. Although the files can be split between several
278 // tabs, there is no need to split 'files' accordingly, as
279 // the DolphinView will just ignore invalid selections.
280 for (int i = 0; i < tabCount; ++i) {
281 DolphinTabPage* tabPage = tabPageAt(i);
282 tabPage->markUrlsAsSelected(files);
283 tabPage->markUrlAsCurrent(files.first());
284 if (i < oldTabCount) {
285 // Force selection of file if directory was already open, BUG: 417230
286 tabPage->activeViewContainer()->view()->updateViewState();
287 }
288 }
289 }
290
291 void DolphinTabWidget::closeTab()
292 {
293 closeTab(currentIndex());
294 }
295
296 void DolphinTabWidget::closeTab(const int index)
297 {
298 Q_ASSERT(index >= 0);
299 Q_ASSERT(index < count());
300
301 if (count() < 2) {
302 // Close Dolphin when closing the last tab.
303 parentWidget()->close();
304 return;
305 }
306
307 DolphinTabPage* tabPage = tabPageAt(index);
308 Q_EMIT rememberClosedTab(tabPage->activeViewContainer()->url(), tabPage->saveState());
309
310 removeTab(index);
311 tabPage->deleteLater();
312 }
313
314 void DolphinTabWidget::activateTab(const int index)
315 {
316 if (index < count()) {
317 setCurrentIndex(index);
318 }
319 }
320
321 void DolphinTabWidget::activateLastTab()
322 {
323 setCurrentIndex(count() - 1);
324 }
325
326 void DolphinTabWidget::activateNextTab()
327 {
328 const int index = currentIndex() + 1;
329 setCurrentIndex(index < count() ? index : 0);
330 }
331
332 void DolphinTabWidget::activatePrevTab()
333 {
334 const int index = currentIndex() - 1;
335 setCurrentIndex(index >= 0 ? index : (count() - 1));
336 }
337
338 void DolphinTabWidget::restoreClosedTab(const QByteArray& state)
339 {
340 openNewActivatedTab();
341 currentTabPage()->restoreState(state);
342 }
343
344 void DolphinTabWidget::copyToInactiveSplitView()
345 {
346 const DolphinTabPage* tabPage = tabPageAt(currentIndex());
347 DolphinViewContainer* activeViewContainer = currentTabPage()->activeViewContainer();
348 if (!tabPage->splitViewEnabled() || activeViewContainer->view()->selectedItems().isEmpty()) {
349 return;
350 }
351
352 if (tabPage->primaryViewActive()) {
353 // copy from left panel to right
354 activeViewContainer->view()->copySelectedItems(activeViewContainer->view()->selectedItems(), tabPage->secondaryViewContainer()->url());
355 } else {
356 // copy from right panel to left
357 activeViewContainer->view()->copySelectedItems(activeViewContainer->view()->selectedItems(), tabPage->primaryViewContainer()->url());
358 }
359 }
360
361 void DolphinTabWidget::moveToInactiveSplitView()
362 {
363 const DolphinTabPage* tabPage = tabPageAt(currentIndex());
364 DolphinViewContainer* activeViewContainer = currentTabPage()->activeViewContainer();
365 if (!tabPage->splitViewEnabled() || activeViewContainer->view()->selectedItems().isEmpty()) {
366 return;
367 }
368
369 if (tabPage->primaryViewActive()) {
370 // move from left panel to right
371 activeViewContainer->view()->moveSelectedItems(activeViewContainer->view()->selectedItems(), tabPage->secondaryViewContainer()->url());
372 } else {
373 // move from right panel to left
374 activeViewContainer->view()->moveSelectedItems(activeViewContainer->view()->selectedItems(), tabPage->primaryViewContainer()->url());
375 }
376 }
377
378 void DolphinTabWidget::detachTab(int index)
379 {
380 Q_ASSERT(index >= 0);
381
382 QStringList args;
383
384 const DolphinTabPage* tabPage = tabPageAt(index);
385 args << tabPage->primaryViewContainer()->url().url();
386 if (tabPage->splitViewEnabled()) {
387 args << tabPage->secondaryViewContainer()->url().url();
388 args << QStringLiteral("--split");
389 }
390 args << QStringLiteral("--new-window");
391
392 KIO::CommandLauncherJob *job = new KIO::CommandLauncherJob("dolphin", args, this);
393 job->setDesktopName(QStringLiteral("org.kde.dolphin"));
394 job->start();
395
396 closeTab(index);
397 }
398
399 void DolphinTabWidget::openNewActivatedTab(int index)
400 {
401 Q_ASSERT(index >= 0);
402 const DolphinTabPage* tabPage = tabPageAt(index);
403 openNewActivatedTab(tabPage->activeViewContainer()->url());
404 }
405
406 void DolphinTabWidget::tabDropEvent(int index, QDropEvent* event)
407 {
408 if (index >= 0) {
409 DolphinView* view = tabPageAt(index)->activeViewContainer()->view();
410 view->dropUrls(view->url(), event, view);
411 } else {
412 const auto urls = event->mimeData()->urls();
413
414 for (const QUrl &url : urls) {
415 auto *job = KIO::statDetails(url, KIO::StatJob::SourceSide, KIO::StatDetail::StatBasic, KIO::JobFlag::HideProgressInfo);
416 connect(job, &KJob::result, this, [this, job]() {
417 if (!job->error() && job->statResult().isDir()) {
418 openNewTab(job->url(), QUrl(), NewTabPosition::AtEnd);
419 }
420 });
421 }
422 }
423 }
424
425 void DolphinTabWidget::tabUrlChanged(const QUrl& url)
426 {
427 const int index = indexOf(qobject_cast<QWidget*>(sender()));
428 if (index >= 0) {
429 tabBar()->setTabText(index, tabName(tabPageAt(index)));
430 tabBar()->setTabToolTip(index, url.toDisplayString(QUrl::PreferLocalFile));
431 if (tabBar()->isVisible()) {
432 // ensure the path url ends with a slash to have proper folder icon for remote folders
433 const QUrl pathUrl = QUrl(url.adjusted(QUrl::StripTrailingSlash).toString(QUrl::FullyEncoded).append("/"));
434 tabBar()->setTabIcon(index, QIcon::fromTheme(KIO::iconNameForUrl(pathUrl)));
435 } else {
436 // Mark as dirty, actually load once the tab bar actually gets shown
437 tabBar()->setTabIcon(index, QIcon());
438 }
439
440 // Emit the currentUrlChanged signal if the url of the current tab has been changed.
441 if (index == currentIndex()) {
442 Q_EMIT currentUrlChanged(url);
443 }
444 }
445 }
446
447 void DolphinTabWidget::currentTabChanged(int index)
448 {
449 DolphinTabPage *tabPage = tabPageAt(index);
450 if (tabPage == m_lastViewedTab) {
451 return;
452 }
453 if (m_lastViewedTab) {
454 m_lastViewedTab->disconnectNavigators();
455 m_lastViewedTab->setActive(false);
456 }
457 if (tabPage->splitViewEnabled() && !m_navigatorsWidget->secondaryUrlNavigator()) {
458 m_navigatorsWidget->createSecondaryUrlNavigator();
459 }
460 DolphinViewContainer* viewContainer = tabPage->activeViewContainer();
461 Q_EMIT activeViewChanged(viewContainer);
462 Q_EMIT currentUrlChanged(viewContainer->url());
463 tabPage->setActive(true);
464 tabPage->connectNavigators(m_navigatorsWidget);
465 m_navigatorsWidget->setSecondaryNavigatorVisible(tabPage->splitViewEnabled());
466 m_lastViewedTab = tabPage;
467 }
468
469 void DolphinTabWidget::tabInserted(int index)
470 {
471 QTabWidget::tabInserted(index);
472
473 if (count() > 1) {
474 // Resolve all pending tab icons
475 for (int i = 0; i < count(); ++i) {
476 const QUrl url = tabPageAt(i)->activeViewContainer()->url();
477 if (tabBar()->tabIcon(i).isNull()) {
478 // ensure the path url ends with a slash to have proper folder icon for remote folders
479 const QUrl pathUrl = QUrl(url.adjusted(QUrl::StripTrailingSlash).toString(QUrl::FullyEncoded).append("/"));
480 tabBar()->setTabIcon(i, QIcon::fromTheme(KIO::iconNameForUrl(pathUrl)));
481 }
482 if (tabBar()->tabToolTip(i).isEmpty()) {
483 tabBar()->setTabToolTip(index, url.toDisplayString(QUrl::PreferLocalFile));
484 }
485 }
486
487 tabBar()->show();
488 }
489
490 Q_EMIT tabCountChanged(count());
491 }
492
493 void DolphinTabWidget::tabRemoved(int index)
494 {
495 QTabWidget::tabRemoved(index);
496
497 // If only one tab is left, then remove the tab entry so that
498 // closing the last tab is not possible.
499 if (count() < 2) {
500 tabBar()->hide();
501 }
502
503 Q_EMIT tabCountChanged(count());
504 }
505
506 QString DolphinTabWidget::tabName(DolphinTabPage* tabPage) const
507 {
508 if (!tabPage) {
509 return QString();
510 }
511 QString name = tabPage->activeViewContainer()->caption();
512 // Make sure that a '&' inside the directory name is displayed correctly
513 // and not misinterpreted as a keyboard shortcut in QTabBar::setTabText()
514 return name.replace('&', QLatin1String("&&"));
515 }
516
517 const std::optional<const DolphinTabWidget::ViewIndex> DolphinTabWidget::viewOpenAtDirectory(const QUrl& directory) const
518 {
519 int i = currentIndex();
520 if (i < 0) {
521 return std::nullopt;
522 }
523 // loop over the tabs starting from the current one
524 do {
525 const auto tabPage = tabPageAt(i);
526 if (tabPage->primaryViewContainer()->url() == directory) {
527 return std::optional(ViewIndex{i, true});
528 }
529
530 if (tabPage->splitViewEnabled() && tabPage->secondaryViewContainer()->url() == directory) {
531 return std::optional(ViewIndex{i, false});
532 }
533
534 i = (i + 1) % count();
535 }
536 while (i != currentIndex());
537
538 return std::nullopt;
539 }
540
541 const std::optional<const DolphinTabWidget::ViewIndex> DolphinTabWidget::viewShowingItem(const QUrl& item) const
542 {
543 // The item might not be loaded yet even though it exists. So instead
544 // we check if the folder containing the item is showing its contents.
545 const QUrl dirContainingItem(item.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash));
546
547 // The dirContainingItem is either open directly or expanded in a tree-style view mode.
548 // Is dirContainingitem the base url of a view?
549 auto viewOpenAtContainingDirectory = viewOpenAtDirectory(dirContainingItem);
550 if (viewOpenAtContainingDirectory.has_value()) {
551 return viewOpenAtContainingDirectory;
552 }
553
554 // Is dirContainingItem expanded in some tree-style view?
555 // The rest of this method is about figuring this out.
556
557 int i = currentIndex();
558 if (i < 0) {
559 return std::nullopt;
560 }
561 // loop over the tabs starting from the current one
562 do {
563 const auto tabPage = tabPageAt(i);
564 if (tabPage->primaryViewContainer()->url().isParentOf(item)) {
565 const KFileItem fileItemContainingItem = tabPage->primaryViewContainer()->view()->items().findByUrl(dirContainingItem);
566 if (!fileItemContainingItem.isNull() && tabPage->primaryViewContainer()->view()->isExpanded(fileItemContainingItem)) {
567 return std::optional(ViewIndex{i, true});
568 }
569 }
570
571 if (tabPage->splitViewEnabled() && tabPage->secondaryViewContainer()->url().isParentOf(item)) {
572 const KFileItem fileItemContainingItem = tabPage->secondaryViewContainer()->view()->items().findByUrl(dirContainingItem);
573 if (!fileItemContainingItem.isNull() && tabPage->secondaryViewContainer()->view()->isExpanded(fileItemContainingItem)) {
574 return std::optional(ViewIndex{i, false});
575 }
576 }
577
578 i = (i + 1) % count();
579 }
580 while (i != currentIndex());
581
582 return std::nullopt;
583 }