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