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 ¤t, 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