File indexing completed on 2024-05-19 04:59:22

0001 /* ============================================================
0002 * VerticalTabs plugin for Falkon
0003 * Copyright (C) 2018 David Rosca <nowrep@gmail.com>
0004 *
0005 * This program is free software: you can redistribute it and/or modify
0006 * it under the terms of the GNU General Public License as published by
0007 * the Free Software Foundation, either version 3 of the License, or
0008 * (at your option) any later version.
0009 *
0010 * This program is distributed in the hope that it will be useful,
0011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
0012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0013 * GNU General Public License for more details.
0014 *
0015 * You should have received a copy of the GNU General Public License
0016 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
0017 * ============================================================ */
0018 #include "tabtreeview.h"
0019 #include "tabtreedelegate.h"
0020 #include "loadinganimator.h"
0021 
0022 #include "tabmodel.h"
0023 #include "webtab.h"
0024 #include "tabcontextmenu.h"
0025 #include "browserwindow.h"
0026 
0027 #include <QTimer>
0028 #include <QToolTip>
0029 #include <QHoverEvent>
0030 
0031 TabTreeView::TabTreeView(BrowserWindow *window, QWidget *parent)
0032     : QTreeView(parent)
0033     , m_window(window)
0034     , m_expandedSessionKey(QSL("VerticalTabs-expanded"))
0035 {
0036     setDragEnabled(true);
0037     setAcceptDrops(true);
0038     setHeaderHidden(true);
0039     setUniformRowHeights(true);
0040     setDropIndicatorShown(true);
0041     setAllColumnsShowFocus(true);
0042     setMouseTracking(true);
0043     setFocusPolicy(Qt::NoFocus);
0044     setFrameShape(QFrame::NoFrame);
0045     setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
0046     setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
0047     setIndentation(0);
0048 
0049     m_delegate = new TabTreeDelegate(this);
0050     setItemDelegate(m_delegate);
0051 
0052     // Move scrollbar to the left
0053     setLayoutDirection(isRightToLeft() ? Qt::LeftToRight : Qt::RightToLeft);
0054 
0055     // Enable hover to force redrawing close button
0056     viewport()->setAttribute(Qt::WA_Hover);
0057 
0058     auto saveExpandedState = [this](const QModelIndex &index, bool expanded) {
0059         if (m_initializing) {
0060             return;
0061         }
0062         auto *tab = index.data(TabModel::WebTabRole).value<WebTab*>();
0063         if (tab) {
0064             tab->setSessionData(m_expandedSessionKey, expanded);
0065         }
0066     };
0067     connect(this, &TabTreeView::expanded, this, std::bind(saveExpandedState, std::placeholders::_1, true));
0068     connect(this, &TabTreeView::collapsed, this, std::bind(saveExpandedState, std::placeholders::_1, false));
0069 }
0070 
0071 int TabTreeView::backgroundIndentation() const
0072 {
0073     return m_backgroundIndentation;
0074 }
0075 
0076 void TabTreeView::setBackgroundIndentation(int indentation)
0077 {
0078     m_backgroundIndentation = indentation;
0079 }
0080 
0081 bool TabTreeView::areTabsInOrder() const
0082 {
0083     return m_tabsInOrder;
0084 }
0085 
0086 void TabTreeView::setTabsInOrder(bool enable)
0087 {
0088     m_tabsInOrder = enable;
0089 }
0090 
0091 bool TabTreeView::haveTreeModel() const
0092 {
0093     return m_haveTreeModel;
0094 }
0095 
0096 void TabTreeView::setHaveTreeModel(bool enable)
0097 {
0098     m_haveTreeModel = enable;
0099 }
0100 
0101 void TabTreeView::setModel(QAbstractItemModel *model)
0102 {
0103     QTreeView::setModel(model);
0104 
0105     m_initializing = true;
0106     QTimer::singleShot(0, this, &TabTreeView::initView);
0107 }
0108 
0109 void TabTreeView::updateIndex(const QModelIndex &index)
0110 {
0111     QRect rect = visualRect(index);
0112     if (!rect.isValid()) {
0113         return;
0114     }
0115     // Need to update a little above/under to account for negative margins
0116     rect.moveTop(rect.y() - rect.height() / 2);
0117     rect.setHeight(rect.height() * 2);
0118     viewport()->update(rect);
0119 }
0120 
0121 void TabTreeView::adjustStyleOption(QStyleOptionViewItem *option)
0122 {
0123     const QModelIndex index = option->index;
0124 
0125     option->state.setFlag(QStyle::State_Active, true);
0126     option->state.setFlag(QStyle::State_HasFocus, false);
0127     option->state.setFlag(QStyle::State_Selected, index.data(TabModel::CurrentTabRole).toBool());
0128 
0129     if (!index.isValid()) {
0130         option->viewItemPosition = QStyleOptionViewItem::Invalid;
0131     } else if (model()->rowCount() == 1) {
0132         option->viewItemPosition = QStyleOptionViewItem::OnlyOne;
0133     } else {
0134         if (!indexAbove(index).isValid()) {
0135             option->viewItemPosition = QStyleOptionViewItem::Beginning;
0136         } else if (!indexBelow(index).isValid()) {
0137             option->viewItemPosition = QStyleOptionViewItem::End;
0138         } else {
0139             option->viewItemPosition = QStyleOptionViewItem::Middle;
0140         }
0141     }
0142 }
0143 
0144 void TabTreeView::drawBranches(QPainter *, const QRect &, const QModelIndex &) const
0145 {
0146     // Disable drawing branches
0147 }
0148 
0149 void TabTreeView::currentChanged(const QModelIndex &current, const QModelIndex &previous)
0150 {
0151     if (current.data(TabModel::CurrentTabRole).toBool()) {
0152         QTreeView::currentChanged(current, previous);
0153     } else if (previous.data(TabModel::CurrentTabRole).toBool()) {
0154         setCurrentIndex(previous);
0155     }
0156 }
0157 
0158 void TabTreeView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles)
0159 {
0160     QTreeView::dataChanged(topLeft, bottomRight, roles);
0161 
0162     if (roles.size() == 1 && roles.at(0) == TabModel::CurrentTabRole && topLeft.data(TabModel::CurrentTabRole).toBool()) {
0163         setCurrentIndex(topLeft);
0164     }
0165 }
0166 
0167 void TabTreeView::rowsInserted(const QModelIndex &parent, int start, int end)
0168 {
0169     QTreeView::rowsInserted(parent, start, end);
0170 
0171     if (m_initializing) {
0172         return;
0173     }
0174 
0175     // Parent for WebTab is set after insertTab is emitted
0176     const QPersistentModelIndex index = model()->index(start, 0, parent);
0177     QTimer::singleShot(0, this, [=]() {
0178         if (!index.isValid()) {
0179             return;
0180         }
0181         QModelIndex idx = index;
0182         QVector<QModelIndex> stack;
0183         do {
0184             stack.append(idx);
0185             idx = idx.parent();
0186         } while (idx.isValid());
0187         for (const QModelIndex &index : std::as_const(stack)) {
0188             expand(index);
0189         }
0190         if (index.data(TabModel::CurrentTabRole).toBool()) {
0191             setCurrentIndex(index);
0192         }
0193     });
0194 }
0195 
0196 bool TabTreeView::viewportEvent(QEvent *event)
0197 {
0198     switch (event->type()) {
0199     case QEvent::MouseButtonPress: {
0200         auto *me = static_cast<QMouseEvent*>(event);
0201         const QModelIndex index = indexAt(me->pos());
0202         updateIndex(index);
0203         auto *tab = index.data(TabModel::WebTabRole).value<WebTab*>();
0204         if (me->buttons() == Qt::MiddleButton) {
0205             if (tab) {
0206                 if (isExpanded(index)) {
0207                     tab->closeTab();
0208                 } else {
0209                     closeTree(index);
0210                 }
0211             } else {
0212                 m_window->addTab();
0213             }
0214         }
0215         if (me->buttons() != Qt::LeftButton) {
0216             m_pressedIndex = QModelIndex();
0217             m_pressedButton = NoButton;
0218             break;
0219         }
0220         m_pressedIndex = index;
0221         m_pressedButton = buttonAt(me->pos(), m_pressedIndex);
0222         if (m_pressedIndex.isValid()) {
0223             if (m_pressedButton == ExpandButton) {
0224                 if (isExpanded(m_pressedIndex)) {
0225                     collapse(m_pressedIndex);
0226                 } else {
0227                     expand(m_pressedIndex);
0228                 }
0229                 me->accept();
0230                 return true;
0231             } else if (m_pressedButton == NoButton && tab) {
0232                 tab->makeCurrentTab();
0233             }
0234         }
0235         if (m_pressedButton == CloseButton) {
0236             me->accept();
0237             return true;
0238         }
0239         break;
0240     }
0241 
0242     case QEvent::MouseMove: {
0243         auto *me = static_cast<QMouseEvent*>(event);
0244         if (m_pressedButton == CloseButton) {
0245             me->accept();
0246             return true;
0247         }
0248         break;
0249     }
0250 
0251     case QEvent::MouseButtonRelease: {
0252         auto *me = static_cast<QMouseEvent*>(event);
0253         if (me->buttons() != Qt::NoButton) {
0254             break;
0255         }
0256         const QModelIndex index = indexAt(me->pos());
0257         updateIndex(index);
0258         if (m_pressedIndex != index) {
0259             break;
0260         }
0261         DelegateButton button = buttonAt(me->pos(), index);
0262         if (m_pressedButton == button) {
0263             if (m_pressedButton == ExpandButton) {
0264                 me->accept();
0265                 return true;
0266             }
0267             auto *tab = index.data(TabModel::WebTabRole).value<WebTab*>();
0268             if (tab) {
0269                 if (m_pressedButton == CloseButton) {
0270                     tab->closeTab();
0271                 } else if (m_pressedButton == AudioButton) {
0272                     tab->toggleMuted();
0273                 }
0274             }
0275         }
0276         if (m_pressedButton == CloseButton) {
0277             me->accept();
0278             return true;
0279         }
0280         break;
0281     }
0282 
0283     case QEvent::MouseButtonDblClick: {
0284         auto *me = static_cast<QMouseEvent*>(event);
0285         const QModelIndex index = indexAt(me->pos());
0286         if (me->button() == Qt::LeftButton && !index.isValid()) {
0287             m_window->addTab();
0288         }
0289         break;
0290     }
0291 
0292     case QEvent::HoverEnter:
0293     case QEvent::HoverLeave:
0294     case QEvent::HoverMove: {
0295         auto *he = static_cast<QHoverEvent*>(event);
0296         updateIndex(m_hoveredIndex);
0297         m_hoveredIndex = indexAt(he->position().toPoint());
0298         updateIndex(m_hoveredIndex);
0299         break;
0300     }
0301 
0302     case QEvent::ToolTip: {
0303         auto *he = static_cast<QHelpEvent*>(event);
0304         const QModelIndex index = indexAt(he->pos());
0305         DelegateButton button = buttonAt(he->pos(), index);
0306         if (button == AudioButton) {
0307             const bool muted = index.data(TabModel::AudioMutedRole).toBool();
0308             QToolTip::showText(he->globalPos(), muted ? tr("Unmute Tab") : tr("Mute Tab"), this, visualRect(index));
0309             he->accept();
0310             return true;
0311         } else if (button == CloseButton) {
0312             QToolTip::showText(he->globalPos(), tr("Close Tab"), this, visualRect(index));
0313             he->accept();
0314             return true;
0315         } else if (button == NoButton) {
0316             QToolTip::showText(he->globalPos(), index.data().toString(), this, visualRect(index));
0317             he->accept();
0318             return true;
0319         }
0320         break;
0321     }
0322 
0323     case QEvent::ContextMenu: {
0324         auto *ce = static_cast<QContextMenuEvent*>(event);
0325         const QModelIndex index = indexAt(ce->pos());
0326         auto *tab = index.data(TabModel::WebTabRole).value<WebTab*>();
0327         const int tabIndex = tab ? tab->tabIndex() : -1;
0328         TabContextMenu::Options options = TabContextMenu::VerticalTabs | TabContextMenu::ShowDetachTabAction;
0329         if (m_tabsInOrder) {
0330             options |= TabContextMenu::ShowCloseOtherTabsActions;
0331         }
0332         TabContextMenu menu(tabIndex, m_window, options);
0333         addMenuActions(&menu, index);
0334         menu.exec(ce->globalPos());
0335         break;
0336     }
0337 
0338     default:
0339         break;
0340     }
0341     return QTreeView::viewportEvent(event);
0342 }
0343 
0344 void TabTreeView::initView()
0345 {
0346     // Restore expanded state
0347     for (int i = 0; i < model()->rowCount(); ++i) {
0348         const QModelIndex index = model()->index(i, 0);
0349         reverseTraverse(index, [this](const QModelIndex &index) {
0350             auto *tab = index.data(TabModel::WebTabRole).value<WebTab*>();
0351             if (tab) {
0352                 setExpanded(index, tab->sessionData().value(m_expandedSessionKey, true).toBool());
0353             }
0354         });
0355     }
0356 
0357     m_initializing = false;
0358 }
0359 
0360 TabTreeView::DelegateButton TabTreeView::buttonAt(const QPoint &pos, const QModelIndex &index) const
0361 {
0362     if (m_delegate->expandButtonRect(index).contains(pos)) {
0363         if (model()->rowCount(index) > 0) {
0364             return ExpandButton;
0365         }
0366     } else if (m_delegate->audioButtonRect(index).contains(pos)) {
0367         return AudioButton;
0368     } else if (m_delegate->closeButtonRect(index).contains(pos)) {
0369         return CloseButton;
0370     }
0371     return NoButton;
0372 }
0373 
0374 void TabTreeView::addMenuActions(QMenu *menu, const QModelIndex &index)
0375 {
0376     if (!m_haveTreeModel) {
0377         return;
0378     }
0379 
0380     menu->addSeparator();
0381     QMenu *m = menu->addMenu(tr("Tab Tree"));
0382 
0383     if (index.isValid() && model()->rowCount(index) > 0) {
0384         QPersistentModelIndex pindex = index;
0385         m->addAction(tr("Close Tree"), this, [=]() {
0386             closeTree(pindex);
0387         });
0388         m->addAction(tr("Unload Tree"), this, [=]() {
0389             unloadTree(pindex);
0390         });
0391     }
0392 
0393     m->addSeparator();
0394     m->addAction(tr("Expand All"), this, &TabTreeView::expandAll);
0395     m->addAction(tr("Collapse All"), this, &TabTreeView::collapseAll);
0396 }
0397 
0398 void TabTreeView::reverseTraverse(const QModelIndex &root, const std::function<void(const QModelIndex&)> &callback) const
0399 {
0400     if (!root.isValid()) {
0401         return;
0402     }
0403     for (int i = 0; i < model()->rowCount(root); ++i) {
0404         reverseTraverse(model()->index(i, 0, root), callback);
0405     }
0406     callback(root);
0407 }
0408 
0409 void TabTreeView::closeTree(const QModelIndex &root)
0410 {
0411     QVector<WebTab*> tabs;
0412     reverseTraverse(root, [&](const QModelIndex &index) {
0413         auto *tab = index.data(TabModel::WebTabRole).value<WebTab*>();
0414         if (tab) {
0415             tabs.append(tab);
0416         }
0417     });
0418     for (WebTab *tab : std::as_const(tabs)) {
0419         tab->closeTab();
0420     }
0421 }
0422 
0423 void TabTreeView::unloadTree(const QModelIndex &root)
0424 {
0425     reverseTraverse(root, [&](const QModelIndex &index) {
0426         auto *tab = index.data(TabModel::WebTabRole).value<WebTab*>();
0427         if (tab && tab->isRestored()) {
0428             tab->unload();
0429         }
0430     });
0431 }