diff options
Diffstat (limited to 'Swift/QtUI/Trellis')
-rw-r--r-- | Swift/QtUI/Trellis/QtDNDTabBar.cpp | 164 | ||||
-rw-r--r-- | Swift/QtUI/Trellis/QtDNDTabBar.h | 44 | ||||
-rw-r--r-- | Swift/QtUI/Trellis/QtDynamicGridLayout.cpp | 561 | ||||
-rw-r--r-- | Swift/QtUI/Trellis/QtDynamicGridLayout.h | 84 | ||||
-rw-r--r-- | Swift/QtUI/Trellis/QtGridSelectionDialog.cpp | 194 | ||||
-rw-r--r-- | Swift/QtUI/Trellis/QtGridSelectionDialog.h | 66 |
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; + }; +} |