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