From e57fb65c955847e5070919e817486b8b25db2f87 Mon Sep 17 00:00:00 2001 From: David Rosca Date: Wed, 31 Jan 2018 18:48:38 +0100 Subject: [PATCH] Add TabTreeModel This model orders tabs in "Tree Style Tabs" fashion. --- autotests/tabmodeltest.cpp | 98 ++++++++ autotests/tabmodeltest.h | 1 + src/lib/CMakeLists.txt | 1 + src/lib/tabwidget/tabtreemodel.cpp | 390 +++++++++++++++++++++++++++++ src/lib/tabwidget/tabtreemodel.h | 67 +++++ src/lib/webtab/webtab.cpp | 13 +- 6 files changed, 566 insertions(+), 4 deletions(-) create mode 100644 src/lib/tabwidget/tabtreemodel.cpp create mode 100644 src/lib/tabwidget/tabtreemodel.h diff --git a/autotests/tabmodeltest.cpp b/autotests/tabmodeltest.cpp index 541f68e0a..92123af3e 100644 --- a/autotests/tabmodeltest.cpp +++ b/autotests/tabmodeltest.cpp @@ -18,6 +18,7 @@ #include "tabmodeltest.h" #include "autotests.h" #include "tabmodel.h" +#include "tabtreemodel.h" #include "webtab.h" #include "tabwidget.h" #include "tabbedwebview.h" @@ -118,5 +119,102 @@ void TabModelTest::dataTest() delete w; } +void TabModelTest::treeModelTest() +{ + BrowserWindow *w = mApp->createWindow(Qz::BW_NewWindow); + + TabModel sourceModel(w); + TabTreeModel model; + model.setSourceModel(&sourceModel); + ModelTest modelTest(&model); + + w->tabWidget()->addView(QUrl()); + w->tabWidget()->addView(QUrl()); + w->tabWidget()->addView(QUrl()); + w->tabWidget()->addView(QUrl()); + w->tabWidget()->addView(QUrl()); + + QTRY_COMPARE(model.rowCount(QModelIndex()), 6); + + WebTab *tab1 = w->weView(0)->webTab(); + WebTab *tab2 = w->weView(1)->webTab(); + WebTab *tab3 = w->weView(2)->webTab(); + WebTab *tab4 = w->weView(3)->webTab(); + WebTab *tab5 = w->weView(4)->webTab(); + WebTab *tab6 = w->weView(5)->webTab(); + + QCOMPARE(model.index(0, 0).data(TabModel::WebTabRole).value(), tab1); + QCOMPARE(model.index(1, 0).data(TabModel::WebTabRole).value(), tab2); + QCOMPARE(model.index(2, 0).data(TabModel::WebTabRole).value(), tab3); + QCOMPARE(model.index(3, 0).data(TabModel::WebTabRole).value(), tab4); + QCOMPARE(model.index(4, 0).data(TabModel::WebTabRole).value(), tab5); + QCOMPARE(model.index(5, 0).data(TabModel::WebTabRole).value(), tab6); + + QPersistentModelIndex tab1index = model.index(0, 0); + QPersistentModelIndex tab2index = model.index(1, 0); + QPersistentModelIndex tab3index = model.index(2, 0); + QPersistentModelIndex tab4index = model.index(3, 0); + QPersistentModelIndex tab5index = model.index(4, 0); + QPersistentModelIndex tab6index = model.index(5, 0); + + QCOMPARE(model.rowCount(tab1index), 0); + tab1->addChildTab(tab2); + + QCOMPARE(model.rowCount(tab1index), 1); + QCOMPARE(model.index(0, 0, tab1index).data(TabModel::WebTabRole).value(), tab2); + + tab1->addChildTab(tab3); + QCOMPARE(model.rowCount(tab1index), 2); + QCOMPARE(model.index(0, 0, tab1index).data(TabModel::WebTabRole).value(), tab2); + QCOMPARE(model.index(1, 0, tab1index).data(TabModel::WebTabRole).value(), tab3); + + tab1->addChildTab(tab4, 1); + QCOMPARE(model.rowCount(tab1index), 3); + QCOMPARE(model.index(0, 0, tab1index).data(TabModel::WebTabRole).value(), tab2); + QCOMPARE(model.index(1, 0, tab1index).data(TabModel::WebTabRole).value(), tab4); + QCOMPARE(model.index(2, 0, tab1index).data(TabModel::WebTabRole).value(), tab3); + + tab4->addChildTab(tab5); + tab4->addChildTab(tab6); + + QCOMPARE(model.rowCount(tab4index), 2); + QCOMPARE(model.index(0, 0, tab4index).data(TabModel::WebTabRole).value(), tab5); + QCOMPARE(model.index(1, 0, tab4index).data(TabModel::WebTabRole).value(), tab6); + + w->tabWidget()->closeTab(tab4->tabIndex()); + + QCOMPARE(model.rowCount(tab1index), 4); + QCOMPARE(model.index(0, 0, tab1index).data(TabModel::WebTabRole).value(), tab2); + QCOMPARE(model.index(1, 0, tab1index).data(TabModel::WebTabRole).value(), tab5); + QCOMPARE(model.index(2, 0, tab1index).data(TabModel::WebTabRole).value(), tab6); + QCOMPARE(model.index(3, 0, tab1index).data(TabModel::WebTabRole).value(), tab3); + + tab1->addChildTab(tab3, 0); + + QCOMPARE(model.rowCount(tab1index), 4); + QCOMPARE(model.index(0, 0, tab1index).data(TabModel::WebTabRole).value(), tab3); + QCOMPARE(model.index(1, 0, tab1index).data(TabModel::WebTabRole).value(), tab2); + QCOMPARE(model.index(2, 0, tab1index).data(TabModel::WebTabRole).value(), tab5); + QCOMPARE(model.index(3, 0, tab1index).data(TabModel::WebTabRole).value(), tab6); + + tab2->setParentTab(nullptr); + + QCOMPARE(model.rowCount(tab1index), 3); + QCOMPARE(model.index(0, 0).data(TabModel::WebTabRole).value(), tab2); + QCOMPARE(model.index(0, 0, tab1index).data(TabModel::WebTabRole).value(), tab3); + QCOMPARE(model.index(1, 0, tab1index).data(TabModel::WebTabRole).value(), tab5); + QCOMPARE(model.index(2, 0, tab1index).data(TabModel::WebTabRole).value(), tab6); + + w->tabWidget()->closeTab(tab1->tabIndex()); + + QCOMPARE(model.rowCount(QModelIndex()), 4); + QCOMPARE(model.index(0, 0).data(TabModel::WebTabRole).value(), tab2); + QCOMPARE(model.index(1, 0).data(TabModel::WebTabRole).value(), tab3); + QCOMPARE(model.index(2, 0).data(TabModel::WebTabRole).value(), tab5); + QCOMPARE(model.index(3, 0).data(TabModel::WebTabRole).value(), tab6); + + QTest::qWait(1); + delete w; +} FALKONTEST_MAIN(TabModelTest) diff --git a/autotests/tabmodeltest.h b/autotests/tabmodeltest.h index 564299840..384d54309 100644 --- a/autotests/tabmodeltest.h +++ b/autotests/tabmodeltest.h @@ -29,4 +29,5 @@ private slots: void basicTest(); void dataTest(); + void treeModelTest(); }; diff --git a/src/lib/CMakeLists.txt b/src/lib/CMakeLists.txt index 9f91a6ca1..097dd7598 100644 --- a/src/lib/CMakeLists.txt +++ b/src/lib/CMakeLists.txt @@ -180,6 +180,7 @@ set(SRCS ${SRCS} tabwidget/tabbar.cpp tabwidget/tabicon.cpp tabwidget/tabmodel.cpp + tabwidget/tabtreemodel.cpp tabwidget/tabstackedwidget.cpp tabwidget/tabwidget.cpp tabwidget/tabcontextmenu.cpp diff --git a/src/lib/tabwidget/tabtreemodel.cpp b/src/lib/tabwidget/tabtreemodel.cpp new file mode 100644 index 000000000..02692eb5f --- /dev/null +++ b/src/lib/tabwidget/tabtreemodel.cpp @@ -0,0 +1,390 @@ +/* ============================================================ +* Falkon - Qt web browser +* Copyright (C) 2018 David Rosca +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +* ============================================================ */ +#include "tabtreemodel.h" +#include "tabmodel.h" +#include "webtab.h" + +#include + +class TabTreeModelItem +{ +public: + explicit TabTreeModelItem(WebTab *tab = nullptr, const QModelIndex &index = QModelIndex()); + ~TabTreeModelItem(); + + bool isRoot() const; + void setParent(TabTreeModelItem *item); + void addChild(TabTreeModelItem *item, int index = -1); + + WebTab *tab = nullptr; + TabTreeModelItem *parent = nullptr; + QVector children; + QPersistentModelIndex sourceIndex; +}; + +TabTreeModelItem::TabTreeModelItem(WebTab *tab, const QModelIndex &index) + : tab(tab) + , sourceIndex(index) +{ +} + +TabTreeModelItem::~TabTreeModelItem() +{ + for (TabTreeModelItem *child : qAsConst(children)) { + delete child; + } +} + +bool TabTreeModelItem::isRoot() const +{ + return !tab; +} + +void TabTreeModelItem::setParent(TabTreeModelItem *item) +{ + if (parent == item) { + return; + } + if (parent) { + parent->children.removeOne(this); + } + parent = item; + if (parent) { + parent->children.append(this); + } +} + +void TabTreeModelItem::addChild(TabTreeModelItem *item, int index) +{ + item->setParent(nullptr); + item->parent = this; + if (index < 0 || index > children.size()) { + children.append(item); + } else { + children.insert(index, item); + } +} + +TabTreeModel::TabTreeModel(QObject *parent) + : QAbstractProxyModel(parent) +{ + connect(this, &QAbstractProxyModel::sourceModelChanged, this, &TabTreeModel::init); +} + +TabTreeModel::~TabTreeModel() +{ + delete m_root; +} + +QModelIndex TabTreeModel::tabIndex(WebTab *tab) const +{ + TabTreeModelItem *item = m_items.value(tab); + if (!item) { + return QModelIndex(); + } + return createIndex(item->parent->children.indexOf(item), 0, item); +} + +WebTab *TabTreeModel::tab(const QModelIndex &index) const +{ + TabTreeModelItem *it = item(index); + return it ? it->tab : nullptr; +} + +Qt::ItemFlags TabTreeModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) { + return Qt::ItemIsDropEnabled; + } + return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled; +} + +QVariant TabTreeModel::data(const QModelIndex &index, int role) const +{ + return sourceModel()->data(mapToSource(index), role); +} + +int TabTreeModel::rowCount(const QModelIndex &parent) const +{ + TabTreeModelItem *it = item(parent); + if (!it) { + return 0; + } + return it->children.count(); +} + +int TabTreeModel::columnCount(const QModelIndex &parent) const +{ + if (parent.column() > 0) { + return 0; + } + return 1; +} + +bool TabTreeModel::hasChildren(const QModelIndex &parent) const +{ + TabTreeModelItem *it = item(parent); + if (!it) { + return false; + } + return !it->children.isEmpty(); +} + +QModelIndex TabTreeModel::parent(const QModelIndex &child) const +{ + TabTreeModelItem *it = item(child); + if (!it) { + return QModelIndex(); + } + return index(it->parent); +} + +QModelIndex TabTreeModel::index(int row, int column, const QModelIndex &parent) const +{ + if (!hasIndex(row, column, parent)) { + return QModelIndex(); + } + TabTreeModelItem *parentItem = item(parent); + return createIndex(row, column, parentItem->children.at(row)); +} + +QModelIndex TabTreeModel::mapFromSource(const QModelIndex &sourceIndex) const +{ + return tabIndex(sourceIndex.data(TabModel::WebTabRole).value()); +} + +QModelIndex TabTreeModel::mapToSource(const QModelIndex &proxyIndex) const +{ + TabTreeModelItem *it = item(proxyIndex); + if (!it) { + return QModelIndex(); + } + return it->sourceIndex; +} + +bool TabTreeModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) +{ + if (action == Qt::IgnoreAction) { + return true; + } + + const QString mimeType = mimeTypes().at(0); + + if (!data->hasFormat(mimeType) || column > 0) { + return false; + } + + QByteArray encodedData = data->data(mimeType); + QDataStream stream(&encodedData, QIODevice::ReadOnly); + + QVector tabs; + while (!stream.atEnd()) { + int index; + stream >> index; + WebTab *tab = sourceModel()->index(index, 0).data(TabModel::WebTabRole).value(); + if (tab) { + tabs.append(tab); + } + } + + if (tabs.isEmpty()) { + return false; + } + + // Only support moving one tab + WebTab *tab = tabs.at(0); + + TabTreeModelItem *it = m_items.value(tab); + TabTreeModelItem *parentItem = item(parent); + if (!it || !parentItem) { + return false; + } + + if (!parentItem->tab) { + tab->setParentTab(nullptr); + if (row < 0) { + row = m_root->children.count(); + } + const QModelIndex fromIdx = index(it); + const int childPos = row > fromIdx.row() ? row - 1 : row; + if (!beginMoveRows(fromIdx.parent(), fromIdx.row(), fromIdx.row(), QModelIndex(), row)) { + qWarning() << "Invalid beginMoveRows" << fromIdx.parent() << fromIdx.row() << "root" << row; + return true; + } + m_root->addChild(it, childPos); + endMoveRows(); + } else { + parentItem->tab->addChildTab(tab, row); + } + + return true; +} + +void TabTreeModel::init() +{ + delete m_root; + m_items.clear(); + + m_root = new TabTreeModelItem; + + for (int i = 0; i < sourceModel()->rowCount(); ++i) { + const QModelIndex index = sourceModel()->index(i, 0); + WebTab *tab = index.data(TabModel::WebTabRole).value(); + if (tab && !tab->parentTab()) { + TabTreeModelItem *item = new TabTreeModelItem(tab, index); + m_items[tab] = item; + m_root->addChild(createItems(item)); + } + } + + for (TabTreeModelItem *item : qAsConst(m_items)) { + connectTab(item->tab); + } + + connect(sourceModel(), &QAbstractItemModel::dataChanged, this, &TabTreeModel::sourceDataChanged); + connect(sourceModel(), &QAbstractItemModel::rowsInserted, this, &TabTreeModel::sourceRowsInserted); + connect(sourceModel(), &QAbstractItemModel::rowsAboutToBeRemoved, this, &TabTreeModel::sourceRowsAboutToBeRemoved); +} + +QModelIndex TabTreeModel::index(TabTreeModelItem *item) const +{ + if (!item || item == m_root) { + return QModelIndex(); + } + return createIndex(item->parent->children.indexOf(item), 0, item); +} + +TabTreeModelItem *TabTreeModel::item(const QModelIndex &index) const +{ + TabTreeModelItem *it = static_cast(index.internalPointer()); + return it ? it : m_root; +} + +TabTreeModelItem *TabTreeModel::createItems(TabTreeModelItem *root) +{ + const auto children = root->tab->childTabs(); + for (WebTab *child : children) { + const QModelIndex index = sourceModel()->index(child->tabIndex(), 0); + TabTreeModelItem *item = new TabTreeModelItem(child, index); + m_items[child] = item; + root->addChild(createItems(item)); + } + return root; +} + +void TabTreeModel::sourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) +{ + emit dataChanged(mapFromSource(topLeft), mapFromSource(bottomRight), roles); +} + +void TabTreeModel::sourceRowsInserted(const QModelIndex &parent, int start, int end) +{ + for (int i = start; i <= end; ++i) { + insertIndex(sourceModel()->index(i, 0, parent)); + } +} + +void TabTreeModel::sourceRowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) +{ + for (int i = start; i <= end; ++i) { + removeIndex(sourceModel()->index(i, 0, parent)); + } +} + +void TabTreeModel::insertIndex(const QModelIndex &sourceIndex) +{ + WebTab *tab = sourceIndex.data(TabModel::WebTabRole).value(); + if (!tab) { + return; + } + TabTreeModelItem *parent = m_items.value(tab->parentTab()); + if (!parent) { + parent = m_root; + } + TabTreeModelItem *item = new TabTreeModelItem(tab, sourceIndex); + + const int idx = parent->children.count(); + beginInsertRows(tabIndex(tab->parentTab()), idx, idx); + m_items[tab] = item; + parent->addChild(item); + endInsertRows(); + + connectTab(tab); +} + +void TabTreeModel::removeIndex(const QModelIndex &sourceIndex) +{ + WebTab *tab = sourceIndex.data(TabModel::WebTabRole).value(); + if (!tab) { + return; + } + TabTreeModelItem *item = m_items.value(tab); + if (!item) { + return; + } + + const QModelIndex index = mapFromSource(sourceIndex); + beginRemoveRows(index.parent(), index.row(), index.row()); + item->setParent(nullptr); + Q_ASSERT(item->children.isEmpty()); + delete item; + endRemoveRows(); + + tab->disconnect(this); +} + +void TabTreeModel::connectTab(WebTab *tab) +{ + TabTreeModelItem *item = m_items.value(tab); + Q_ASSERT(item); + + connect(tab, &WebTab::parentTabChanged, this, [=](WebTab *parent) { + // Handle only move to root, everything else is done in childTabAdded + if (item->parent == m_root || parent) { + return; + } + int pos = m_root->children.count(); + // Move it to the same spot as old parent + if (item->parent->parent == m_root) { + pos = m_root->children.indexOf(item->parent); + } + const QModelIndex fromIdx = index(item); + if (!beginMoveRows(fromIdx.parent(), fromIdx.row(), fromIdx.row(), QModelIndex(), pos)) { + qWarning() << "Invalid beginMoveRows" << fromIdx.parent() << fromIdx.row() << "root" << pos; + return; + } + m_root->addChild(item, pos); + endMoveRows(); + }); + + connect(tab, &WebTab::childTabAdded, this, [=](WebTab *child, int pos) { + TabTreeModelItem *from = m_items.value(child); + if (!from) { + return; + } + const QModelIndex fromIdx = index(from); + const QModelIndex toIdx = index(item); + const int childPos = fromIdx.parent() == toIdx && pos > fromIdx.row() ? pos - 1 : pos; + if (!beginMoveRows(fromIdx.parent(), fromIdx.row(), fromIdx.row(), toIdx, pos)) { + qWarning() << "Invalid beginMoveRows" << fromIdx.parent() << fromIdx.row() << toIdx << pos; + return; + } + item->addChild(from, childPos); + endMoveRows(); + }); +} diff --git a/src/lib/tabwidget/tabtreemodel.h b/src/lib/tabwidget/tabtreemodel.h new file mode 100644 index 000000000..3e1e86edb --- /dev/null +++ b/src/lib/tabwidget/tabtreemodel.h @@ -0,0 +1,67 @@ +/* ============================================================ +* Falkon - Qt web browser +* Copyright (C) 2018 David Rosca +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +* ============================================================ */ +#pragma once + +#include + +#include "qzcommon.h" + +class WebTab; +class TabTreeModelItem; + +class FALKON_EXPORT TabTreeModel : public QAbstractProxyModel +{ + Q_OBJECT + +public: + explicit TabTreeModel(QObject *parent = nullptr); + ~TabTreeModel(); + + QModelIndex tabIndex(WebTab *tab) const; + WebTab *tab(const QModelIndex &index) const; + + Qt::ItemFlags flags(const QModelIndex &index) const override; + QVariant data(const QModelIndex &index, int role) const override; + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + bool hasChildren(const QModelIndex &parent) const override; + QModelIndex parent(const QModelIndex &child) const override; + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + + QModelIndex mapFromSource(const QModelIndex &sourceIndex) const override; + QModelIndex mapToSource(const QModelIndex &proxyIndex) const override; + + bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override; + +private: + void init(); + QModelIndex index(TabTreeModelItem *item) const; + TabTreeModelItem *item(const QModelIndex &index) const; + TabTreeModelItem *createItems(TabTreeModelItem *root); + + void sourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles); + void sourceRowsInserted(const QModelIndex &parent, int start, int end); + void sourceRowsAboutToBeRemoved(const QModelIndex &parent, int start, int end); + + void insertIndex(const QModelIndex &sourceIndex); + void removeIndex(const QModelIndex &sourceIndex); + void connectTab(WebTab *tab); + + TabTreeModelItem *m_root = nullptr; + QHash m_items; +}; diff --git a/src/lib/webtab/webtab.cpp b/src/lib/webtab/webtab.cpp index e746eee87..0883ab8ee 100644 --- a/src/lib/webtab/webtab.cpp +++ b/src/lib/webtab/webtab.cpp @@ -444,12 +444,17 @@ void WebTab::setParentTab(WebTab *tab) void WebTab::addChildTab(WebTab *tab, int index) { - if (tab->parentTab()) { - tab->setParentTab(nullptr); - } - tab->m_parentTab = this; + WebTab *tabParent = tab->m_parentTab; + if (tabParent) { + const int index = tabParent->m_childTabs.indexOf(tab); + if (index >= 0) { + tabParent->m_childTabs.removeAt(index); + emit tabParent->childTabRemoved(tab, index); + } + } + if (index < 0 || index > m_childTabs.size()) { m_childTabs.append(tab); emit childTabAdded(tab, m_childTabs.size() - 1);