1 /* ============================================================
2 * VerticalTabs plugin for Falkon
3 * Copyright (C) 2018 David Rosca <nowrep@gmail.com>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 * ============================================================ */
18 #include "tabtreeview.h"
19 #include "tabtreedelegate.h"
20 #include "loadinganimator.h"
21 
22 #include "tabmodel.h"
23 #include "webtab.h"
24 #include "tabcontextmenu.h"
25 #include "browserwindow.h"
26 
27 #include <QTimer>
28 #include <QToolTip>
29 #include <QHoverEvent>
30 
TabTreeView(BrowserWindow * window,QWidget * parent)31 TabTreeView::TabTreeView(BrowserWindow *window, QWidget *parent)
32     : QTreeView(parent)
33     , m_window(window)
34     , m_expandedSessionKey(QSL("VerticalTabs-expanded"))
35 {
36     setDragEnabled(true);
37     setAcceptDrops(true);
38     setHeaderHidden(true);
39     setUniformRowHeights(true);
40     setDropIndicatorShown(true);
41     setAllColumnsShowFocus(true);
42     setMouseTracking(true);
43     setFocusPolicy(Qt::NoFocus);
44     setFrameShape(QFrame::NoFrame);
45     setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
46     setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
47     setIndentation(0);
48 
49     m_delegate = new TabTreeDelegate(this);
50     setItemDelegate(m_delegate);
51 
52     // Move scrollbar to the left
53     setLayoutDirection(isRightToLeft() ? Qt::LeftToRight : Qt::RightToLeft);
54 
55     // Enable hover to force redrawing close button
56     viewport()->setAttribute(Qt::WA_Hover);
57 
58     auto saveExpandedState = [this](const QModelIndex &index, bool expanded) {
59         if (m_initializing) {
60             return;
61         }
62         WebTab *tab = index.data(TabModel::WebTabRole).value<WebTab*>();
63         if (tab) {
64             tab->setSessionData(m_expandedSessionKey, expanded);
65         }
66     };
67     connect(this, &TabTreeView::expanded, this, std::bind(saveExpandedState, std::placeholders::_1, true));
68     connect(this, &TabTreeView::collapsed, this, std::bind(saveExpandedState, std::placeholders::_1, false));
69 }
70 
backgroundIndentation() const71 int TabTreeView::backgroundIndentation() const
72 {
73     return m_backgroundIndentation;
74 }
75 
setBackgroundIndentation(int indentation)76 void TabTreeView::setBackgroundIndentation(int indentation)
77 {
78     m_backgroundIndentation = indentation;
79 }
80 
areTabsInOrder() const81 bool TabTreeView::areTabsInOrder() const
82 {
83     return m_tabsInOrder;
84 }
85 
setTabsInOrder(bool enable)86 void TabTreeView::setTabsInOrder(bool enable)
87 {
88     m_tabsInOrder = enable;
89 }
90 
haveTreeModel() const91 bool TabTreeView::haveTreeModel() const
92 {
93     return m_haveTreeModel;
94 }
95 
setHaveTreeModel(bool enable)96 void TabTreeView::setHaveTreeModel(bool enable)
97 {
98     m_haveTreeModel = enable;
99 }
100 
setModel(QAbstractItemModel * model)101 void TabTreeView::setModel(QAbstractItemModel *model)
102 {
103     QTreeView::setModel(model);
104 
105     m_initializing = true;
106     QTimer::singleShot(0, this, &TabTreeView::initView);
107 }
108 
updateIndex(const QModelIndex & index)109 void TabTreeView::updateIndex(const QModelIndex &index)
110 {
111     QRect rect = visualRect(index);
112     if (!rect.isValid()) {
113         return;
114     }
115     // Need to update a little above/under to account for negative margins
116     rect.moveTop(rect.y() - rect.height() / 2);
117     rect.setHeight(rect.height() * 2);
118     viewport()->update(rect);
119 }
120 
adjustStyleOption(QStyleOptionViewItem * option)121 void TabTreeView::adjustStyleOption(QStyleOptionViewItem *option)
122 {
123     const QModelIndex index = option->index;
124 
125     option->state.setFlag(QStyle::State_Active, true);
126     option->state.setFlag(QStyle::State_HasFocus, false);
127     option->state.setFlag(QStyle::State_Selected, index.data(TabModel::CurrentTabRole).toBool());
128 
129     if (!index.isValid()) {
130         option->viewItemPosition = QStyleOptionViewItem::Invalid;
131     } else if (model()->rowCount() == 1) {
132         option->viewItemPosition = QStyleOptionViewItem::OnlyOne;
133     } else {
134         if (!indexAbove(index).isValid()) {
135             option->viewItemPosition = QStyleOptionViewItem::Beginning;
136         } else if (!indexBelow(index).isValid()) {
137             option->viewItemPosition = QStyleOptionViewItem::End;
138         } else {
139             option->viewItemPosition = QStyleOptionViewItem::Middle;
140         }
141     }
142 }
143 
drawBranches(QPainter *,const QRect &,const QModelIndex &) const144 void TabTreeView::drawBranches(QPainter *, const QRect &, const QModelIndex &) const
145 {
146     // Disable drawing branches
147 }
148 
currentChanged(const QModelIndex & current,const QModelIndex & previous)149 void TabTreeView::currentChanged(const QModelIndex &current, const QModelIndex &previous)
150 {
151     if (current.data(TabModel::CurrentTabRole).toBool()) {
152         QTreeView::currentChanged(current, previous);
153     } else if (previous.data(TabModel::CurrentTabRole).toBool()) {
154         setCurrentIndex(previous);
155     }
156 }
157 
dataChanged(const QModelIndex & topLeft,const QModelIndex & bottomRight,const QVector<int> & roles)158 void TabTreeView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles)
159 {
160     QTreeView::dataChanged(topLeft, bottomRight, roles);
161 
162     if (roles.size() == 1 && roles.at(0) == TabModel::CurrentTabRole && topLeft.data(TabModel::CurrentTabRole).toBool()) {
163         setCurrentIndex(topLeft);
164     }
165 }
166 
rowsInserted(const QModelIndex & parent,int start,int end)167 void TabTreeView::rowsInserted(const QModelIndex &parent, int start, int end)
168 {
169     QTreeView::rowsInserted(parent, start, end);
170 
171     if (m_initializing) {
172         return;
173     }
174 
175     // Parent for WebTab is set after insertTab is emitted
176     const QPersistentModelIndex index = model()->index(start, 0, parent);
177     QTimer::singleShot(0, this, [=]() {
178         if (!index.isValid()) {
179             return;
180         }
181         QModelIndex idx = index;
182         QVector<QModelIndex> stack;
183         do {
184             stack.append(idx);
185             idx = idx.parent();
186         } while (idx.isValid());
187         for (const QModelIndex &index : qAsConst(stack)) {
188             expand(index);
189         }
190         if (index.data(TabModel::CurrentTabRole).toBool()) {
191             setCurrentIndex(index);
192         }
193     });
194 }
195 
viewportEvent(QEvent * event)196 bool TabTreeView::viewportEvent(QEvent *event)
197 {
198     switch (event->type()) {
199     case QEvent::MouseButtonPress: {
200         QMouseEvent *me = static_cast<QMouseEvent*>(event);
201         const QModelIndex index = indexAt(me->pos());
202         updateIndex(index);
203         WebTab *tab = index.data(TabModel::WebTabRole).value<WebTab*>();
204         if (me->buttons() == Qt::MiddleButton) {
205             if (tab) {
206                 if (isExpanded(index)) {
207                     tab->closeTab();
208                 } else {
209                     closeTree(index);
210                 }
211             } else {
212                 m_window->addTab();
213             }
214         }
215         if (me->buttons() != Qt::LeftButton) {
216             m_pressedIndex = QModelIndex();
217             m_pressedButton = NoButton;
218             break;
219         }
220         m_pressedIndex = index;
221         m_pressedButton = buttonAt(me->pos(), m_pressedIndex);
222         if (m_pressedIndex.isValid()) {
223             if (m_pressedButton == ExpandButton) {
224                 if (isExpanded(m_pressedIndex)) {
225                     collapse(m_pressedIndex);
226                 } else {
227                     expand(m_pressedIndex);
228                 }
229                 me->accept();
230                 return true;
231             } else if (m_pressedButton == NoButton && tab) {
232                 tab->makeCurrentTab();
233             }
234         }
235         if (m_pressedButton == CloseButton) {
236             me->accept();
237             return true;
238         }
239         break;
240     }
241 
242     case QEvent::MouseMove: {
243         QMouseEvent *me = static_cast<QMouseEvent*>(event);
244         if (m_pressedButton == CloseButton) {
245             me->accept();
246             return true;
247         }
248         break;
249     }
250 
251     case QEvent::MouseButtonRelease: {
252         QMouseEvent *me = static_cast<QMouseEvent*>(event);
253         if (me->buttons() != Qt::NoButton) {
254             break;
255         }
256         const QModelIndex index = indexAt(me->pos());
257         updateIndex(index);
258         if (m_pressedIndex != index) {
259             break;
260         }
261         DelegateButton button = buttonAt(me->pos(), index);
262         if (m_pressedButton == button) {
263             if (m_pressedButton == ExpandButton) {
264                 me->accept();
265                 return true;
266             }
267             WebTab *tab = index.data(TabModel::WebTabRole).value<WebTab*>();
268             if (tab) {
269                 if (m_pressedButton == CloseButton) {
270                     tab->closeTab();
271                 } else if (m_pressedButton == AudioButton) {
272                     tab->toggleMuted();
273                 }
274             }
275         }
276         if (m_pressedButton == CloseButton) {
277             me->accept();
278             return true;
279         }
280         break;
281     }
282 
283     case QEvent::MouseButtonDblClick: {
284         QMouseEvent *me = static_cast<QMouseEvent*>(event);
285         const QModelIndex index = indexAt(me->pos());
286         if (me->button() == Qt::LeftButton && !index.isValid()) {
287             m_window->addTab();
288         }
289         break;
290     }
291 
292     case QEvent::HoverEnter:
293     case QEvent::HoverLeave:
294     case QEvent::HoverMove: {
295         QHoverEvent *he = static_cast<QHoverEvent*>(event);
296         updateIndex(m_hoveredIndex);
297         m_hoveredIndex = indexAt(he->pos());
298         updateIndex(m_hoveredIndex);
299         break;
300     }
301 
302     case QEvent::ToolTip: {
303         QHelpEvent *he = static_cast<QHelpEvent*>(event);
304         const QModelIndex index = indexAt(he->pos());
305         DelegateButton button = buttonAt(he->pos(), index);
306         if (button == AudioButton) {
307             const bool muted = index.data(TabModel::AudioMutedRole).toBool();
308             QToolTip::showText(he->globalPos(), muted ? tr("Unmute Tab") : tr("Mute Tab"), this, visualRect(index));
309             he->accept();
310             return true;
311         } else if (button == CloseButton) {
312             QToolTip::showText(he->globalPos(), tr("Close Tab"), this, visualRect(index));
313             he->accept();
314             return true;
315         } else if (button == NoButton) {
316             QToolTip::showText(he->globalPos(), index.data().toString(), this, visualRect(index));
317             he->accept();
318             return true;
319         }
320         break;
321     }
322 
323     case QEvent::ContextMenu: {
324         QContextMenuEvent *ce = static_cast<QContextMenuEvent*>(event);
325         const QModelIndex index = indexAt(ce->pos());
326         WebTab *tab = index.data(TabModel::WebTabRole).value<WebTab*>();
327         const int tabIndex = tab ? tab->tabIndex() : -1;
328         TabContextMenu::Options options = TabContextMenu::VerticalTabs | TabContextMenu::ShowDetachTabAction;
329         if (m_tabsInOrder) {
330             options |= TabContextMenu::ShowCloseOtherTabsActions;
331         }
332         TabContextMenu menu(tabIndex, m_window, options);
333         addMenuActions(&menu, index);
334         menu.exec(ce->globalPos());
335         break;
336     }
337 
338     default:
339         break;
340     }
341     return QTreeView::viewportEvent(event);
342 }
343 
initView()344 void TabTreeView::initView()
345 {
346     // Restore expanded state
347     for (int i = 0; i < model()->rowCount(); ++i) {
348         const QModelIndex index = model()->index(i, 0);
349         reverseTraverse(index, [this](const QModelIndex &index) {
350             WebTab *tab = index.data(TabModel::WebTabRole).value<WebTab*>();
351             if (tab) {
352                 setExpanded(index, tab->sessionData().value(m_expandedSessionKey, true).toBool());
353             }
354         });
355     }
356 
357     m_initializing = false;
358 }
359 
buttonAt(const QPoint & pos,const QModelIndex & index) const360 TabTreeView::DelegateButton TabTreeView::buttonAt(const QPoint &pos, const QModelIndex &index) const
361 {
362     if (m_delegate->expandButtonRect(index).contains(pos)) {
363         if (model()->rowCount(index) > 0) {
364             return ExpandButton;
365         }
366     } else if (m_delegate->audioButtonRect(index).contains(pos)) {
367         return AudioButton;
368     } else if (m_delegate->closeButtonRect(index).contains(pos)) {
369         return CloseButton;
370     }
371     return NoButton;
372 }
373 
addMenuActions(QMenu * menu,const QModelIndex & index)374 void TabTreeView::addMenuActions(QMenu *menu, const QModelIndex &index)
375 {
376     if (!m_haveTreeModel) {
377         return;
378     }
379 
380     menu->addSeparator();
381     QMenu *m = menu->addMenu(tr("Tab Tree"));
382 
383     if (index.isValid() && model()->rowCount(index) > 0) {
384         QPersistentModelIndex pindex = index;
385         m->addAction(tr("Close Tree"), this, [=]() {
386             closeTree(pindex);
387         });
388         m->addAction(tr("Unload Tree"), this, [=]() {
389             unloadTree(pindex);
390         });
391     }
392 
393     m->addSeparator();
394     m->addAction(tr("Expand All"), this, &TabTreeView::expandAll);
395     m->addAction(tr("Collapse All"), this, &TabTreeView::collapseAll);
396 }
397 
reverseTraverse(const QModelIndex & root,const std::function<void (const QModelIndex &)> & callback) const398 void TabTreeView::reverseTraverse(const QModelIndex &root, const std::function<void(const QModelIndex&)> &callback) const
399 {
400     if (!root.isValid()) {
401         return;
402     }
403     for (int i = 0; i < model()->rowCount(root); ++i) {
404         reverseTraverse(model()->index(i, 0, root), callback);
405     }
406     callback(root);
407 }
408 
closeTree(const QModelIndex & root)409 void TabTreeView::closeTree(const QModelIndex &root)
410 {
411     QVector<WebTab*> tabs;
412     reverseTraverse(root, [&](const QModelIndex &index) {
413         WebTab *tab = index.data(TabModel::WebTabRole).value<WebTab*>();
414         if (tab) {
415             tabs.append(tab);
416         }
417     });
418     for (WebTab *tab : qAsConst(tabs)) {
419         tab->closeTab();
420     }
421 }
422 
unloadTree(const QModelIndex & root)423 void TabTreeView::unloadTree(const QModelIndex &root)
424 {
425     reverseTraverse(root, [&](const QModelIndex &index) {
426         WebTab *tab = index.data(TabModel::WebTabRole).value<WebTab*>();
427         if (tab && tab->isRestored()) {
428             tab->unload();
429         }
430     });
431 }
432