summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to 'Swift/QtUI/Trellis')
-rw-r--r--Swift/QtUI/Trellis/QtDNDTabBar.cpp164
-rw-r--r--Swift/QtUI/Trellis/QtDNDTabBar.h44
-rw-r--r--Swift/QtUI/Trellis/QtDynamicGridLayout.cpp561
-rw-r--r--Swift/QtUI/Trellis/QtDynamicGridLayout.h84
-rw-r--r--Swift/QtUI/Trellis/QtGridSelectionDialog.cpp194
-rw-r--r--Swift/QtUI/Trellis/QtGridSelectionDialog.h66
6 files changed, 1113 insertions, 0 deletions
diff --git a/Swift/QtUI/Trellis/QtDNDTabBar.cpp b/Swift/QtUI/Trellis/QtDNDTabBar.cpp
new file mode 100644
index 0000000..3ae2124
--- /dev/null
+++ b/Swift/QtUI/Trellis/QtDNDTabBar.cpp
@@ -0,0 +1,164 @@
+/*
+ * Copyright (c) 2014-2017 Isode Limited.
+ * All rights reserved.
+ * See the COPYING file for more information.
+ */
+
+#include <Swift/QtUI/Trellis/QtDNDTabBar.h>
+
+#include <cassert>
+
+#include <QDrag>
+#include <QDropEvent>
+#include <QMimeData>
+#include <QMouseEvent>
+#include <QPainter>
+#include <QTabWidget>
+
+namespace Swift {
+
+QtDNDTabBar::QtDNDTabBar(QWidget* parent) : QTabBar(parent) {
+ setAcceptDrops(true);
+
+ // detect the default tab bar height;
+ insertTab(0, "");
+ defaultTabHeight = QTabBar::sizeHint().height();
+ removeTab(0);
+}
+
+QtDNDTabBar::~QtDNDTabBar() {
+
+}
+
+int QtDNDTabBar::getDragIndex() const {
+ return dragIndex;
+}
+
+QString QtDNDTabBar::getDragText() const {
+ return dragText;
+}
+
+QWidget* QtDNDTabBar::getDragWidget() const {
+ return dragWidget;
+}
+
+QSize QtDNDTabBar::sizeHint() const {
+ QSize hint = QTabBar::sizeHint();
+ if (hint.isEmpty()) {
+ hint = QSize(parentWidget()->width(), defaultTabHeight);
+ }
+ return hint;
+}
+
+QSize QtDNDTabBar::tabSizeHint(int index) const {
+ QSize tabSize = QTabBar::tabSizeHint(index);
+#if defined(Q_OS_MAC)
+ // With multiple tabs having the same label in a QTabBar, the size hint computed by
+ // Qt on OS X is too small and it is elided even though there is enough horizontal
+ // space available. We work around this issue by adding the width of a letter to the
+ // size hint.
+ tabSize += QSize(QFontMetrics(font()).width("I"), 0);
+#endif
+ return tabSize;
+}
+
+void QtDNDTabBar::dragEnterEvent(QDragEnterEvent* dragEnterEvent) {
+ QtDNDTabBar* sourceTabBar = dynamic_cast<QtDNDTabBar*>(dragEnterEvent->source());
+ if (sourceTabBar) {
+ dragEnterEvent->acceptProposedAction();
+ }
+}
+
+void QtDNDTabBar::dropEvent(QDropEvent* dropEvent) {
+ auto sourceTabBar = dynamic_cast<QtDNDTabBar*>(dropEvent->source());
+ if (sourceTabBar && dropEvent->mimeData() && dropEvent->mimeData()->data("action") == QByteArray("application/tab-detach")) {
+ int targetTabIndex = tabAt(dropEvent->pos());
+ QRect rect = tabRect(targetTabIndex);
+ if (targetTabIndex >= 0 && (dropEvent->pos().x() - rect.left() - rect.width()/2 > 0)) {
+ targetTabIndex++;
+ }
+
+ QWidget* tab = sourceTabBar->getDragWidget();
+ assert(tab);
+ QTabWidget* targetTabWidget = dynamic_cast<QTabWidget*>(parentWidget());
+
+ QString tabText = sourceTabBar->getDragText();
+
+ /*
+ * When you add a widget to an empty QTabWidget, it's automatically made the current widget.
+ * Making the widget the current widget, widget->show() is called for the widget. Directly reacting
+ * to that event, and adding the widget again to the QTabWidget results in undefined behavior. For
+ * example the tab label is shown but the widget is neither has the old nor in the new QTabWidget as
+ * parent. Blocking signals on the QWidget to be added to a QTabWidget prevents this behavior.
+ */
+ targetTabWidget->setUpdatesEnabled(false);
+ tab->blockSignals(true);
+ targetTabWidget->insertTab(targetTabIndex, tab, tabText);
+ dropEvent->acceptProposedAction();
+ tab->blockSignals(false);
+ targetTabWidget->setUpdatesEnabled(true);
+ onDropSucceeded();
+ }
+}
+
+bool QtDNDTabBar::event(QEvent* event) {
+ QMouseEvent* mouseEvent = dynamic_cast<QMouseEvent*>(event);
+ if (mouseEvent) {
+ QWidget* childAtPoint = window()->childAt(mapTo(window(), mouseEvent->pos()));
+ QtDNDTabBar* underMouse = dynamic_cast<QtDNDTabBar*>(childAtPoint);
+ if (!underMouse && childAtPoint) {
+ underMouse = dynamic_cast<QtDNDTabBar*>(childAtPoint->parent());
+ }
+ if (!underMouse && currentIndex() >= 0) {
+ // detach and drag
+
+ // stop move event
+ QMouseEvent* finishMoveEvent = new QMouseEvent (QEvent::MouseMove, mouseEvent->pos (), Qt::NoButton, Qt::NoButton, Qt::NoModifier);
+ QTabBar::event(finishMoveEvent);
+ delete finishMoveEvent;
+ finishMoveEvent = nullptr;
+
+ // start drag
+ QDrag* drag = new QDrag(this);
+ QMimeData* mimeData = new QMimeData;
+
+ // distinguish tab-reordering drops from other ones
+ mimeData->setData("action", "application/tab-detach") ;
+ drag->setMimeData(mimeData);
+
+ // set drag image
+ QRect rect = tabRect( currentIndex() );
+#if QT_VERSION >= 0x050000
+ QPixmap pixmap = grab(rect);
+#else
+ QPixmap pixmap = QPixmap::grabWidget(this, rect);
+#endif
+ QPixmap targetPixmap (pixmap.size ());
+ QPainter painter (&targetPixmap);
+ painter.setOpacity(0.9);
+ painter.drawPixmap(0,0, pixmap);
+ painter.end ();
+ drag->setPixmap (targetPixmap);
+
+ drag->setHotSpot(QPoint(drag->pixmap().width()/2, drag->pixmap().height()));
+
+ dragIndex = currentIndex();
+ dragText = tabText(dragIndex);
+ dragWidget = dynamic_cast<QTabWidget*>(parent())->widget(dragIndex);
+ assert(dragWidget);
+ dynamic_cast<QTabWidget*>(parent())->removeTab(currentIndex());
+ Qt::DropAction dropAction = drag->exec();
+ if (dropAction == Qt::IgnoreAction) {
+ // aborted drag, put tab back in place
+ // disable event handling during the insert for the tab to prevent infinite recursion (stack overflow)
+ dragWidget->blockSignals(true);
+ dynamic_cast<QTabWidget*>(parent())->insertTab(dragIndex, dragWidget, dragText);
+ dragWidget->blockSignals(false);
+ }
+ return true;
+ }
+ }
+ return QTabBar::event(event);
+}
+
+}
diff --git a/Swift/QtUI/Trellis/QtDNDTabBar.h b/Swift/QtUI/Trellis/QtDNDTabBar.h
new file mode 100644
index 0000000..6de04d5
--- /dev/null
+++ b/Swift/QtUI/Trellis/QtDNDTabBar.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2014-2016 Isode Limited.
+ * All rights reserved.
+ * See the COPYING file for more information.
+ */
+
+#pragma once
+
+#include <QTabBar>
+
+#include <Swift/QtUI/QtTabWidget.h>
+
+namespace Swift {
+
+class QtDNDTabBar : public QTabBar {
+ Q_OBJECT
+ public:
+ explicit QtDNDTabBar(QWidget* parent = nullptr);
+ virtual ~QtDNDTabBar();
+
+ int getDragIndex() const;
+ QString getDragText() const;
+ QWidget* getDragWidget() const;
+
+ virtual QSize sizeHint() const;
+
+ friend class QtTabWidget;
+ signals:
+ void onDropSucceeded();
+
+ protected:
+ virtual void dragEnterEvent(QDragEnterEvent* dragEnterEvent);
+ virtual void dropEvent(QDropEvent* dropEvent);
+ virtual bool event(QEvent* event);
+ virtual QSize tabSizeHint(int index) const;
+
+ private:
+ int defaultTabHeight;
+ int dragIndex = -1;
+ QString dragText;
+ QWidget* dragWidget = nullptr;
+};
+
+}
diff --git a/Swift/QtUI/Trellis/QtDynamicGridLayout.cpp b/Swift/QtUI/Trellis/QtDynamicGridLayout.cpp
new file mode 100644
index 0000000..53e2733
--- /dev/null
+++ b/Swift/QtUI/Trellis/QtDynamicGridLayout.cpp
@@ -0,0 +1,561 @@
+/*
+ * Copyright (c) 2014-2019 Isode Limited.
+ * All rights reserved.
+ * See the COPYING file for more information.
+ */
+
+#include <Swift/QtUI/Trellis/QtDynamicGridLayout.h>
+
+#include <cassert>
+
+#include <QApplication>
+#include <QEvent>
+#include <QGridLayout>
+#include <QLayoutItem>
+#include <QtDebug>
+
+#include <Swiften/Base/Log.h>
+
+#include <Swift/QtUI/QtSwiftUtil.h>
+#include <Swift/QtUI/QtTabWidget.h>
+#include <Swift/QtUI/QtTabbable.h>
+#include <Swift/QtUI/Trellis/QtDNDTabBar.h>
+
+namespace Swift {
+
+QtDynamicGridLayout::QtDynamicGridLayout(bool future, QWidget* parent, bool enableDND) : QWidget(parent), dndEnabled_(enableDND), movingTab_(nullptr), future_(future) {
+ gridLayout_ = new QGridLayout(this);
+ setContentsMargins(0,0,0,0);
+ setDimensions(QSize(1,1));
+ connect(qApp, SIGNAL(focusChanged(QWidget*, QWidget*)), this, SLOT(handleApplicationFocusChanged(QWidget*,QWidget*)));
+}
+
+QtDynamicGridLayout::~QtDynamicGridLayout() {
+}
+
+int QtDynamicGridLayout::addTab(QtTabbable* tab, const QString& title) {
+ assert(gridLayout_->rowCount() > 0 && gridLayout_->columnCount() > 0);
+
+ QPoint lastPos(0,0);
+ if (tabPositions_.contains(P2QSTRING(tab->getID()))) {
+ lastPos = tabPositions_[P2QSTRING(tab->getID())];
+ }
+
+ lastPos = QPoint(qMin(lastPos.x(), gridLayout_->columnCount() - 1), qMin(lastPos.y(), gridLayout_->rowCount() - 1));
+
+ QLayoutItem* item = gridLayout_->itemAtPosition(lastPos.y(), lastPos.x());
+ QtTabWidget* tabWidget = dynamic_cast<QtTabWidget*>(item ? item->widget() : nullptr);
+ if (tabWidget) {
+ tabWidget->addTab(tab, title);
+ }
+ tab->setEmphasiseFocus(getDimension().width() > 1 || getDimension().height() > 1);
+ if (future_) {
+ showHideFirstTabs(); // FIXME: Putting it here as a workaround until I work out why it doesn't work initially
+ }
+ return tabWidget ? indexOf(tab) : -1;
+}
+
+int QtDynamicGridLayout::count() const {
+ int count = 0;
+ for (int y = 0; y < gridLayout_->rowCount(); y++) {
+ for (int x = 0; x < gridLayout_->columnCount(); x++) {
+ QLayoutItem* layoutItem = gridLayout_->itemAtPosition(y, x);
+ QtTabWidget* tabWidget = dynamic_cast<QtTabWidget*>(layoutItem->widget());
+ if (tabWidget) {
+ count += tabWidget->count();
+ }
+ }
+ }
+ return count;
+}
+
+QWidget* QtDynamicGridLayout::widget(int index) const {
+ QWidget* widgetAtIndex = nullptr;
+ for (int y = 0; y < gridLayout_->rowCount(); y++) {
+ for (int x = 0; x < gridLayout_->columnCount(); x++) {
+ QLayoutItem* layoutItem = gridLayout_->itemAtPosition(y, x);
+ QtTabWidget* tabWidget = dynamic_cast<QtTabWidget*>(layoutItem ? layoutItem->widget() : nullptr);
+ if (tabWidget) {
+ if (index < tabWidget->count()) {
+ widgetAtIndex = tabWidget->widget(index);
+ return widgetAtIndex;
+ }
+ else {
+ index -= tabWidget->count();
+ }
+ }
+ }
+ }
+ return widgetAtIndex;
+}
+
+int QtDynamicGridLayout::indexOf(const QWidget* widget) const {
+ int index = 0;
+ if (widget) {
+ for (int y = 0; y < gridLayout_->rowCount(); y++) {
+ for (int x = 0; x < gridLayout_->columnCount(); x++) {
+ QLayoutItem* layoutItem = gridLayout_->itemAtPosition(y, x);
+ QtTabWidget* tabWidget = dynamic_cast<QtTabWidget*>(layoutItem->widget());
+ if (tabWidget) {
+ for (int n = 0; n < tabWidget->count(); n++) {
+ QWidget* nthWidget = tabWidget->widget(n);
+ if (nthWidget == widget) {
+ return index;
+ }
+ index++;
+ }
+ }
+ }
+ }
+ }
+ return -1;
+}
+
+void QtDynamicGridLayout::handleApplicationFocusChanged(QWidget*, QWidget* newFocus) {
+ if (movingTab_ || resizing_) {
+ return;
+ }
+
+ if (newFocus) {
+ if (isAncestorOf(newFocus)) {
+ QtTabbable *newTab = dynamic_cast<QtTabbable*>(newFocus->parentWidget());
+ if (newTab) {
+ onCurrentIndexChanged(currentIndex());
+ }
+ }
+ }
+}
+
+int QtDynamicGridLayout::currentIndex() const {
+ return indexOf(currentWidget());
+}
+
+void QtDynamicGridLayout::setCurrentIndex(int index) {
+ int tabIndex = -1;
+ QtTabWidget* tabWidget = indexToTabWidget(index, tabIndex);
+ if (tabIndex >= 0) {
+ tabWidget->setCurrentIndex(tabIndex);
+ if (!tabWidget->hasFocus()) {
+ tabWidget->widget(tabIndex)->setFocus(Qt::TabFocusReason);
+ }
+ } else {
+ assert(false);
+ }
+}
+
+void QtDynamicGridLayout::removeTab(int index) {
+ int tabIndex = -1;
+ QtTabWidget* tabWidget = indexToTabWidget(index, tabIndex);
+ if (tabWidget) {
+ QWidget* tab = tabWidget->widget(tabIndex);
+ tabWidget->removeTab(tabIndex);
+ tab->setParent(nullptr);
+ }
+}
+
+/**
+ * This event filter serves the purpose of filtering out all QEvent::Show events targeted at
+ * all widgets excepts the currently moving widget.
+ * It is required because of the way Qt internally implements the QTabBar::moveTab method.
+ * It does not move the actual tab in the underlying structure, but instead removes it from
+ * a stacked layout and later adds it again.
+ * Both the remove and insert produce a lot signal emission and focus changes. Most of which
+ * the application MUST NOT react on because of the QTabBar and the corresponding QTabWidget
+ * being out of sync in an inconsistent state.
+ */
+bool QtDynamicGridLayout::eventFilter(QObject* object, QEvent* event) {
+ QtTabbable* tab = qobject_cast<QtTabbable*>(object);
+ if (!tab) {
+ return false;
+ }
+ if (tab && (tab != movingTab_)) {
+ if (event->type() == QEvent::Show) {
+ return true;
+ }
+ }
+ return false;
+}
+
+QWidget* QtDynamicGridLayout::currentWidget() const {
+ QWidget* current = nullptr;
+ current = focusWidget();
+ while (current && !dynamic_cast<QtTabbable*>(current)) {
+ if (current->parentWidget()) {
+ current = current->parentWidget();
+ } else {
+ current = nullptr;
+ break;
+ }
+ }
+ if (!current) {
+ current = widget(0);
+ }
+ return current;
+}
+
+void QtDynamicGridLayout::setCurrentWidget(QWidget* widget) {
+ if (widget) {
+ for (int y = 0; y < gridLayout_->rowCount(); y++) {
+ for (int x = 0; x < gridLayout_->columnCount(); x++) {
+ QLayoutItem* layoutItem = gridLayout_->itemAtPosition(y, x);
+ QtTabWidget* tabWidget = dynamic_cast<QtTabWidget*>(layoutItem->widget());
+ if (tabWidget) {
+ for (int n = 0; n < tabWidget->count(); n++) {
+ if (tabWidget->widget(n) == widget) {
+ tabWidget->setCurrentWidget(widget);
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+QtTabWidget* QtDynamicGridLayout::indexToTabWidget(int index, int& tabIndex) {
+ for (int y = 0; y < gridLayout_->rowCount(); y++) {
+ for (int x = 0; x < gridLayout_->columnCount(); x++) {
+ QLayoutItem* layoutItem = gridLayout_->itemAtPosition(y, x);
+ QtTabWidget* tabWidget = dynamic_cast<QtTabWidget*>(layoutItem->widget());
+ if (tabWidget) {
+ if (index < tabWidget->count()) {
+ tabIndex = index;
+ return tabWidget;
+ }
+ else {
+ index -= tabWidget->count();
+ if (index < 0) {
+ qWarning() << "Called QtDynamicGridLayout::setCurrentIndex with index out of bounds: index = " << index;
+ tabIndex = -1;
+ return nullptr;
+ }
+ }
+ }
+ }
+ }
+ tabIndex = -1;
+ return nullptr;
+}
+
+bool QtDynamicGridLayout::isDNDEnabled() const {
+ return dndEnabled_;
+}
+
+QHash<QString, QPoint> QtDynamicGridLayout::getTabPositions() const {
+ return tabPositions_;
+}
+
+void QtDynamicGridLayout::setTabPositions(const QHash<QString, QPoint> positions) {
+ tabPositions_ = positions;
+}
+
+QSize QtDynamicGridLayout::getDimension() const {
+ return QSize(gridLayout_->columnCount(), gridLayout_->rowCount());
+}
+
+void QtDynamicGridLayout::setDimensions(const QSize& dim) {
+ resizing_ = true;
+ assert(dim.width() > 0 && dim.height() > 0);
+ setUpdatesEnabled(false);
+
+ QWidget* restoredWidget = currentWidget();
+
+ QGridLayout* oldLayout = dynamic_cast<QGridLayout*>(layout());
+ QGridLayout* newLayout = new QGridLayout(this);
+ newLayout->setSpacing(4);
+ newLayout->setContentsMargins(0,0,0,0);
+
+ int oldWidth = oldLayout->columnCount();
+ int oldHeight = oldLayout->rowCount();
+ int maxCol = qMax(oldWidth, dim.width());
+ int minCol = qMin(oldWidth, dim.width());
+ int maxRow = qMax(oldHeight, dim.height());
+ int minRow = qMin(oldHeight, dim.height());
+
+ for (int row = 0; row < maxRow; row++) {
+ for (int col = 0; col < maxCol; col++) {
+ QLayoutItem* oldItem = oldLayout->itemAtPosition(row, col);
+ QLayoutItem* newItem = newLayout->itemAtPosition(row, col);
+ bool removeRow = !(row < dim.height());
+ bool removeCol = !(col < dim.width());
+
+ if (removeCol || removeRow) {
+ if (oldItem) {
+ int squeezeRow = removeRow ? (minRow - 1) : row;
+ int squeezeCol = removeCol ? (minCol - 1) : col;
+ newItem = newLayout->itemAtPosition(squeezeRow, squeezeCol);
+ if (!newItem) {
+ newLayout->addWidget(createDNDTabWidget(this), squeezeRow, squeezeCol);
+ newItem = newLayout->itemAtPosition(squeezeRow, squeezeCol);
+ }
+ QtTabWidget* oldTabWidget = dynamic_cast<QtTabWidget*>(oldItem->widget());
+ QtTabWidget* newTabWidget = dynamic_cast<QtTabWidget*>(newItem->widget());
+ assert(oldTabWidget && newTabWidget);
+
+ oldTabWidget->hide();
+ while(oldTabWidget->count()) {
+ QIcon icon = oldTabWidget->tabIcon(0);
+ QString text = oldTabWidget->tabText(0);
+ QWidget* movingTab = oldTabWidget->widget(0);
+ //If handling was allowed, QtChatTabs::handleWidgetShown() would be triggered when newTabWidget has no tabs.
+ //That would access indices of the gridLayout_ that are null because they have been migrated to the newLayout.
+ movingTab->blockSignals(true);
+ newTabWidget->addTab(movingTab, icon, text);
+ movingTab->blockSignals(false);
+ }
+ delete oldTabWidget;
+ }
+ } else {
+ if (oldItem) {
+ newLayout->addWidget(oldItem->widget(), row, col);
+ newItem = newLayout->itemAtPosition(row, col);
+ } else {
+ newLayout->addWidget(createDNDTabWidget(this), row, col);
+ }
+ }
+ }
+ }
+
+ for (int col = 0; col < dim.width(); col++) {
+ newLayout->setColumnStretch(col, 1);
+ }
+ for (int row = 0; row < dim.height(); row++) {
+ newLayout->setRowStretch(row, 1);
+ }
+
+ setUpdatesEnabled(true);
+ delete layout();
+ setLayout(newLayout);
+ gridLayout_ = newLayout;
+
+ resizing_ = false;
+ setCurrentWidget(restoredWidget);
+
+ updateEmphasiseFocusOnTabs();
+
+ if (future_) {
+ showHideFirstTabs();
+ }
+}
+
+void QtDynamicGridLayout::showHideFirstTabs() {
+ int tmp;
+ auto firstTabs = indexToTabWidget(0, tmp);
+
+ if (firstTabs) {
+ if (gridLayout_->columnCount() == 1 && gridLayout_->rowCount() == 1) {
+ firstTabs->tabBar()->hide();
+ }
+ else {
+ firstTabs->tabBar()->show();
+ }
+ }
+}
+
+void QtDynamicGridLayout::updateEmphasiseFocusOnTabs() {
+ const auto currentDimensions = getDimension();
+
+ for (int y = 0; y < gridLayout_->rowCount(); y++) {
+ for (int x = 0; x < gridLayout_->columnCount(); x++) {
+ QLayoutItem* layoutItem = gridLayout_->itemAtPosition(y, x);
+ QtTabWidget* tabWidget = dynamic_cast<QtTabWidget*>(layoutItem->widget());
+ assert(tabWidget);
+ for (int index = 0; index < tabWidget->count(); index++) {
+ QtTabbable* tab = dynamic_cast<QtTabbable*>(tabWidget->widget(index));
+ assert(tab);
+ tab->setEmphasiseFocus(currentDimensions.height() > 1 || currentDimensions.width() > 1);
+ }
+ }
+ }
+}
+
+void QtDynamicGridLayout::moveCurrentTabRight() {
+ int index = currentIndex();
+ if (index >= 0) {
+ int tabIndex = -1;
+ QtTabWidget* tabWidget = indexToTabWidget(index, tabIndex);
+ assert(tabWidget);
+ int newTabIndex = (tabIndex + 1) % tabWidget->count();
+ moveTab(tabWidget, tabIndex, newTabIndex);
+ }
+}
+
+void QtDynamicGridLayout::moveCurrentTabLeft() {
+ int index = currentIndex();
+ if (index >= 0) {
+ int tabIndex = -1;
+ QtTabWidget* tabWidget = indexToTabWidget(index, tabIndex);
+ assert(tabWidget);
+ int newTabIndex = (tabWidget->count() + tabIndex - 1) % tabWidget->count();
+ moveTab(tabWidget, tabIndex, newTabIndex);
+ }
+}
+
+void QtDynamicGridLayout::moveCurrentTabToNextGroup() {
+ int index = currentIndex();
+ if (index >= 0) {
+ int tabIndex = -1;
+ QtTabWidget* tabWidget = indexToTabWidget(index, tabIndex);
+
+ int row = -1;
+ int col = -1;
+ int tmp;
+ gridLayout_->getItemPosition(gridLayout_->indexOf(tabWidget), &row, &col, &tmp, &tmp);
+
+ // calculate next cell
+ col++;
+ if (!(col < gridLayout_->columnCount())) {
+ col = 0;
+ row++;
+ if (!(row < gridLayout_->rowCount())) {
+ row = 0;
+ }
+ }
+
+ QtTabWidget* targetTabWidget = dynamic_cast<QtTabWidget*>(gridLayout_->itemAtPosition(row, col)->widget());
+ assert(tabWidget);
+ assert(targetTabWidget);
+
+ // fetch tab information
+ QWidget* tab = tabWidget->widget(tabIndex);
+ QString tabText = tabWidget->tabText(tabIndex);
+
+ // move tab
+ tab->blockSignals(true);
+ targetTabWidget->addTab(tab, tabText);
+ tab->blockSignals(false);
+ tab->setFocus(Qt::TabFocusReason);
+
+ updateTabPositions();
+ }
+}
+
+void QtDynamicGridLayout::moveCurrentTabToPreviousGroup() {
+ int index = currentIndex();
+ if (index >= 0) {
+ int tabIndex = -1;
+ QtTabWidget* tabWidget = indexToTabWidget(index, tabIndex);
+
+ int row = -1;
+ int col = -1;
+ int tmp;
+ gridLayout_->getItemPosition(gridLayout_->indexOf(tabWidget), &row, &col, &tmp, &tmp);
+
+ // calculate next cell
+ col--;
+ if (col < 0) {
+ col = gridLayout_->columnCount() - 1;
+ row--;
+ if (row < 0) {
+ row = gridLayout_->rowCount() - 1;
+ }
+ }
+
+ QtTabWidget* targetTabWidget = dynamic_cast<QtTabWidget*>(gridLayout_->itemAtPosition(row, col)->widget());
+ assert(tabWidget);
+ assert(targetTabWidget);
+
+ // fetch tab information
+ QWidget* tab = tabWidget->widget(tabIndex);
+ QString tabText = tabWidget->tabText(tabIndex);
+
+ // move tab
+ tab->blockSignals(true);
+ targetTabWidget->addTab(tab, tabText);
+ tab->blockSignals(false);
+ tab->setFocus(Qt::TabFocusReason);
+
+ updateTabPositions();
+ }
+}
+
+void QtDynamicGridLayout::handleTabCloseRequested(int index) {
+ updateTabPositions();
+ QtTabWidget* tabWidgetSender = dynamic_cast<QtTabWidget*>(sender());
+ for (int y = 0; y < gridLayout_->rowCount(); y++) {
+ for (int x = 0; x < gridLayout_->columnCount(); x++) {
+ QLayoutItem* layoutItem = gridLayout_->itemAtPosition(y, x);
+ QtTabWidget* tabWidget = dynamic_cast<QtTabWidget*>(layoutItem->widget());
+ if (tabWidget == tabWidgetSender) {
+ tabCloseRequested(index);
+ }
+ else {
+ index += tabWidget->count();
+ }
+ }
+ }
+}
+
+void QtDynamicGridLayout::handleTabCurrentChanged(int index) {
+ if (movingTab_) {
+ return;
+ }
+
+ if (index >= 0) {
+ QTabWidget* sendingTabWidget = dynamic_cast<QTabWidget*>(sender());
+ assert(sendingTabWidget);
+ sendingTabWidget->widget(index)->setFocus();
+ }
+}
+
+void QtDynamicGridLayout::updateTabPositions() {
+ for (int y = 0; y < gridLayout_->rowCount(); y++) {
+ for (int x = 0; x < gridLayout_->columnCount(); x++) {
+ QLayoutItem* layoutItem = gridLayout_->itemAtPosition(y, x);
+ QtTabWidget* tabWidget = dynamic_cast<QtTabWidget*>(layoutItem->widget());
+ assert(tabWidget);
+ for (int index = 0; index < tabWidget->count(); index++) {
+ QtTabbable* tab = dynamic_cast<QtTabbable*>(tabWidget->widget(index));
+ assert(tab);
+ tabPositions_.insert(P2QSTRING(tab->getID()), QPoint(x, y));
+ }
+ }
+ }
+}
+
+void QtDynamicGridLayout::moveTab(QtTabWidget* tabWidget, int oldIndex, int newIndex) {
+#if QT_VERSION >= 0x040500
+ SWIFT_LOG_ASSERT(movingTab_ == nullptr, error);
+ movingTab_ = qobject_cast<QtTabbable*>(tabWidget->widget(oldIndex));
+ SWIFT_LOG_ASSERT(movingTab_ != nullptr, error);
+
+ if (movingTab_) {
+ // Install event filter that filters out events issued during the internal movement of the
+ // tab but not targeted at the moving tab.
+ qApp->installEventFilter(this);
+
+ tabWidget->tabBar()->moveTab(oldIndex, newIndex);
+
+ qApp->removeEventFilter(this);
+ SWIFT_LOG_ASSERT(movingTab_ == tabWidget->widget(newIndex), error);
+ }
+ movingTab_ = nullptr;
+ tabWidget->widget(newIndex)->setFocus();
+#else
+#warning Qt 4.5 or later is needed. Trying anyway, some things will be disabled.
+#endif
+}
+
+QtTabWidget* QtDynamicGridLayout::createDNDTabWidget(QWidget* parent) {
+ QtTabWidget* tab = new QtTabWidget(parent);
+ if (dndEnabled_) {
+ QtDNDTabBar* tabBar = new QtDNDTabBar(tab);
+ connect(tabBar, SIGNAL(onDropSucceeded()), this, SLOT(updateTabPositions()));
+ tab->setTabBar(tabBar);
+ }
+ tab->setUsesScrollButtons(true);
+ tab->setElideMode(Qt::ElideRight);
+#if QT_VERSION >= 0x040500
+ /*For Macs, change the tab rendering.*/
+ tab->setDocumentMode(true);
+ /*Closable tabs are only in Qt4.5 and later*/
+ tab->setTabsClosable(true);
+ tab->setMovable(true);
+ connect(tab, SIGNAL(tabCloseRequested(int)), this, SLOT(handleTabCloseRequested(int)));
+ connect(tab, SIGNAL(currentChanged(int)), this, SLOT(handleTabCurrentChanged(int)));
+#else
+#warning Qt 4.5 or later is needed. Trying anyway, some things will be disabled.
+#endif
+ return tab;
+}
+
+}
diff --git a/Swift/QtUI/Trellis/QtDynamicGridLayout.h b/Swift/QtUI/Trellis/QtDynamicGridLayout.h
new file mode 100644
index 0000000..f3a2e96
--- /dev/null
+++ b/Swift/QtUI/Trellis/QtDynamicGridLayout.h
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2014-2016 Isode Limited.
+ * All rights reserved.
+ * See the COPYING file for more information.
+ */
+
+#pragma once
+
+#include <QGridLayout>
+#include <QHash>
+#include <QPoint>
+#include <QSize>
+#include <QString>
+#include <QWidget>
+
+namespace Swift {
+ class QtTabbable;
+ class QtTabWidget;
+
+ class QtDynamicGridLayout : public QWidget {
+ Q_OBJECT
+ public:
+ explicit QtDynamicGridLayout(bool future, QWidget* parent = nullptr, bool enableDND = false);
+ virtual ~QtDynamicGridLayout();
+
+ QSize getDimension() const;
+
+ // emulate QtTabWidget API
+ int addTab(QtTabbable* tab, const QString& title);
+ void removeTab(int index);
+ int count() const;
+
+ QWidget* widget(int index) const;
+ QWidget* currentWidget() const;
+ void setCurrentWidget(QWidget* widget);
+
+ QtTabWidget* indexToTabWidget(int index, int& tabIndex);
+
+ int indexOf(const QWidget* widget) const;
+ int currentIndex() const;
+ void setCurrentIndex(int index);
+
+ bool isDNDEnabled() const;
+
+ QHash<QString, QPoint> getTabPositions() const;
+ void setTabPositions(const QHash<QString, QPoint> positions);
+
+ bool eventFilter(QObject* object, QEvent* event);
+
+ signals:
+ void tabCloseRequested(int index);
+ void onCurrentIndexChanged(int newIndex);
+
+ public slots:
+ void setDimensions(const QSize& dim);
+
+ // Tab Management
+ void moveCurrentTabRight();
+ void moveCurrentTabLeft();
+ void moveCurrentTabToNextGroup();
+ void moveCurrentTabToPreviousGroup();
+
+ void updateTabPositions();
+
+ private slots:
+ void handleTabCloseRequested(int index);
+ void handleTabCurrentChanged(int index);
+ void handleApplicationFocusChanged(QWidget* oldFocus, QWidget* newFocus);
+
+ private:
+ void moveTab(QtTabWidget* tabWidget, int oldIndex, int newIndex);
+ QtTabWidget* createDNDTabWidget(QWidget* parent);
+ void updateEmphasiseFocusOnTabs();
+ void showHideFirstTabs();
+
+ private:
+ QGridLayout *gridLayout_;
+ bool dndEnabled_;
+ QHash<QString, QPoint> tabPositions_;
+ QtTabbable* movingTab_;
+ bool resizing_ = false;
+ bool future_ = false;
+ };
+}
diff --git a/Swift/QtUI/Trellis/QtGridSelectionDialog.cpp b/Swift/QtUI/Trellis/QtGridSelectionDialog.cpp
new file mode 100644
index 0000000..e922e07
--- /dev/null
+++ b/Swift/QtUI/Trellis/QtGridSelectionDialog.cpp
@@ -0,0 +1,194 @@
+/*
+ * Copyright (c) 2014-2018 Isode Limited.
+ * All rights reserved.
+ * See the COPYING file for more information.
+ */
+
+#include <Swift/QtUI/Trellis/QtGridSelectionDialog.h>
+
+#include <QApplication>
+#include <QCursor>
+#include <QPaintEvent>
+#include <QPainter>
+#include <QStyle>
+#include <QStyleOption>
+
+namespace Swift {
+
+QtGridSelectionDialog::QtGridSelectionDialog(QWidget* parent) : QWidget(parent), descriptionText(tr("Select the number of rows and columns for your layout. You can change the size by moving the mouse or cursor keys.")) {
+ frameSize = QSize(28,28) * 2;
+ maxGridSize = QSize(7,7);
+ minGridSize = QSize(1,1);
+ currentGridSize = QSize(1,1);
+ padding = 4;
+ setWindowFlags(Qt::FramelessWindowHint);
+ setCursor(Qt::SizeAllCursor);
+ horizontalMargin = style()->pixelMetric(QStyle::PM_LayoutLeftMargin);
+ verticalMargin = style()->pixelMetric(QStyle::PM_LayoutBottomMargin);
+}
+
+QSize QtGridSelectionDialog::sizeHint() const {
+ // PM_MenuVMargin | frameSize | ( padding | frameSize ) * | PM_MenuVMargin
+ int width = horizontalMargin + frameSize.width() + (padding + frameSize.width()) * (currentGridSize.width() - 1) + horizontalMargin;
+ int height = verticalMargin + frameSize.height() + (padding + frameSize.height()) * (currentGridSize.height() - 1) + verticalMargin;
+
+ height += getDescriptionTextHeight(width);
+
+ return QSize(width, height);
+}
+
+void QtGridSelectionDialog::setCurrentGridSize(const QSize& size) {
+ currentGridSize = size;
+ emit currentGridSizeChanged(size);
+}
+
+QSize QtGridSelectionDialog::getCurrentGridSize() const {
+ return currentGridSize;
+}
+
+void QtGridSelectionDialog::setMinGridSize(const QSize& size) {
+ minGridSize = size;
+ emit minGridSizeChanged(size);
+}
+
+QSize QtGridSelectionDialog::getMinGridSize() const {
+ return minGridSize;
+}
+
+void QtGridSelectionDialog::setMaxGridSize(const QSize& size) {
+ maxGridSize = size;
+ emit maxGridSizeChanged(size);
+}
+
+QSize QtGridSelectionDialog::getMaxGridSize() const {
+ return maxGridSize;
+}
+
+QSize QtGridSelectionDialog::getFrameSize() const {
+ return frameSize;
+}
+
+int QtGridSelectionDialog::getDescriptionTextHeight() const {
+ auto width = horizontalMargin + frameSize.width() + (padding + frameSize.width()) * (currentGridSize.width() - 1) + horizontalMargin;
+ return getDescriptionTextHeight(width);
+}
+
+int QtGridSelectionDialog::getDescriptionTextHeight(int width) const {
+ // Height of descriptive centered text below trellis
+ auto fontMetrics = QFontMetrics(QApplication::font());
+ auto descriptionBB = fontMetrics.boundingRect(QRect(0, 0, width - 2 * horizontalMargin, 1000), Qt::TextWordWrap, descriptionText, 0, nullptr);
+
+ return (descriptionBB.height() + descriptionBB.y());
+}
+
+void QtGridSelectionDialog::keyReleaseEvent(QKeyEvent* event) {
+ if (event) {
+ QSize newGridSize = currentGridSize;
+ if (event->key() == Qt::Key_Up) {
+ newGridSize += QSize(0, -1);
+ }
+ else if (event->key() == Qt::Key_Down) {
+ newGridSize += QSize(0, 1);
+ }
+ else if (event->key() == Qt::Key_Left) {
+ newGridSize += QSize(-1, 0);
+ }
+ else if (event->key() == Qt::Key_Right) {
+ newGridSize += QSize(1, 0);
+ }
+ else if (event->key() == Qt::Key_Return) {
+ hide();
+ setCurrentGridSize(currentGridSize);
+ }
+ else if (event->key() == Qt::Key_Escape) {
+ hide();
+ }
+
+ if (minGridSize.expandedTo(newGridSize).boundedTo(maxGridSize) != currentGridSize) {
+ currentGridSize = minGridSize.expandedTo(newGridSize).boundedTo(maxGridSize);
+
+ QSize newSizeHint = sizeHint();
+ resize(newSizeHint);
+ QCursor::setPos(mapToGlobal(QPoint(newSizeHint.width(), newSizeHint.height()- getDescriptionTextHeight()) - QPoint(frameSize.width() / 2, frameSize.height() / 2)));
+ }
+ }
+}
+
+void QtGridSelectionDialog::mousePressEvent(QMouseEvent*) {
+ hide();
+ setCurrentGridSize(currentGridSize);
+}
+
+void QtGridSelectionDialog::paintEvent(QPaintEvent*) {
+ QPainter painter(this);
+ // draw grid
+ QRect gridCell = QRect(QPoint(0,0), frameSize);
+ painter.setBrush(palette().highlight());
+ painter.setPen(Qt::NoPen);
+ for (int x = 0; x < currentGridSize.width(); x++) {
+ for (int y = 0; y < currentGridSize.height(); y++) {
+ int xPos = horizontalMargin + (x * (frameSize.width() + padding));
+ int yPos = verticalMargin + (y * (frameSize.height() + padding));
+ gridCell.moveTo(QPoint(xPos, yPos));
+ painter.drawRect(gridCell);
+ }
+ }
+
+ // draw description text
+ auto fontMetrics = QFontMetrics(QApplication::font());
+ auto descriptionBB = fontMetrics.boundingRect(QRect(0,0, width() - 2 * horizontalMargin,0), Qt::AlignHCenter | Qt::AlignTop | Qt::TextWordWrap, descriptionText, 0, nullptr);
+
+ QStyleOption opt;
+ opt.initFrom(this);
+ int textY = verticalMargin + (currentGridSize.height() * (frameSize.height() + padding));
+ int textX = (size().width() - descriptionBB.width()) / 2;
+ style()->drawItemText(&painter, QRect(textX, textY, descriptionBB.width(), descriptionBB.height()), Qt::AlignHCenter | Qt::AlignTop | Qt::TextWordWrap, opt.palette, true, descriptionText, foregroundRole());
+}
+
+void QtGridSelectionDialog::showEvent(QShowEvent*) {
+ timerId = startTimer(1000 / 25);
+}
+
+void QtGridSelectionDialog::hideEvent(QHideEvent*) {
+ killTimer(timerId);
+}
+
+void QtGridSelectionDialog::timerEvent(QTimerEvent*) {
+
+ const QPoint diff = QCursor::pos() - frameGeometry().topLeft() - QPoint(horizontalMargin, verticalMargin);
+ const auto toleranceFactor = 4; // Ratio of how far (1/tolerance) the mouse should move for the next frame to be plotted
+ // dx, dy - mouse position with respect to first top-left square
+ const auto dx = diff.x();
+ const auto dy = diff.y();
+ // width, height - dimension of each square with padding
+ const auto width = frameSize.width() + padding;
+ const auto height = frameSize.height() + padding;
+ // xThreshold, yThreshold - how far the mouse should be moved so that a new square is added or the existing one is hidden
+ const auto xThreshold = width / toleranceFactor;
+ const auto yThreshold = height / toleranceFactor;
+
+ const auto getSize = [](int length, int threshold, int delta) {
+ if (delta < length + threshold) {
+ return 1;
+ }
+ else {
+ return (delta + (length - threshold)) / length;
+ }
+ };
+ const QSize newGridSize(getSize(width, xThreshold, dx), getSize(height, yThreshold, dy));
+
+ if (minGridSize.expandedTo(newGridSize).boundedTo(maxGridSize) != currentGridSize) {
+ currentGridSize = minGridSize.expandedTo(newGridSize).boundedTo(maxGridSize);
+ resize(sizeHint());
+ }
+}
+bool QtGridSelectionDialog::event(QEvent* event) {
+ // Hide the window when it becomes a non-top-level window.
+ if (event->type() == QEvent::WindowDeactivate) {
+ hide();
+ return true;
+ }
+ return QWidget::event(event);
+}
+
+}
diff --git a/Swift/QtUI/Trellis/QtGridSelectionDialog.h b/Swift/QtUI/Trellis/QtGridSelectionDialog.h
new file mode 100644
index 0000000..c448979
--- /dev/null
+++ b/Swift/QtUI/Trellis/QtGridSelectionDialog.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2014-2017 Isode Limited.
+ * All rights reserved.
+ * See the COPYING file for more information.
+ */
+
+#pragma once
+
+#include <QSize>
+#include <QWidget>
+
+namespace Swift {
+
+ class QtGridSelectionDialog : public QWidget {
+ Q_OBJECT
+
+ Q_PROPERTY(QSize currentGridSize READ getCurrentGridSize WRITE setCurrentGridSize NOTIFY currentGridSizeChanged)
+ Q_PROPERTY(QSize minGridSize READ getMinGridSize WRITE setMinGridSize NOTIFY minGridSizeChanged)
+ Q_PROPERTY(QSize maxGridSize READ getMaxGridSize WRITE setMaxGridSize NOTIFY maxGridSizeChanged)
+ public:
+ explicit QtGridSelectionDialog(QWidget* parent = nullptr);
+
+ virtual QSize sizeHint() const;
+
+ void setCurrentGridSize(const QSize& size);
+ QSize getCurrentGridSize() const;
+ void setMinGridSize(const QSize& size);
+ QSize getMinGridSize() const;
+ void setMaxGridSize(const QSize& size);
+ QSize getMaxGridSize() const;
+
+ QSize getFrameSize() const;
+ int getDescriptionTextHeight() const;
+
+ signals:
+ void currentGridSizeChanged(QSize);
+ void minGridSizeChanged(QSize);
+ void maxGridSizeChanged(QSize);
+
+ protected:
+ void keyReleaseEvent(QKeyEvent* event);
+ void mousePressEvent(QMouseEvent* event);
+ void paintEvent(QPaintEvent* event);
+ void showEvent(QShowEvent* event);
+ void hideEvent(QHideEvent* event);
+ bool event(QEvent* event);
+ void timerEvent(QTimerEvent* event);
+
+ private:
+ int getDescriptionTextHeight(int width) const;
+
+ private:
+ int padding;
+ int horizontalMargin;
+ int verticalMargin;
+ int timerId = -1;
+
+ QSize frameSize;
+
+ QSize currentGridSize;
+ QSize minGridSize;
+ QSize maxGridSize;
+
+ const QString descriptionText;
+ };
+}