1 /*
2 Copyright (c) 2020, Lukas Holecek <hluk@email.cz>
3
4 This file is part of CopyQ.
5
6 CopyQ is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
10
11 CopyQ is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with CopyQ. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20 #include "tabwidget.h"
21 #include "tabbar.h"
22 #include "tabtree.h"
23
24 #include "common/config.h"
25
26 #include <QAction>
27 #include <QBoxLayout>
28 #include <QEvent>
29 #include <QMainWindow>
30 #include <QMimeData>
31 #include <QPoint>
32 #include <QSettings>
33 #include <QStackedWidget>
34 #include <QToolBar>
35
36 namespace {
37
getTabWidgetConfigurationFilePath()38 QString getTabWidgetConfigurationFilePath()
39 {
40 return getConfigurationFilePath("_tabs.ini");
41 }
42
43 template <typename Receiver, typename Slot>
addTabAction(QWidget * widget,const QKeySequence & shortcut,Receiver * receiver,Slot slot,Qt::ShortcutContext context=Qt::WindowShortcut)44 void addTabAction(QWidget *widget, const QKeySequence &shortcut,
45 Receiver *receiver, Slot slot,
46 Qt::ShortcutContext context = Qt::WindowShortcut)
47 {
48 auto act = new QAction(widget);
49 act->setShortcut(shortcut);
50 act->setShortcutContext(context);
51 QObject::connect(act, &QAction::triggered, receiver, slot);
52 widget->addAction(act);
53 }
54
55 } // namespace
56
TabWidget(QWidget * parent)57 TabWidget::TabWidget(QWidget *parent)
58 : QWidget(parent)
59 , m_toolBar(new QToolBar(this))
60 , m_toolBarTree(new QToolBar(this))
61 , m_stackedWidget(nullptr)
62 , m_hideTabBar(false)
63 , m_showTabItemCount(false)
64 {
65 // Set object name for tool bars so they can be saved with QMainWindow::saveState().
66 m_toolBar->setObjectName("toolBarTabBar");
67 m_toolBarTree->setObjectName("toolBarTabTree");
68
69 m_toolBar->setContextMenuPolicy(Qt::NoContextMenu);
70 m_toolBarTree->setContextMenuPolicy(Qt::NoContextMenu);
71
72 m_toolBar->installEventFilter(this);
73 connect( m_toolBar, &QToolBar::orientationChanged,
74 this, &TabWidget::onToolBarOrientationChanged );
75
76 auto layout = new QBoxLayout(QBoxLayout::TopToBottom, this);
77 setLayout(layout);
78 layout->setContentsMargins(0, 0, 0, 0);
79 layout->setSpacing(0);
80
81 m_stackedWidget = new QStackedWidget(this);
82 layout->addWidget(m_stackedWidget);
83
84 createTabBar();
85
86 addTabAction(this, QKeySequence::PreviousChild, this, &TabWidget::previousTab);
87 addTabAction(this, QKeySequence::NextChild, this, &TabWidget::nextTab);
88
89 loadTabInfo();
90 }
91
getCurrentTabPath() const92 QString TabWidget::getCurrentTabPath() const
93 {
94 return m_tabs->getCurrentTabPath();
95 }
96
isTabGroup(const QString & tab) const97 bool TabWidget::isTabGroup(const QString &tab) const
98 {
99 return m_tabs->isTabGroup(tab);
100 }
101
isTabGroupSelected() const102 bool TabWidget::isTabGroupSelected() const
103 {
104 QWidget *w = currentWidget();
105 return w != nullptr && w->isHidden();
106 }
107
currentIndex() const108 int TabWidget::currentIndex() const
109 {
110 return m_stackedWidget->currentIndex();
111 }
112
widget(int tabIndex) const113 QWidget *TabWidget::widget(int tabIndex) const
114 {
115 return m_stackedWidget->widget(tabIndex);
116 }
117
count() const118 int TabWidget::count() const
119 {
120 return m_stackedWidget->count();
121 }
122
tabName(int tabIndex) const123 QString TabWidget::tabName(int tabIndex) const
124 {
125 return m_tabs->tabName(tabIndex);
126 }
127
setTabName(int tabIndex,const QString & tabName)128 void TabWidget::setTabName(int tabIndex, const QString &tabName)
129 {
130 const QString oldTabName = this->tabName(tabIndex);
131 if ( m_tabItemCounters.contains(oldTabName) )
132 m_tabItemCounters.insert( tabName, m_tabItemCounters.take(oldTabName) );
133
134 m_tabs->setTabName(tabIndex, tabName);
135
136 m_tabs->adjustSize();
137 }
138
setTabItemCountVisible(bool visible)139 void TabWidget::setTabItemCountVisible(bool visible)
140 {
141 m_showTabItemCount = visible;
142 for (int i = 0; i < count(); ++i)
143 updateTabItemCount( tabName(i) );
144
145 m_tabs->adjustSize();
146 }
147
updateTabIcon(const QString & tabName)148 void TabWidget::updateTabIcon(const QString &tabName)
149 {
150 m_tabs->updateTabIcon(tabName);
151 }
152
insertTab(int tabIndex,QWidget * widget,const QString & tabName)153 void TabWidget::insertTab(int tabIndex, QWidget *widget, const QString &tabName)
154 {
155 const bool firstTab = count() == 0;
156 m_stackedWidget->insertWidget(tabIndex, widget);
157
158 m_tabs->insertTab(tabIndex, tabName);
159 m_toolBarCurrent->layout()->setSizeConstraint(QLayout::SetMinAndMaxSize);
160
161 if (firstTab)
162 emit currentChanged(0, -1);
163
164 updateTabItemCount(tabName);
165 updateToolBar();
166 }
167
removeTab(int tabIndex)168 void TabWidget::removeTab(int tabIndex)
169 {
170 if (tabIndex == currentIndex())
171 setCurrentIndex(tabIndex == 0 ? 1 : 0);
172
173 const QString tabName = this->tabName(tabIndex);
174 m_tabItemCounters.remove(tabName);
175
176 // Item count must be updated If tab is removed but tab group remains.
177 m_tabs->setTabItemCount(tabName, QString());
178
179 QWidget *w = m_stackedWidget->widget(tabIndex);
180 m_stackedWidget->removeWidget(w);
181 delete w;
182
183 m_tabs->removeTab(tabIndex);
184
185 updateToolBar();
186 }
187
tabs() const188 QStringList TabWidget::tabs() const
189 {
190 QStringList tabs;
191 tabs.reserve( count() );
192 for( int i = 0; i < count(); ++i )
193 tabs.append( tabName(i) );
194
195 return tabs;
196 }
197
addToolBars(QMainWindow * mainWindow)198 void TabWidget::addToolBars(QMainWindow *mainWindow)
199 {
200 mainWindow->addToolBar(Qt::TopToolBarArea, m_toolBar);
201 mainWindow->addToolBar(Qt::LeftToolBarArea, m_toolBarTree);
202 }
203
saveTabInfo()204 void TabWidget::saveTabInfo()
205 {
206 QStringList tabs;
207 tabs.reserve( count() );
208 for ( int i = 0; i < count(); ++i )
209 tabs.append( tabName(i) );
210
211 QSettings settings(getTabWidgetConfigurationFilePath(), QSettings::IniFormat);
212
213 m_tabs->updateCollapsedTabs(&m_collapsedTabs);
214 settings.setValue("TabWidget/collapsed_tabs", m_collapsedTabs);
215
216 QVariantMap tabItemCounters;
217 for (const auto &key : tabs) {
218 const int count = m_tabItemCounters.value(key, -1);
219 if (count >= 0)
220 tabItemCounters[key] = count;
221 }
222 settings.setValue("TabWidget/tab_item_counters", tabItemCounters);
223 }
224
loadTabInfo()225 void TabWidget::loadTabInfo()
226 {
227 QSettings settings(getTabWidgetConfigurationFilePath(), QSettings::IniFormat);
228
229 m_collapsedTabs = settings.value("TabWidget/collapsed_tabs").toStringList();
230
231 QVariantMap tabItemCounters = settings.value("TabWidget/tab_item_counters").toMap();
232 m_tabItemCounters.clear();
233 for (auto it = tabItemCounters.constBegin(); it != tabItemCounters.constEnd(); ++it)
234 m_tabItemCounters[it.key()] = it.value().toInt();
235 }
236
updateTabs()237 void TabWidget::updateTabs()
238 {
239 m_tabs->setCollapsedTabs(m_collapsedTabs);
240 m_tabs->updateTabIcons();
241 }
242
setCurrentIndex(int tabIndex)243 void TabWidget::setCurrentIndex(int tabIndex)
244 {
245 if (m_ignoreCurrentTabChanges)
246 return;
247
248 QWidget *w = currentWidget();
249 const int current = isTabGroupSelected() ? -1 : currentIndex();
250
251 if (tabIndex == current)
252 return;
253
254 if (tabIndex != -1) {
255 m_stackedWidget->setCurrentIndex(tabIndex);
256
257 w = currentWidget();
258 if (w == nullptr)
259 return;
260
261 w->show();
262 if (m_toolBarCurrent->hasFocus())
263 w->setFocus();
264
265 m_tabs->setCurrentTab(tabIndex);
266 } else if (w != nullptr) {
267 if (w->hasFocus())
268 m_toolBarCurrent->setFocus();
269 w->hide();
270 }
271
272 emit currentChanged(tabIndex, current);
273 }
274
nextTab()275 void TabWidget::nextTab()
276 {
277 m_tabs->nextTab();
278 }
279
previousTab()280 void TabWidget::previousTab()
281 {
282 m_tabs->previousTab();
283 }
284
setTabBarHidden(bool hidden)285 void TabWidget::setTabBarHidden(bool hidden)
286 {
287 m_hideTabBar = hidden;
288 updateToolBar();
289 }
290
setTreeModeEnabled(bool enabled)291 void TabWidget::setTreeModeEnabled(bool enabled)
292 {
293 setTreeModeEnabled(enabled, this->tabs());
294 }
295
setTabItemCount(const QString & tabName,int itemCount)296 void TabWidget::setTabItemCount(const QString &tabName, int itemCount)
297 {
298 if ( m_tabItemCounters.value(tabName, -1) == itemCount)
299 return;
300
301 m_tabItemCounters[tabName] = itemCount;
302
303 updateTabItemCount(tabName);
304 }
305
setTabsOrder(const QStringList & tabs)306 void TabWidget::setTabsOrder(const QStringList &tabs)
307 {
308 QStringList currentTabs = this->tabs();
309 if (tabs == currentTabs)
310 return;
311
312 m_ignoreCurrentTabChanges = true;
313
314 for (int i = 0; i < tabs.size(); ++i) {
315 const int tabIndex = currentTabs.indexOf(tabs[i]);
316 if (tabIndex != -1 && tabIndex != i) {
317 QWidget *widget = m_stackedWidget->widget(tabIndex);
318 m_stackedWidget->removeWidget(widget);
319 m_stackedWidget->insertWidget(i, widget);
320 currentTabs.move(tabIndex, i);
321 }
322 }
323
324 m_stackedWidget->setCurrentIndex( currentIndex() );
325 setTreeModeEnabled(m_toolBarCurrent == m_toolBarTree, currentTabs);
326
327 m_ignoreCurrentTabChanges = false;
328 }
329
eventFilter(QObject *,QEvent * event)330 bool TabWidget::eventFilter(QObject *, QEvent *event)
331 {
332 if (event->type() == QEvent::Move)
333 updateToolBar();
334
335 return false;
336 }
337
onTabMoved(int from,int to)338 void TabWidget::onTabMoved(int from, int to)
339 {
340 m_stackedWidget->insertWidget(to, m_stackedWidget->widget(from));
341 emit tabMoved(from, to);
342 }
343
onTabsMoved(const QString & oldPrefix,const QString & newPrefix,const QList<int> & indexes)344 void TabWidget::onTabsMoved(const QString &oldPrefix, const QString &newPrefix, const QList<int> &indexes)
345 {
346 const int newCurrentIndex = indexes.indexOf(currentIndex());
347 Q_ASSERT(newCurrentIndex != -1);
348
349 m_stackedWidget->hide();
350
351 QVector<QWidget*> widgets;
352 widgets.reserve(m_stackedWidget->count());
353
354 while ( m_stackedWidget->count() > 0 ) {
355 QWidget *w = m_stackedWidget->widget(0);
356 widgets.append(w);
357 m_stackedWidget->removeWidget(w);
358 }
359
360 for (int index : indexes) {
361 Q_ASSERT(index >= 0);
362 Q_ASSERT(index < widgets.count());
363 m_stackedWidget->insertWidget(-1, widgets[index]);
364 }
365
366 m_stackedWidget->setCurrentIndex(newCurrentIndex);
367
368 m_stackedWidget->show();
369
370 emit tabsMoved(oldPrefix, newPrefix);
371 }
372
onToolBarOrientationChanged(Qt::Orientation orientation)373 void TabWidget::onToolBarOrientationChanged(Qt::Orientation orientation)
374 {
375 if (m_tabBar) {
376 if (orientation == Qt::Vertical)
377 m_tabBar->setShape(QTabBar::RoundedWest);
378 else
379 m_tabBar->setShape(QTabBar::RoundedNorth);
380 }
381 }
382
onTreeItemClicked()383 void TabWidget::onTreeItemClicked()
384 {
385 auto w = currentWidget();
386 if (w)
387 w->setFocus(Qt::MouseFocusReason);
388 }
389
createTabBar()390 void TabWidget::createTabBar()
391 {
392 auto tabBar = new TabBar(this);
393
394 tabBar->setObjectName("tab_bar");
395
396 tabBar->setExpanding(false);
397 tabBar->setMovable(true);
398
399 connect( tabBar, &TabBar::tabBarMenuRequested,
400 this, &TabWidget::tabBarMenuRequested );
401 connect( tabBar, &TabBar::tabRenamed,
402 this, &TabWidget::tabRenamed );
403 connect( tabBar, &QTabBar::tabCloseRequested,
404 this, &TabWidget::tabCloseRequested );
405 connect( tabBar, &TabBar::dropItems,
406 this, &TabWidget::dropItems );
407 connect( tabBar, &QTabBar::currentChanged,
408 this, &TabWidget::setCurrentIndex );
409 connect( tabBar, &QTabBar::tabMoved,
410 this, &TabWidget::onTabMoved );
411
412 delete m_tabs;
413 m_tabs = tabBar;
414 m_tabBar = tabBar;
415
416 m_toolBarCurrent = m_toolBar;
417 m_toolBarCurrent->addWidget(tabBar);
418 }
419
createTabTree()420 void TabWidget::createTabTree()
421 {
422 auto tabTree = new TabTree(this);
423 tabTree->setObjectName("tab_tree");
424
425 connect( tabTree, &TabTree::tabTreeMenuRequested,
426 this, &TabWidget::tabTreeMenuRequested );
427 connect( tabTree, &TabTree::tabsMoved,
428 this, &TabWidget::onTabsMoved );
429 connect( tabTree, &TabTree::dropItems,
430 this, &TabWidget::dropItems );
431 connect( tabTree, &TabTree::currentTabChanged,
432 this, &TabWidget::setCurrentIndex );
433 connect( tabTree, &QTreeWidget::itemClicked,
434 this, &TabWidget::onTreeItemClicked );
435
436 delete m_tabs;
437 m_tabs = tabTree;
438 m_tabBar = nullptr;
439
440 m_toolBarCurrent = m_toolBarTree;
441 m_toolBarCurrent->addWidget(tabTree);
442 }
443
updateToolBar()444 void TabWidget::updateToolBar()
445 {
446 bool forceHide = count() == 1;
447 m_toolBar->setVisible(!forceHide && !m_hideTabBar && m_toolBarCurrent == m_toolBar);
448 m_toolBarTree->setVisible(!forceHide && !m_hideTabBar && m_toolBarCurrent == m_toolBarTree);
449
450 if (m_tabBar) {
451 QMainWindow *mainWindow = qobject_cast<QMainWindow*>(m_toolBar->window());
452 if (mainWindow) {
453 Qt::ToolBarArea area = mainWindow->toolBarArea(m_toolBar);
454 if (area == Qt::LeftToolBarArea)
455 m_tabBar->setShape(QTabBar::RoundedWest);
456 else if (area == Qt::RightToolBarArea)
457 m_tabBar->setShape(QTabBar::RoundedEast);
458 else if (area == Qt::TopToolBarArea)
459 m_tabBar->setShape(QTabBar::RoundedNorth);
460 else if (area == Qt::BottomToolBarArea)
461 m_tabBar->setShape(QTabBar::RoundedSouth);
462 }
463 }
464
465 m_tabs->adjustSize();
466 }
467
updateTabItemCount(const QString & name)468 void TabWidget::updateTabItemCount(const QString &name)
469 {
470 m_tabs->setTabItemCount(name, itemCountLabel(name));
471 m_tabs->adjustSize();
472 }
473
itemCountLabel(const QString & name)474 QString TabWidget::itemCountLabel(const QString &name)
475 {
476 if (!m_showTabItemCount)
477 return QString();
478
479 const int count = m_tabItemCounters.value(name, -1);
480 return count > 0 ? QString::number(count) : count == 0 ? QString() : QLatin1String("?");
481 }
482
setTreeModeEnabled(bool enabled,const QStringList & tabs)483 void TabWidget::setTreeModeEnabled(bool enabled, const QStringList &tabs)
484 {
485 if (enabled)
486 createTabTree();
487 else
488 createTabBar();
489
490 m_ignoreCurrentTabChanges = true;
491 for (int i = 0; i < tabs.size(); ++i) {
492 const QString &tabName = tabs[i];
493 m_tabs->insertTab(i, tabName);
494 m_tabs->setTabItemCount(tabName, itemCountLabel(tabName));
495 }
496 m_tabs->setCurrentTab( currentIndex() );
497 m_ignoreCurrentTabChanges = false;
498
499 updateToolBar();
500 }
501