]> cloud.milkyroute.net Git - dolphin.git/blob - src/dolphintabwidget.cpp
Improve naming consistency and leave mode on Escape
[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 indexByUrl(url).first >= 0;
118 }
119
120 bool DolphinTabWidget::isUrlOrParentOpen(const QUrl &url) const
121 {
122 return indexByUrl(url, ReturnIndexForOpenedParentAlso).first >= 0;
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)
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 int newTabIndex = -1;
177 if (!GeneralSettings::openNewTabAfterLastTab()) {
178 newTabIndex = currentIndex() + 1;
179 }
180
181 insertTab(newTabIndex, tabPage, QIcon() /* loaded in tabInserted */, tabName(tabPage));
182
183 if (focusWidget) {
184 // The DolphinViewContainer grabbed the keyboard focus. As the tab is opened
185 // in background, assure that the previous focused widget gets the focus back.
186 focusWidget->setFocus();
187 }
188 }
189
190 void DolphinTabWidget::openDirectories(const QList<QUrl>& dirs, bool splitView, bool skipChildUrls)
191 {
192 Q_ASSERT(dirs.size() > 0);
193
194 bool somethingWasAlreadyOpen = false;
195
196 QList<QUrl>::const_iterator it = dirs.constBegin();
197 while (it != dirs.constEnd()) {
198 const QUrl& primaryUrl = *(it++);
199 const QPair<int, bool> indexInfo = indexByUrl(primaryUrl, skipChildUrls ? ReturnIndexForOpenedParentAlso : ReturnIndexForOpenedUrlOnly);
200 const int index = indexInfo.first;
201 const bool isInPrimaryView = indexInfo.second;
202
203 // When the user asks for a URL that's already open (or it's parent is open if skipChildUrls is set),
204 // activate it instead of opening a new tab
205 if (index >= 0) {
206 somethingWasAlreadyOpen = true;
207 activateTab(index);
208 const auto tabPage = tabPageAt(index);
209 if (isInPrimaryView) {
210 tabPage->primaryViewContainer()->setActive(true);
211 } else {
212 tabPage->secondaryViewContainer()->setActive(true);
213 }
214 // BUG: 417230
215 // Required for updateViewState() call in openFiles() to work as expected
216 // If there is a selection, updateViewState() calls are effectively a no-op
217 tabPage->activeViewContainer()->view()->clearSelection();
218 } else if (splitView && (it != dirs.constEnd())) {
219 const QUrl& secondaryUrl = *(it++);
220 if (somethingWasAlreadyOpen) {
221 openNewTab(primaryUrl, secondaryUrl);
222 } else {
223 openNewActivatedTab(primaryUrl, secondaryUrl);
224 }
225 } else {
226 if (somethingWasAlreadyOpen) {
227 openNewTab(primaryUrl);
228 } else {
229 openNewActivatedTab(primaryUrl);
230 }
231 }
232 }
233 }
234
235 void DolphinTabWidget::openFiles(const QList<QUrl>& files, bool splitView)
236 {
237 Q_ASSERT(files.size() > 0);
238
239 // Get all distinct directories from 'files' and open a tab
240 // for each directory. If the "split view" option is enabled, two
241 // directories are shown inside one tab (see openDirectories()).
242 QList<QUrl> dirs;
243 for (const QUrl& url : files) {
244 const QUrl dir(url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash));
245 if (!dirs.contains(dir)) {
246 dirs.append(dir);
247 }
248 }
249
250 const int oldTabCount = count();
251 openDirectories(dirs, splitView, true);
252 const int tabCount = count();
253
254 // Select the files. Although the files can be split between several
255 // tabs, there is no need to split 'files' accordingly, as
256 // the DolphinView will just ignore invalid selections.
257 for (int i = 0; i < tabCount; ++i) {
258 DolphinTabPage* tabPage = tabPageAt(i);
259 tabPage->markUrlsAsSelected(files);
260 tabPage->markUrlAsCurrent(files.first());
261 if (i < oldTabCount) {
262 // Force selection of file if directory was already open, BUG: 417230
263 tabPage->activeViewContainer()->view()->updateViewState();
264 }
265 }
266 }
267
268 void DolphinTabWidget::closeTab()
269 {
270 closeTab(currentIndex());
271 }
272
273 void DolphinTabWidget::closeTab(const int index)
274 {
275 Q_ASSERT(index >= 0);
276 Q_ASSERT(index < count());
277
278 if (count() < 2) {
279 // Close Dolphin when closing the last tab.
280 parentWidget()->close();
281 return;
282 }
283
284 DolphinTabPage* tabPage = tabPageAt(index);
285 Q_EMIT rememberClosedTab(tabPage->activeViewContainer()->url(), tabPage->saveState());
286
287 removeTab(index);
288 tabPage->deleteLater();
289 }
290
291 void DolphinTabWidget::activateTab(const int index)
292 {
293 if (index < count()) {
294 setCurrentIndex(index);
295 }
296 }
297
298 void DolphinTabWidget::activateLastTab()
299 {
300 setCurrentIndex(count() - 1);
301 }
302
303 void DolphinTabWidget::activateNextTab()
304 {
305 const int index = currentIndex() + 1;
306 setCurrentIndex(index < count() ? index : 0);
307 }
308
309 void DolphinTabWidget::activatePrevTab()
310 {
311 const int index = currentIndex() - 1;
312 setCurrentIndex(index >= 0 ? index : (count() - 1));
313 }
314
315 void DolphinTabWidget::restoreClosedTab(const QByteArray& state)
316 {
317 openNewActivatedTab();
318 currentTabPage()->restoreState(state);
319 }
320
321 void DolphinTabWidget::copyToInactiveSplitView()
322 {
323 const DolphinTabPage* tabPage = tabPageAt(currentIndex());
324 DolphinViewContainer* activeViewContainer = currentTabPage()->activeViewContainer();
325 if (!tabPage->splitViewEnabled() || activeViewContainer->view()->selectedItems().isEmpty()) {
326 return;
327 }
328
329 if (tabPage->primaryViewActive()) {
330 // copy from left panel to right
331 activeViewContainer->view()->copySelectedItems(activeViewContainer->view()->selectedItems(), tabPage->secondaryViewContainer()->url());
332 } else {
333 // copy from right panel to left
334 activeViewContainer->view()->copySelectedItems(activeViewContainer->view()->selectedItems(), tabPage->primaryViewContainer()->url());
335 }
336 }
337
338 void DolphinTabWidget::moveToInactiveSplitView()
339 {
340 const DolphinTabPage* tabPage = tabPageAt(currentIndex());
341 DolphinViewContainer* activeViewContainer = currentTabPage()->activeViewContainer();
342 if (!tabPage->splitViewEnabled() || activeViewContainer->view()->selectedItems().isEmpty()) {
343 return;
344 }
345
346 if (tabPage->primaryViewActive()) {
347 // move from left panel to right
348 activeViewContainer->view()->moveSelectedItems(activeViewContainer->view()->selectedItems(), tabPage->secondaryViewContainer()->url());
349 } else {
350 // move from right panel to left
351 activeViewContainer->view()->moveSelectedItems(activeViewContainer->view()->selectedItems(), tabPage->primaryViewContainer()->url());
352 }
353 }
354
355 void DolphinTabWidget::detachTab(int index)
356 {
357 Q_ASSERT(index >= 0);
358
359 QStringList args;
360
361 const DolphinTabPage* tabPage = tabPageAt(index);
362 args << tabPage->primaryViewContainer()->url().url();
363 if (tabPage->splitViewEnabled()) {
364 args << tabPage->secondaryViewContainer()->url().url();
365 args << QStringLiteral("--split");
366 }
367 args << QStringLiteral("--new-window");
368
369 KIO::CommandLauncherJob *job = new KIO::CommandLauncherJob("dolphin", args, this);
370 job->setDesktopName(QStringLiteral("org.kde.dolphin"));
371 job->start();
372
373 closeTab(index);
374 }
375
376 void DolphinTabWidget::openNewActivatedTab(int index)
377 {
378 Q_ASSERT(index >= 0);
379 const DolphinTabPage* tabPage = tabPageAt(index);
380 openNewActivatedTab(tabPage->activeViewContainer()->url());
381 }
382
383 void DolphinTabWidget::tabDropEvent(int index, QDropEvent* event)
384 {
385 if (index >= 0) {
386 DolphinView* view = tabPageAt(index)->activeViewContainer()->view();
387 view->dropUrls(view->url(), event, view);
388 }
389 }
390
391 void DolphinTabWidget::tabUrlChanged(const QUrl& url)
392 {
393 const int index = indexOf(qobject_cast<QWidget*>(sender()));
394 if (index >= 0) {
395 tabBar()->setTabText(index, tabName(tabPageAt(index)));
396 tabBar()->setTabToolTip(index, url.toDisplayString(QUrl::PreferLocalFile));
397 if (tabBar()->isVisible()) {
398 // ensure the path url ends with a slash to have proper folder icon for remote folders
399 const QUrl pathUrl = QUrl(url.adjusted(QUrl::StripTrailingSlash).toString(QUrl::FullyEncoded).append("/"));
400 tabBar()->setTabIcon(index, QIcon::fromTheme(KIO::iconNameForUrl(pathUrl)));
401 } else {
402 // Mark as dirty, actually load once the tab bar actually gets shown
403 tabBar()->setTabIcon(index, QIcon());
404 }
405
406 // Emit the currentUrlChanged signal if the url of the current tab has been changed.
407 if (index == currentIndex()) {
408 Q_EMIT currentUrlChanged(url);
409 }
410 }
411 }
412
413 void DolphinTabWidget::currentTabChanged(int index)
414 {
415 DolphinTabPage *tabPage = tabPageAt(index);
416 if (tabPage == m_lastViewedTab) {
417 return;
418 }
419 if (m_lastViewedTab) {
420 m_lastViewedTab->disconnectNavigators();
421 m_lastViewedTab->setActive(false);
422 }
423 if (tabPage->splitViewEnabled() && !m_navigatorsWidget->secondaryUrlNavigator()) {
424 m_navigatorsWidget->createSecondaryUrlNavigator();
425 }
426 DolphinViewContainer* viewContainer = tabPage->activeViewContainer();
427 Q_EMIT activeViewChanged(viewContainer);
428 Q_EMIT currentUrlChanged(viewContainer->url());
429 tabPage->setActive(true);
430 tabPage->connectNavigators(m_navigatorsWidget);
431 m_navigatorsWidget->setSecondaryNavigatorVisible(tabPage->splitViewEnabled());
432 m_lastViewedTab = tabPage;
433 }
434
435 void DolphinTabWidget::tabInserted(int index)
436 {
437 QTabWidget::tabInserted(index);
438
439 if (count() > 1) {
440 // Resolve all pending tab icons
441 for (int i = 0; i < count(); ++i) {
442 const QUrl url = tabPageAt(i)->activeViewContainer()->url();
443 if (tabBar()->tabIcon(i).isNull()) {
444 // ensure the path url ends with a slash to have proper folder icon for remote folders
445 const QUrl pathUrl = QUrl(url.adjusted(QUrl::StripTrailingSlash).toString(QUrl::FullyEncoded).append("/"));
446 tabBar()->setTabIcon(i, QIcon::fromTheme(KIO::iconNameForUrl(pathUrl)));
447 }
448 if (tabBar()->tabToolTip(i).isEmpty()) {
449 tabBar()->setTabToolTip(index, url.toDisplayString(QUrl::PreferLocalFile));
450 }
451 }
452
453 tabBar()->show();
454 }
455
456 Q_EMIT tabCountChanged(count());
457 }
458
459 void DolphinTabWidget::tabRemoved(int index)
460 {
461 QTabWidget::tabRemoved(index);
462
463 // If only one tab is left, then remove the tab entry so that
464 // closing the last tab is not possible.
465 if (count() < 2) {
466 tabBar()->hide();
467 }
468
469 Q_EMIT tabCountChanged(count());
470 }
471
472 QString DolphinTabWidget::tabName(DolphinTabPage* tabPage) const
473 {
474 if (!tabPage) {
475 return QString();
476 }
477 QString name = tabPage->activeViewContainer()->caption();
478 // Make sure that a '&' inside the directory name is displayed correctly
479 // and not misinterpreted as a keyboard shortcut in QTabBar::setTabText()
480 return name.replace('&', QLatin1String("&&"));
481 }
482
483 QPair<int, bool> DolphinTabWidget::indexByUrl(const QUrl& url, ChildUrlBehavior childUrlBehavior) const
484 {
485 int i = currentIndex();
486 if (i < 0) {
487 return qMakePair(-1, false);
488 }
489 // loop over the tabs starting from the current one
490 do {
491 const auto tabPage = tabPageAt(i);
492 if (tabPage->primaryViewContainer()->url() == url ||
493 (childUrlBehavior == ReturnIndexForOpenedParentAlso && tabPage->primaryViewContainer()->url().isParentOf(url))) {
494 return qMakePair(i, true);
495 }
496
497 if (tabPage->splitViewEnabled() &&
498 (url == tabPage->secondaryViewContainer()->url() ||
499 (childUrlBehavior == ReturnIndexForOpenedParentAlso && tabPage->secondaryViewContainer()->url().isParentOf(url)))) {
500 return qMakePair(i, false);
501 }
502
503 i = (i + 1) % count();
504 }
505 while (i != currentIndex());
506
507 return qMakePair(-1, false);
508 }