From b4a54583c4d575fe152122c21da616c3c942bbfd Mon Sep 17 00:00:00 2001 From: Tobias Markmann <tm@ayena.de> Date: Tue, 25 Nov 2014 15:07:29 +0100 Subject: Add support for new trellis layout for chat windows. This includes dynamic customizable grid layouting of chat tabs including: - arrengement of tabs via menu, keyboard shortcuts or drag'n'drop - change of grid size via mouse or keyboard - save/restore of grid size and positions of chats inside the grid Test-Information: Tested on OS X 10.9.8. Change-Id: I43f94293a1c672971640c3000abfc6530f2ea7a8 diff --git a/Swift/Controllers/SettingConstants.cpp b/Swift/Controllers/SettingConstants.cpp index a5328a4..a6756f5 100644 --- a/Swift/Controllers/SettingConstants.cpp +++ b/Swift/Controllers/SettingConstants.cpp @@ -25,4 +25,6 @@ const SettingsProvider::Setting<std::string> SettingConstants::DICT_PATH("dictPa const SettingsProvider::Setting<std::string> SettingConstants::PERSONAL_DICT_PATH("personaldictPath", "/home/"); const SettingsProvider::Setting<std::string> SettingConstants::DICT_FILE("dictFile", "en_US.dic"); const SettingsProvider::Setting<std::string> SettingConstants::INVITE_AUTO_ACCEPT_MODE("inviteAutoAcceptMode", "presence"); +const SettingsProvider::Setting<std::string> SettingConstants::TRELLIS_GRID_SIZE("trellisGridSize", ""); +const SettingsProvider::Setting<std::string> SettingConstants::TRELLIS_GRID_POSITIONS("trellisGridPositions", ""); } diff --git a/Swift/Controllers/SettingConstants.h b/Swift/Controllers/SettingConstants.h index 4d25bde..90e1db6 100644 --- a/Swift/Controllers/SettingConstants.h +++ b/Swift/Controllers/SettingConstants.h @@ -28,5 +28,7 @@ namespace Swift { static const SettingsProvider::Setting<std::string> PERSONAL_DICT_PATH; static const SettingsProvider::Setting<std::string> DICT_FILE; static const SettingsProvider::Setting<std::string> INVITE_AUTO_ACCEPT_MODE; + static const SettingsProvider::Setting<std::string> TRELLIS_GRID_SIZE; + static const SettingsProvider::Setting<std::string> TRELLIS_GRID_POSITIONS; }; } diff --git a/Swift/QtUI/CocoaApplicationActivateHelper.h b/Swift/QtUI/CocoaApplicationActivateHelper.h index 8bc2af5..8ea86e0 100644 --- a/Swift/QtUI/CocoaApplicationActivateHelper.h +++ b/Swift/QtUI/CocoaApplicationActivateHelper.h @@ -16,7 +16,7 @@ namespace Swift { class CocoaApplicationActivateHelper : public QObject { public: CocoaApplicationActivateHelper(); - ~CocoaApplicationActivateHelper(); + virtual ~CocoaApplicationActivateHelper(); private: bool eventFilter(QObject* o, QEvent* e); diff --git a/Swift/QtUI/QtChatTabs.cpp b/Swift/QtUI/QtChatTabs.cpp index cf335e1..d04977e 100644 --- a/Swift/QtUI/QtChatTabs.cpp +++ b/Swift/QtUI/QtChatTabs.cpp @@ -1,72 +1,141 @@ /* - * Copyright (c) 2010 Kevin Smith + * Copyright (c) 2010-2014 Kevin Smith * Licensed under the GNU General Public License v3. * See Documentation/Licenses/GPLv3.txt for more information. */ -#include "QtChatTabs.h" +#include <Swift/QtUI/QtChatTabs.h> #include <algorithm> #include <vector> +#include <Swiften/Base/Log.h> + #include <Swift/Controllers/ChatMessageSummarizer.h> +#include <Swift/Controllers/SettingConstants.h> +#include <Swift/Controllers/Settings/SettingsProvider.h> #include <Swift/QtUI/QtSwiftUtil.h> +#include <Swift/QtUI/QtTabWidget.h> +#include <Swift/QtUI/QtTabbable.h> +#include <Swift/QtUI/Trellis/QtDynamicGridLayout.h> +#include <Swift/QtUI/Trellis/QtGridSelectionDialog.h> +#include <QAction> +#include <QApplication> #include <QCloseEvent> +#include <QCursor> #include <QDesktopWidget> -#include <QtGlobal> -#include <QTabWidget> #include <QLayout> +#include <QMenu> #include <QTabBar> -#include <QApplication> -#include <qdebug.h> +#include <QTabWidget> +#include <QtGlobal> namespace Swift { -QtChatTabs::QtChatTabs(bool singleWindow) : QWidget(), singleWindow_(singleWindow) { +QtChatTabs::QtChatTabs(bool singleWindow, SettingsProvider* settingsProvider, bool trellisMode) : QWidget(), singleWindow_(singleWindow), settingsProvider_(settingsProvider), trellisMode_(trellisMode), dynamicGrid_(NULL), gridSelectionDialog_(NULL) { #ifndef Q_OS_MAC setWindowIcon(QIcon(":/logo-chat-16.png")); #else setAttribute(Qt::WA_ShowWithoutActivating); #endif + dynamicGrid_ = new QtDynamicGridLayout(this, trellisMode); + connect(dynamicGrid_, SIGNAL(tabCloseRequested(int)), this, SLOT(handleTabCloseRequested(int))); - tabs_ = new QtTabWidget(this); - tabs_->setUsesScrollButtons(true); - tabs_->setElideMode(Qt::ElideRight); -#if QT_VERSION >= 0x040500 - /*For Macs, change the tab rendering.*/ - tabs_->setDocumentMode(true); - /*Closable tabs are only in Qt4.5 and later*/ - tabs_->setTabsClosable(true); - tabs_->setMovable(true); - connect(tabs_, SIGNAL(tabCloseRequested(int)), this, SLOT(handleTabCloseRequested(int))); -#else -#warning Qt 4.5 or later is needed. Trying anyway, some things will be disabled. -#endif QVBoxLayout *layout = new QVBoxLayout; layout->setSpacing(0); - layout->setContentsMargins(0, 3, 0, 0); - layout->addWidget(tabs_); + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(dynamicGrid_); setLayout(layout); + + if (trellisMode) { + // restore size + std::string gridSizeString = settingsProvider->getSetting(SettingConstants::TRELLIS_GRID_SIZE); + if (!gridSizeString.empty()) { + QByteArray gridSizeData = QByteArray::fromBase64(P2QSTRING(gridSizeString).toUtf8()); + QDataStream dataStreamGridSize(&gridSizeData, QIODevice::ReadWrite); + QSize gridSize(1,1); + dataStreamGridSize >> gridSize; + dynamicGrid_->setDimensions(gridSize); + } + + // restore positions + std::string tabPositionsString = settingsProvider->getSetting(SettingConstants::TRELLIS_GRID_POSITIONS); + if (!tabPositionsString.empty()) { + QByteArray tabPositionsData = QByteArray::fromBase64(P2QSTRING(tabPositionsString).toUtf8()); + QDataStream inTabPositions(&tabPositionsData, QIODevice::ReadWrite); + QHash<QString, QPoint> tabPositions; + inTabPositions >> tabPositions; + dynamicGrid_->setTabPositions(tabPositions); + } + } + + gridSelectionDialog_ = new QtGridSelectionDialog(); +} + +QtChatTabs::~QtChatTabs() { + if (trellisMode_) { + storeTabPositions(); + } + delete gridSelectionDialog_; } void QtChatTabs::closeEvent(QCloseEvent* event) { //Hide first to prevent flickering as each tab is removed. hide(); - for (int i = tabs_->count() - 1; i >= 0; i--) { - tabs_->widget(i)->close(); + if (trellisMode_) { + storeTabPositions(); + } + + for (int i = dynamicGrid_->count() - 1; i >= 0; i--) { + dynamicGrid_->widget(i)->close(); } event->accept(); } QtTabbable* QtChatTabs::getCurrentTab() { - return qobject_cast<QtTabbable*>(tabs_->currentWidget()); + return qobject_cast<QtTabbable*>(dynamicGrid_->currentWidget()); +} + +void QtChatTabs::setViewMenu(QMenu* viewMenu) { + if (trellisMode_) { + viewMenu->addSeparator(); + QAction* action = new QAction(tr("Change &layout"), this); + action->setShortcutContext(Qt::ApplicationShortcut); + action->setShortcut(QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_L)); + connect(action, SIGNAL(triggered()), this, SLOT(handleOpenLayoutChangeDialog())); + viewMenu->addAction(action); + + action = new QAction(tr("Move Tab right"), this); + action->setShortcutContext(Qt::ApplicationShortcut); + action->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_Right)); + connect(action, SIGNAL(triggered()), dynamicGrid_, SLOT(moveCurrentTabRight())); + viewMenu->addAction(action); + + action = new QAction(tr("Move Tab left"), this); + action->setShortcutContext(Qt::ApplicationShortcut); + action->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_Left)); + connect(action, SIGNAL(triggered()), dynamicGrid_, SLOT(moveCurrentTabLeft())); + viewMenu->addAction(action); + + action = new QAction(tr("Move Tab to next group"), this); + action->setShortcutContext(Qt::ApplicationShortcut); + action->setShortcut(QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_Right)); + connect(action, SIGNAL(triggered()), dynamicGrid_, SLOT(moveCurrentTabToNextGroup())); + viewMenu->addAction(action); + + action = new QAction(tr("Move Tab to previous group"), this); + action->setShortcutContext(Qt::ApplicationShortcut); + action->setShortcut(QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_Left)); + connect(action, SIGNAL(triggered()), dynamicGrid_, SLOT(moveCurrentTabToPreviousGroup())); + viewMenu->addAction(action); + } } void QtChatTabs::addTab(QtTabbable* tab) { QSizePolicy policy = sizePolicy(); /* Chat windows like to grow - don't let them */ setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - tabs_->addTab(tab, tab->windowTitle()); + dynamicGrid_->addTab(tab, tab->windowTitle()); connect(tab, SIGNAL(titleUpdated()), this, SLOT(handleTabTitleUpdated()), Qt::UniqueConnection); connect(tab, SIGNAL(countUpdated()), this, SLOT(handleTabTitleUpdated()), Qt::UniqueConnection); connect(tab, SIGNAL(windowClosing()), this, SLOT(handleTabClosing()), Qt::UniqueConnection); @@ -85,7 +154,7 @@ void QtChatTabs::handleWidgetShown() { return; } checkForFirstShow(); - if (tabs_->indexOf(widget) >= 0) { + if (dynamicGrid_->indexOf(widget) >= 0) { handleTabTitleUpdated(widget); return; } @@ -101,7 +170,7 @@ void QtChatTabs::handleWantsToActivate() { setWindowState(windowState() | Qt::WindowActive); show(); widget->show(); - tabs_->setCurrentWidget(widget); + dynamicGrid_->setCurrentWidget(widget); handleTabTitleUpdated(widget); widget->setFocus(); raise(); @@ -111,9 +180,9 @@ void QtChatTabs::handleWantsToActivate() { void QtChatTabs::handleTabClosing() { QWidget* widget = qobject_cast<QWidget*>(sender()); int index; - if (widget && ((index = tabs_->indexOf(widget)) >= 0)) { - tabs_->removeTab(index); - if (tabs_->count() == 0) { + if (widget && ((index = dynamicGrid_->indexOf(widget)) >= 0)) { + dynamicGrid_->removeTab(index); + if (dynamicGrid_->count() == 0) { if (!singleWindow_) { hide(); } @@ -123,19 +192,19 @@ void QtChatTabs::handleTabClosing() { } } else { - handleTabTitleUpdated(tabs_->currentWidget()); + handleTabTitleUpdated(dynamicGrid_->currentWidget()); } } } void QtChatTabs::handleRequestedPreviousTab() { - int newIndex = tabs_->currentIndex() - 1; - tabs_->setCurrentIndex(newIndex >= 0 ? newIndex : tabs_->count() - 1); + int newIndex = dynamicGrid_->currentIndex() - 1; + dynamicGrid_->setCurrentIndex(newIndex >= 0 ? newIndex : dynamicGrid_->count() - 1); } void QtChatTabs::handleRequestedNextTab() { - int newIndex = tabs_->currentIndex() + 1; - tabs_->setCurrentIndex(newIndex < tabs_->count() ? newIndex : 0); + int newIndex = dynamicGrid_->currentIndex() + 1; + dynamicGrid_->setCurrentIndex(newIndex < dynamicGrid_->count() ? newIndex : 0); } void QtChatTabs::handleRequestedActiveTab() { @@ -143,16 +212,16 @@ void QtChatTabs::handleRequestedActiveTab() { bool finished = false; for (int j = 0; j < 2; j++) { bool looped = false; - for (int i = tabs_->currentIndex() + 1; !finished && i != tabs_->currentIndex(); i++) { - if (i >= tabs_->count()) { + for (int i = dynamicGrid_->currentIndex() + 1; !finished && i != dynamicGrid_->currentIndex(); i++) { + if (i >= dynamicGrid_->count()) { if (looped) { break; } looped = true; i = 0; } - if (qobject_cast<QtTabbable*>(tabs_->widget(i))->getWidgetAlertState() == types[j]) { - tabs_->setCurrentIndex(i); + if (qobject_cast<QtTabbable*>(dynamicGrid_->widget(i))->getWidgetAlertState() == types[j]) { + dynamicGrid_->setCurrentIndex(i); finished = true; break; } @@ -162,7 +231,13 @@ void QtChatTabs::handleRequestedActiveTab() { void QtChatTabs::handleTabCloseRequested(int index) { - QWidget* widget = tabs_->widget(index); + if (trellisMode_) { + storeTabPositions(); + } + + assert(index < dynamicGrid_->count()); + QWidget* widget = dynamicGrid_->widget(index); + assert(widget); widget->close(); } @@ -176,7 +251,7 @@ void QtChatTabs::handleTabTitleUpdated(QWidget* widget) { return; } QtTabbable* tabbable = qobject_cast<QtTabbable*>(widget); - int index = tabs_->indexOf(widget); + int index = dynamicGrid_->indexOf(widget); if (index < 0) { return; } @@ -202,11 +277,14 @@ void QtChatTabs::handleTabTitleUpdated(QWidget* widget) { accelsTaken[i] = (i == 0); //A is used for 'switch to active tab' i++; } - int other = tabs_->tabBar()->count(); + + int other = dynamicGrid_->count(); while (other >= 0) { other--; if (other != index) { - QString t = tabs_->tabBar()->tabText(other).toLower(); + int tabIndex = -1; + QtTabWidget* tabWidget = dynamicGrid_->indexToTabWidget(other, tabIndex); + QString t = tabWidget->tabBar()->tabText(tabIndex).toLower(); int r = t.indexOf('&'); if (r >= 0 && t[r+1] >= 'a' && t[r+1] <= 'z') { accelsTaken[t[r+1].unicode()-'a'] = true; @@ -235,24 +313,26 @@ void QtChatTabs::handleTabTitleUpdated(QWidget* widget) { // doesn't work on Arabic/Indic keyboards (where Latin letters // aren't available), but I don't care to deal with those. - tabs_->setTabText(index, tabbable->getCount() > 0 ? QString("(%1) %2").arg(tabbable->getCount()).arg(tabText) : tabText); + int tabIndex = -1; + QtTabWidget* tabWidget = dynamicGrid_->indexToTabWidget(index, tabIndex); + tabWidget->setTabText(tabIndex, tabbable->getCount() > 0 ? QString("(%1) %2").arg(tabbable->getCount()).arg(tabText) : tabText); QColor tabTextColor; switch (tabbable->getWidgetAlertState()) { case QtTabbable::WaitingActivity : tabTextColor = QColor(217, 20, 43); break; case QtTabbable::ImpendingActivity : tabTextColor = QColor(27, 171, 32); break; case QtTabbable::NoActivity : tabTextColor = QColor(); break; } - tabs_->tabBar()->setTabTextColor(index, tabTextColor); + tabWidget->tabBar()->setTabTextColor(tabIndex, tabTextColor); std::vector<std::pair<std::string, int> > unreads; - for (int i = 0; i < tabs_->count(); i++) { - QtTabbable* tab = qobject_cast<QtTabbable*>(tabs_->widget(i)); + for (int i = 0; i < dynamicGrid_->count(); i++) { + QtTabbable* tab = qobject_cast<QtTabbable*>(dynamicGrid_->widget(i)); if (tab) { unreads.push_back(std::pair<std::string, int>(Q2PSTRING(tab->windowTitle()), tab->getCount())); } } - std::string current(Q2PSTRING(qobject_cast<QtTabbable*>(tabs_->currentWidget())->windowTitle())); + std::string current(Q2PSTRING(qobject_cast<QtTabbable*>(dynamicGrid_->currentWidget())->windowTitle())); ChatMessageSummarizer summary; QString title = summary.getSummary(current, unreads).c_str(); setWindowTitle(title); @@ -265,6 +345,29 @@ void QtChatTabs::flash() { #endif } +void QtChatTabs::handleOpenLayoutChangeDialog() { + disconnect(gridSelectionDialog_, SIGNAL(currentGridSizeChanged(QSize)), dynamicGrid_, SLOT(setDimensions(QSize))); + gridSelectionDialog_->setCurrentGridSize(dynamicGrid_->getDimension()); + gridSelectionDialog_->move(QCursor::pos()); + connect(gridSelectionDialog_, SIGNAL(currentGridSizeChanged(QSize)), dynamicGrid_, SLOT(setDimensions(QSize))); + gridSelectionDialog_->show(); +} + +void QtChatTabs::storeTabPositions() { + // save size + QByteArray gridSizeData; + QDataStream dataStreamGridSize(&gridSizeData, QIODevice::ReadWrite); + dataStreamGridSize << dynamicGrid_->getDimension(); + settingsProvider_->storeSetting(SettingConstants::TRELLIS_GRID_SIZE, Q2PSTRING(QString(gridSizeData.toBase64()))); + + // save positions + QByteArray tabPositionsData; + QDataStream dataStreamTabPositions(&tabPositionsData, QIODevice::ReadWrite); + dynamicGrid_->updateTabPositions(); + dataStreamTabPositions << dynamicGrid_->getTabPositions(); + settingsProvider_->storeSetting(SettingConstants::TRELLIS_GRID_POSITIONS, Q2PSTRING(QString(tabPositionsData.toBase64()))); +} + void QtChatTabs::resizeEvent(QResizeEvent*) { emit geometryChanged(); } diff --git a/Swift/QtUI/QtChatTabs.h b/Swift/QtUI/QtChatTabs.h index f9cd685..99ab74a 100644 --- a/Swift/QtUI/QtChatTabs.h +++ b/Swift/QtUI/QtChatTabs.h @@ -1,26 +1,36 @@ /* - * Copyright (c) 2010 Kevin Smith + * Copyright (c) 2010-2014 Kevin Smith * Licensed under the GNU General Public License v3. * See Documentation/Licenses/GPLv3.txt for more information. */ #pragma once -#include "QtTabbable.h" -#include "QtTabWidget.h" #include <QWidget> #include <QRect> class QTabWidget; +class QMenu; namespace Swift { + class SettingsProvider; + + class QtTabbable; + class QtTabWidget; + class QtDynamicGridLayout; + class QtGridSelectionDialog; + class QtChatTabs : public QWidget { Q_OBJECT public: - QtChatTabs(bool singleWindow); + QtChatTabs(bool singleWindow, SettingsProvider* settingsProvider, bool trellisMode); + virtual ~QtChatTabs(); + void addTab(QtTabbable* tab); void minimise(); QtTabbable* getCurrentTab(); + void setViewMenu(QMenu* viewMenu); + signals: void geometryChanged(); void onTitleChanged(const QString& title); @@ -42,10 +52,18 @@ namespace Swift { void handleRequestedActiveTab(); void flash(); + void handleOpenLayoutChangeDialog(); + private: + void storeTabPositions(); void checkForFirstShow(); - QtTabWidget* tabs_; + + private: bool singleWindow_; + SettingsProvider* settingsProvider_; + bool trellisMode_; + QtDynamicGridLayout* dynamicGrid_; + QtGridSelectionDialog* gridSelectionDialog_; }; } diff --git a/Swift/QtUI/QtChatWindow.cpp b/Swift/QtUI/QtChatWindow.cpp index a54bf8d..964f67d 100644 --- a/Swift/QtUI/QtChatWindow.cpp +++ b/Swift/QtUI/QtChatWindow.cpp @@ -57,7 +57,7 @@ namespace Swift { -QtChatWindow::QtChatWindow(const QString &contact, QtChatTheme* theme, UIEventStream* eventStream, SettingsProvider* settings) : QtTabbable(), contact_(contact), nextAlertId_(0), eventStream_(eventStream), blockingState_(BlockingUnsupported), isMUC_(false), supportsImpromptuChat_(false) { +QtChatWindow::QtChatWindow(const QString &contact, QtChatTheme* theme, UIEventStream* eventStream, SettingsProvider* settings) : QtTabbable(), id_(Q2PSTRING(contact)), contact_(contact), nextAlertId_(0), eventStream_(eventStream), blockingState_(BlockingUnsupported), isMUC_(false), supportsImpromptuChat_(false) { settings_ = settings; unreadCount_ = 0; isOnline_ = true; @@ -170,7 +170,7 @@ QtChatWindow::QtChatWindow(const QString &contact, QtChatTheme* theme, UIEventSt settings_->onSettingChanged.connect(boost::bind(&QtChatWindow::handleSettingChanged, this, _1)); messageLog_->showEmoticons(settings_->getSetting(QtUISettingConstants::SHOW_EMOTICONS)); - + setMinimumSize(100, 100); } QtChatWindow::~QtChatWindow() { @@ -775,6 +775,10 @@ void QtChatWindow::showBookmarkWindow(const MUCBookmark& bookmark) { window->show(); } +std::string QtChatWindow::getID() const { + return id_; +} + void QtChatWindow::showRoomConfigurationForm(Form::ref form) { if (mucConfigurationWindow_) { delete mucConfigurationWindow_.data(); diff --git a/Swift/QtUI/QtChatWindow.h b/Swift/QtUI/QtChatWindow.h index a73782a..b3d8c3e 100644 --- a/Swift/QtUI/QtChatWindow.h +++ b/Swift/QtUI/QtChatWindow.h @@ -78,7 +78,7 @@ namespace Swift { public: QtChatWindow(const QString &contact, QtChatTheme* theme, UIEventStream* eventStream, SettingsProvider* settings); - ~QtChatWindow(); + virtual ~QtChatWindow(); std::string addMessage(const ChatMessage& message, const std::string &senderName, bool senderIsSelf, boost::shared_ptr<SecurityLabel> label, const std::string& avatarPath, const boost::posix_time::ptime& time, const HighlightAction& highlight); std::string addAction(const ChatMessage& message, const std::string &senderName, bool senderIsSelf, boost::shared_ptr<SecurityLabel> label, const std::string& avatarPath, const boost::posix_time::ptime& time, const HighlightAction& highlight); @@ -130,6 +130,7 @@ namespace Swift { void setBlockingState(BlockingState state); virtual void setCanInitiateImpromptuChats(bool supportsImpromptu); virtual void showBookmarkWindow(const MUCBookmark& bookmark); + virtual std::string getID() const; public slots: void handleChangeSplitterState(QByteArray state); @@ -178,10 +179,11 @@ namespace Swift { void handleOccupantSelectionChanged(RosterItem* item); void handleAppendedToLog(); - + private: int unreadCount_; bool contactIsTyping_; LastLineTracker lastLineTracker_; + std::string id_; QString contact_; QString lastSentMessage_; QTextCursor tabCompleteCursor_; diff --git a/Swift/QtUI/QtFileTransferListWidget.cpp b/Swift/QtUI/QtFileTransferListWidget.cpp index 01c632f..fb2b4de 100644 --- a/Swift/QtUI/QtFileTransferListWidget.cpp +++ b/Swift/QtUI/QtFileTransferListWidget.cpp @@ -69,6 +69,10 @@ void QtFileTransferListWidget::setFileTransferOverview(FileTransferOverview *ove itemModel->setFileTransferOverview(overview); } +std::string QtFileTransferListWidget::getID() const { + return "QtFileTransferListWidget"; +} + void QtFileTransferListWidget::closeEvent(QCloseEvent* event) { emit windowClosing(); event->accept(); diff --git a/Swift/QtUI/QtFileTransferListWidget.h b/Swift/QtUI/QtFileTransferListWidget.h index c828d4e..8adc009 100644 --- a/Swift/QtUI/QtFileTransferListWidget.h +++ b/Swift/QtUI/QtFileTransferListWidget.h @@ -31,6 +31,8 @@ public: void setFileTransferOverview(FileTransferOverview *); + virtual std::string getID() const; + private: virtual void closeEvent(QCloseEvent* event); virtual void showEvent(QShowEvent* event); diff --git a/Swift/QtUI/QtHistoryWindow.cpp b/Swift/QtUI/QtHistoryWindow.cpp index 75eeaad..ae03d7e 100644 --- a/Swift/QtUI/QtHistoryWindow.cpp +++ b/Swift/QtUI/QtHistoryWindow.cpp @@ -261,4 +261,8 @@ boost::gregorian::date QtHistoryWindow::getLastVisibleDate() { return boost::gregorian::date(boost::gregorian::not_a_date_time); } +std::string QtHistoryWindow::getID() const { + return "QtHistoryWindow"; +} + } diff --git a/Swift/QtUI/QtHistoryWindow.h b/Swift/QtUI/QtHistoryWindow.h index fcbfd7e..04ea61d 100644 --- a/Swift/QtUI/QtHistoryWindow.h +++ b/Swift/QtUI/QtHistoryWindow.h @@ -49,6 +49,8 @@ namespace Swift { std::string getSearchBoxText(); boost::gregorian::date getLastVisibleDate(); + virtual std::string getID() const; + signals: void fontResized(int); diff --git a/Swift/QtUI/QtLoginWindow.cpp b/Swift/QtUI/QtLoginWindow.cpp index eeb4317..21095f6 100644 --- a/Swift/QtUI/QtLoginWindow.cpp +++ b/Swift/QtUI/QtLoginWindow.cpp @@ -490,7 +490,9 @@ void QtLoginWindow::morphInto(MainWindow *mainWindow) { stack_->setCurrentWidget(qtMainWindow); setEnabled(true); setInitialMenus(); - foreach (QMenu* menu, qtMainWindow->getMenus()) { + std::vector<QMenu*> mainWindowMenus = qtMainWindow->getMenus(); + viewMenu_ = mainWindowMenus[0]; + foreach (QMenu* menu, mainWindowMenus) { menuBar_->addMenu(menu); } setFocus(); diff --git a/Swift/QtUI/QtLoginWindow.h b/Swift/QtUI/QtLoginWindow.h index 7415fbf..55412bf 100644 --- a/Swift/QtUI/QtLoginWindow.h +++ b/Swift/QtUI/QtLoginWindow.h @@ -97,6 +97,7 @@ namespace Swift { QMenuBar* menuBar_; QMenu* swiftMenu_; QMenu* generalMenu_; + QMenu* viewMenu_; QAction* toggleSoundsAction_; QAction* toggleNotificationsAction_; UIEventStream* uiEventStream_; diff --git a/Swift/QtUI/QtMainWindow.cpp b/Swift/QtUI/QtMainWindow.cpp index 52b6bcc..55c2727 100644 --- a/Swift/QtUI/QtMainWindow.cpp +++ b/Swift/QtUI/QtMainWindow.cpp @@ -6,41 +6,41 @@ #include <Swift/QtUI/QtMainWindow.h> -#include <boost/optional.hpp> #include <boost/bind.hpp> +#include <boost/optional.hpp> #include <boost/smart_ptr/make_shared.hpp> +#include <QAction> #include <QBoxLayout> #include <QComboBox> #include <QLineEdit> #include <QListWidget> #include <QListWidgetItem> -#include <QPushButton> #include <QMenuBar> -#include <QToolBar> -#include <QAction> +#include <QPushButton> #include <QTabWidget> +#include <QToolBar> #include <Swiften/Base/Platform.h> -#include <Swift/Controllers/UIEvents/RequestJoinMUCUIEvent.h> -#include <Swift/Controllers/UIEvents/RequestHistoryUIEvent.h> -#include <Swift/Controllers/UIEvents/RequestAddUserDialogUIEvent.h> -#include <Swift/Controllers/UIEvents/RequestChatWithUserDialogUIEvent.h> -#include <Swift/Controllers/UIEvents/RequestProfileEditorUIEvent.h> +#include <Swift/Controllers/SettingConstants.h> #include <Swift/Controllers/UIEvents/JoinMUCUIEvent.h> #include <Swift/Controllers/UIEvents/RequestAdHocUIEvent.h> +#include <Swift/Controllers/UIEvents/RequestAddUserDialogUIEvent.h> #include <Swift/Controllers/UIEvents/RequestBlockListDialogUIEvent.h> -#include <Swift/Controllers/SettingConstants.h> +#include <Swift/Controllers/UIEvents/RequestChatWithUserDialogUIEvent.h> +#include <Swift/Controllers/UIEvents/RequestHistoryUIEvent.h> +#include <Swift/Controllers/UIEvents/RequestJoinMUCUIEvent.h> +#include <Swift/Controllers/UIEvents/RequestProfileEditorUIEvent.h> -#include <Swift/QtUI/Roster/QtFilterWidget.h> +#include <Swift/QtUI/QtAdHocCommandWithJIDWindow.h> +#include <Swift/QtUI/QtLoginWindow.h> +#include <Swift/QtUI/QtSettingsProvider.h> #include <Swift/QtUI/QtSwiftUtil.h> #include <Swift/QtUI/QtTabWidget.h> -#include <Swift/QtUI/QtSettingsProvider.h> -#include <Swift/QtUI/QtLoginWindow.h> -#include <Swift/QtUI/Roster/QtRosterWidget.h> #include <Swift/QtUI/QtUISettingConstants.h> -#include <Swift/QtUI/QtAdHocCommandWithJIDWindow.h> +#include <Swift/QtUI/Roster/QtFilterWidget.h> +#include <Swift/QtUI/Roster/QtRosterWidget.h> #if defined(SWIFTEN_PLATFORM_MACOSX) #include <Swift/QtUI/CocoaUIHelpers.h> #elif defined(SWIFTEN_PLATFORM_WINDOWS) @@ -139,12 +139,6 @@ QtMainWindow::QtMainWindow(SettingsProvider* settings, UIEventStream* uiEventStr handleShowEmoticonsToggled(settings_->getSetting(QtUISettingConstants::SHOW_EMOTICONS)); } - //QAction* compactRosterAction_ = new QAction(tr("&Compact Roster"), this); - //compactRosterAction_->setCheckable(true); - //compactRosterAction_->setChecked(false); - //connect(compactRosterAction_, SIGNAL(toggled(bool)), uiPreferences_, SLOT(setCompactRosters(bool))); - //viewMenu->addAction(compactRosterAction_); - QMenu* actionsMenu = new QMenu(tr("&Actions"), this); menus_.push_back(actionsMenu); QAction* editProfileAction = new QAction(tr("Edit &Profile…"), this); diff --git a/Swift/QtUI/QtSwift.cpp b/Swift/QtUI/QtSwift.cpp index d2224ba..5e798cf 100644 --- a/Swift/QtUI/QtSwift.cpp +++ b/Swift/QtUI/QtSwift.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2013 Kevin Smith + * Copyright (c) 2010-2014 Kevin Smith * Licensed under the GNU General Public License v3. * See Documentation/Licenses/GPLv3.txt for more information. */ @@ -90,6 +90,7 @@ po::options_description QtSwift::getOptionsDescription() { ("multi-account", po::value<int>()->default_value(1), "Number of accounts to open windows for (unsupported)") ("start-minimized", "Don't show the login/roster window at startup") ("enable-jid-adhocs", "Enable AdHoc commands to custom JID's.") + ("trellis", "Enable support for trellis layout") #if QT_VERSION >= 0x040800 ("language", po::value<std::string>(), "Use a specific language, instead of the system-wide one") #endif @@ -164,7 +165,7 @@ QtSwift::QtSwift(const po::variables_map& options) : networkFactories_(&clientMa } bool enableAdHocCommandOnJID = options.count("enable-jid-adhocs") > 0; - tabs_ = options.count("no-tabs") && !splitter_ ? NULL : new QtChatTabs(splitter_ != NULL); + tabs_ = options.count("no-tabs") && !splitter_ ? NULL : new QtChatTabs(splitter_ != NULL, settingsHierachy_, options.count("trellis")); bool startMinimized = options.count("start-minimized") > 0; applicationPathProvider_ = new PlatformApplicationPathProvider(SWIFT_APPLICATION_NAME); storagesFactory_ = new FileStoragesFactory(applicationPathProvider_->getDataDir(), networkFactories_.getCryptoProvider()); diff --git a/Swift/QtUI/QtSwift.h b/Swift/QtUI/QtSwift.h index 1ea8886..fb8224c 100644 --- a/Swift/QtUI/QtSwift.h +++ b/Swift/QtUI/QtSwift.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2012 Kevin Smith + * Copyright (c) 2010-2014 Kevin Smith * Licensed under the GNU General Public License v3. * See Documentation/Licenses/GPLv3.txt for more information. */ diff --git a/Swift/QtUI/QtTabWidget.cpp b/Swift/QtUI/QtTabWidget.cpp index 614439a..a5e6cdc 100644 --- a/Swift/QtUI/QtTabWidget.cpp +++ b/Swift/QtUI/QtTabWidget.cpp @@ -20,4 +20,7 @@ QTabBar* QtTabWidget::tabBar() { return QTabWidget::tabBar(); } +void QtTabWidget::setTabBar(QTabBar* tabBar) { + QTabWidget::setTabBar(tabBar); +} } diff --git a/Swift/QtUI/QtTabWidget.h b/Swift/QtUI/QtTabWidget.h index e4a44ce..7a73b75 100644 --- a/Swift/QtUI/QtTabWidget.h +++ b/Swift/QtUI/QtTabWidget.h @@ -5,14 +5,17 @@ */ #pragma once + #include <QTabWidget> namespace Swift { class QtTabWidget : public QTabWidget { - Q_OBJECT + Q_OBJECT public: QtTabWidget(QWidget* parent); - ~QtTabWidget(); + virtual ~QtTabWidget(); + QTabBar* tabBar(); + void setTabBar(QTabBar* tabBar); }; } diff --git a/Swift/QtUI/QtTabbable.cpp b/Swift/QtUI/QtTabbable.cpp index 124d1b4..cc901c7 100644 --- a/Swift/QtUI/QtTabbable.cpp +++ b/Swift/QtUI/QtTabbable.cpp @@ -8,6 +8,7 @@ #include <QApplication> #include <QKeyEvent> +#include <QShortcut> #include <Swiften/Base/foreach.h> #include <Swiften/Base/Platform.h> @@ -45,13 +46,17 @@ bool QtTabbable::event(QEvent* event) { if (keyEvent) { // According to Qt's focus documentation, one can only override CTRL+TAB via reimplementing QWidget::event(). #ifdef SWIFTEN_PLATFORM_LINUX - if (keyEvent->modifiers() == QtUtilities::ctrlHardwareKeyModifier && keyEvent->key() == Qt::Key_Tab && event->type() != QEvent::KeyRelease) { + if (keyEvent->modifiers().testFlag(QtUtilities::ctrlHardwareKeyModifier) && keyEvent->key() == Qt::Key_Tab && event->type() != QEvent::KeyRelease) { #else - if (keyEvent->modifiers() == QtUtilities::ctrlHardwareKeyModifier && keyEvent->key() == Qt::Key_Tab) { + if (keyEvent->modifiers().testFlag(QtUtilities::ctrlHardwareKeyModifier) && keyEvent->key() == Qt::Key_Tab) { #endif emit requestNextTab(); return true; } + else if (keyEvent->modifiers().testFlag(QtUtilities::ctrlHardwareKeyModifier) && keyEvent->key() == Qt::Key_Backtab) { + emit requestPreviousTab(); + return true; + } } return QWidget::event(event); } diff --git a/Swift/QtUI/QtTabbable.h b/Swift/QtUI/QtTabbable.h index fc131ca..7201efe 100644 --- a/Swift/QtUI/QtTabbable.h +++ b/Swift/QtUI/QtTabbable.h @@ -1,15 +1,17 @@ /* - * Copyright (c) 2010 Kevin Smith + * Copyright (c) 2014 Kevin Smith * Licensed under the GNU General Public License v3. * See Documentation/Licenses/GPLv3.txt for more information. */ #pragma once -#include <QWidget> -#include <QShortcut> +#include <string> + #include <QList> +#include <QWidget> +class QShortcut; namespace Swift { class QtTabbable : public QWidget { @@ -20,6 +22,8 @@ namespace Swift { bool isWidgetSelected(); virtual AlertType getWidgetAlertState() {return NoActivity;} virtual int getCount() {return 0;} + virtual std::string getID() const = 0; + protected: QtTabbable(); bool event(QEvent* event); diff --git a/Swift/QtUI/QtUIFactory.cpp b/Swift/QtUI/QtUIFactory.cpp index b0c1492..e99b305 100644 --- a/Swift/QtUI/QtUIFactory.cpp +++ b/Swift/QtUI/QtUIFactory.cpp @@ -82,6 +82,7 @@ FileTransferListWidget* QtUIFactory::createFileTransferListWidget() { MainWindow* QtUIFactory::createMainWindow(UIEventStream* eventStream) { lastMainWindow = new QtMainWindow(settings, eventStream, loginWindow->getMenus(), statusCache, emoticonsExist_, enableAdHocCommandOnJID_); + tabs->setViewMenu(lastMainWindow->getMenus()[0]); return lastMainWindow; } diff --git a/Swift/QtUI/QtXMLConsoleWidget.cpp b/Swift/QtUI/QtXMLConsoleWidget.cpp index f8ab28e..f125fc0 100644 --- a/Swift/QtUI/QtXMLConsoleWidget.cpp +++ b/Swift/QtUI/QtXMLConsoleWidget.cpp @@ -89,6 +89,10 @@ void QtXMLConsoleWidget::handleDataWritten(const SafeByteArray& data) { appendTextIfEnabled(tag + "\n" + safeByteArrayToString(data) + "\n", QColor(155,1,0)); } +std::string QtXMLConsoleWidget::getID() const { + return "QtXMLConsoleWidget"; +} + void QtXMLConsoleWidget::appendTextIfEnabled(const std::string& data, const QColor& color) { if (enabled->isChecked()) { QScrollBar* scrollBar = textEdit->verticalScrollBar(); diff --git a/Swift/QtUI/QtXMLConsoleWidget.h b/Swift/QtUI/QtXMLConsoleWidget.h index 912ad90..3cf27cf 100644 --- a/Swift/QtUI/QtXMLConsoleWidget.h +++ b/Swift/QtUI/QtXMLConsoleWidget.h @@ -27,6 +27,8 @@ namespace Swift { virtual void handleDataRead(const SafeByteArray& data); virtual void handleDataWritten(const SafeByteArray& data); + virtual std::string getID() const; + private: virtual void closeEvent(QCloseEvent* event); virtual void showEvent(QShowEvent* event); diff --git a/Swift/QtUI/SConscript b/Swift/QtUI/SConscript index 80daaea..6e90cd4 100644 --- a/Swift/QtUI/SConscript +++ b/Swift/QtUI/SConscript @@ -193,7 +193,10 @@ sources = [ "QtUISettingConstants.cpp", "QtURLValidator.cpp", "QtResourceHelper.cpp", - "QtSpellCheckHighlighter.cpp" + "QtSpellCheckHighlighter.cpp", + "Trellis/QtDynamicGridLayout.cpp", + "Trellis/QtDNDTabBar.cpp", + "Trellis/QtGridSelectionDialog.cpp" ] # QtVCardWidget diff --git a/Swift/QtUI/Trellis/QtDNDTabBar.cpp b/Swift/QtUI/Trellis/QtDNDTabBar.cpp new file mode 100644 index 0000000..af9c0f5 --- /dev/null +++ b/Swift/QtUI/Trellis/QtDNDTabBar.cpp @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2014 Kevin Smith and Remko Tronçon + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#include <Swift/QtUI/Trellis/QtDNDTabBar.h> + +#include <cassert> + +#include <QMouseEvent> +#include <QDropEvent> +#include <QDrag> +#include <QMimeData> +#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; +} + +void QtDNDTabBar::dragEnterEvent(QDragEnterEvent* dragEnterEvent) { + QtDNDTabBar* sourceTabBar = dynamic_cast<QtDNDTabBar*>(dragEnterEvent->source()); + if (sourceTabBar) { + dragEnterEvent->acceptProposedAction(); + } +} + +void QtDNDTabBar::dropEvent(QDropEvent* dropEvent) { + QtDNDTabBar* sourceTabBar = dynamic_cast<QtDNDTabBar*>(dropEvent->source()); + if (sourceTabBar && dropEvent->mimeData() && dropEvent->mimeData()->data("action") == QByteArray("application/tab-detach")) { + QtDNDTabBar* source = dynamic_cast<QtDNDTabBar*>(dropEvent->source()); + + int targetTabIndex = tabAt(dropEvent->pos()); + QRect rect = tabRect(targetTabIndex); + if (targetTabIndex >= 0 && (dropEvent->pos().x() - rect.left() - rect.width()/2 > 0)) { + targetTabIndex++; + } + + QWidget* tab = source->getDragWidget(); + assert(tab); + QTabWidget* targetTabWidget = dynamic_cast<QTabWidget*>(parentWidget()); + + QString tabText = source->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 = NULL; + + // 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 + dynamic_cast<QTabWidget*>(parent())->insertTab(dragIndex, dragWidget, dragText); + } + 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..4f26bc1 --- /dev/null +++ b/Swift/QtUI/Trellis/QtDNDTabBar.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2014 Kevin Smith and Remko Tronçon + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#pragma once + +#include <QTabBar> + +namespace Swift { + +class QtDNDTabBar : public QTabBar { + Q_OBJECT + public: + explicit QtDNDTabBar(QWidget* parent = 0); + virtual ~QtDNDTabBar(); + + int getDragIndex() const; + QString getDragText() const; + QWidget* getDragWidget() const; + + virtual QSize sizeHint() const; + + signals: + void onDropSucceeded(); + + protected: + void dragEnterEvent(QDragEnterEvent* dragEnterEvent); + void dropEvent(QDropEvent* dropEvent); + bool event(QEvent* event); + + private: + int defaultTabHeight; + int dragIndex; + QString dragText; + QWidget* dragWidget; +}; + +} diff --git a/Swift/QtUI/Trellis/QtDynamicGridLayout.cpp b/Swift/QtUI/Trellis/QtDynamicGridLayout.cpp new file mode 100644 index 0000000..3b33dd3 --- /dev/null +++ b/Swift/QtUI/Trellis/QtDynamicGridLayout.cpp @@ -0,0 +1,431 @@ +/* + * Copyright (c) 2014 Kevin Smith and Remko Tronçon + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#include <Swift/QtUI/Trellis/QtDynamicGridLayout.h> + +#include <cassert> + +#include <QLayoutItem> +#include <QGridLayout> +#include <QtDebug> + +#include <Swift/QtUI/QtSwiftUtil.h> +#include <Swift/QtUI/QtTabbable.h> +#include <Swift/QtUI/QtTabWidget.h> +#include <Swift/QtUI/Trellis/QtDNDTabBar.h> + +namespace Swift { + +QtDynamicGridLayout::QtDynamicGridLayout(QWidget* parent, bool enableDND) : QWidget(parent), dndEnabled_(enableDND) { + gridLayout_ = new QGridLayout(this); + setContentsMargins(0,0,0,0); + setDimensions(QSize(1,1)); +} + +QtDynamicGridLayout::~QtDynamicGridLayout() { +} + +int QtDynamicGridLayout::addTab(QtTabbable* tab, const QString& title) { + assert(gridLayout_->rowCount() > 0 && gridLayout_->columnCount() > 0); + int index = -1; + + 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() : 0); + if (tabWidget) { + index = tabWidget->addTab(tab, title); + } + 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 = NULL; + 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()) { + 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; +} + +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) { + tabWidget->removeTab(tabIndex); + } +} + +QWidget* QtDynamicGridLayout::currentWidget() const { + QWidget* current = NULL; + current = focusWidget(); + while (current && !dynamic_cast<QtTabbable*>(current)) { + if (current->parentWidget()) { + current = current->parentWidget(); + } else { + current = NULL; + break; + } + } + if (!current) { + current = widget(0); + } + return current; +} + +void QtDynamicGridLayout::setCurrentWidget(QWidget* 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 NULL; + } + } + } + } + } + tabIndex = -1; + return NULL; +} + +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) { + assert(dim.width() > 0 && dim.height() > 0); + setUpdatesEnabled(false); + + QGridLayout* oldLayout = dynamic_cast<QGridLayout*>(layout()); + QGridLayout* newLayout = new QGridLayout; + 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); + newTabWidget->addTab(oldTabWidget->widget(0), icon, text); + } + 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; +} + +void QtDynamicGridLayout::moveCurrentTabRight() { + int index = currentIndex(); + 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(); + 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(); + 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(); + 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::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) { + tabWidget->widget(oldIndex)->blockSignals(true); + tabWidget->widget(newIndex)->blockSignals(true); +#if QT_VERSION >= 0x040500 + tabWidget->tabBar()->moveTab(oldIndex, newIndex); +#else +#warning Qt 4.5 or later is needed. Trying anyway, some things will be disabled. + tabWidget->widget(oldIndex)->blockSignals(false); + tabWidget->widget(newIndex)->blockSignals(false); +#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))); +#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..c87dc21 --- /dev/null +++ b/Swift/QtUI/Trellis/QtDynamicGridLayout.h @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2014 Kevin Smith and Remko Tronçon + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt 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(QWidget* parent = 0, 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); + + signals: + void tabCloseRequested(int index); + + 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); + + private: + void moveTab(QtTabWidget* tabWidget, int oldIndex, int newIndex); + QtTabWidget* createDNDTabWidget(QWidget* parent); + + private: + QGridLayout *gridLayout_; + bool dndEnabled_; + QHash<QString, QPoint> tabPositions_; + }; +} diff --git a/Swift/QtUI/Trellis/QtGridSelectionDialog.cpp b/Swift/QtUI/Trellis/QtGridSelectionDialog.cpp new file mode 100644 index 0000000..a7d3672 --- /dev/null +++ b/Swift/QtUI/Trellis/QtGridSelectionDialog.cpp @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2014 Kevin Smith and Remko Tronçon + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#include <Swift/QtUI/Trellis/QtGridSelectionDialog.h> + +#include <QStyle> +#include <QPaintEvent> +#include <QPainter> +#include <QStyleOptionFrame> +#include <QStyleOptionMenuItem> + +namespace Swift { + +QtGridSelectionDialog::QtGridSelectionDialog(QWidget* parent) : QWidget(parent) +{ + frameSize = QSize(23,23) * 2; + maxGridSize = QSize(7,7); + minGridSize = QSize(1,1); + currentGridSize = QSize(1,1); + padding = 4; + setWindowFlags(Qt::Popup); + horizontalMargin = style()->pixelMetric(QStyle::PM_MenuVMargin); + verticalMargin = style()->pixelMetric(QStyle::PM_MenuVMargin); + +} + +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; + 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; +} + +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); + } + if (minGridSize.expandedTo(newGridSize).boundedTo(maxGridSize) != currentGridSize) { + currentGridSize = minGridSize.expandedTo(newGridSize).boundedTo(maxGridSize); + resize(sizeHint()); + } + } +} + +void QtGridSelectionDialog::mousePressEvent(QMouseEvent*) { + hide(); + setCurrentGridSize(currentGridSize); +} + +void QtGridSelectionDialog::paintEvent(QPaintEvent*) { + QPainter painter(this); + QStyleOptionMenuItem option; + option.state = QStyle::State_Enabled | QStyle::State_Selected; + option.menuRect = QRect(QPoint(0,0), frameSize); + 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)); + option.menuRect.moveTo(QPoint(xPos, yPos)); + option.rect = option.menuRect; + style()->drawControl(QStyle::CE_MenuItem, &option, &painter, 0); + } + } +} + +void QtGridSelectionDialog::showEvent(QShowEvent*) { + int xPos = horizontalMargin + frameSize.width() + (padding + frameSize.width()) * (currentGridSize.width() - 1) - frameSize.width()/2; + int yPos = verticalMargin + frameSize.height() + (padding + frameSize.height()) * (currentGridSize.height() - 1) - frameSize.height()/2; + QCursor::setPos(mapToGlobal(QPoint(xPos, yPos))); + setMouseTracking(true); +} + +void QtGridSelectionDialog::hideEvent(QHideEvent*) { + setMouseTracking(false); +} + +void QtGridSelectionDialog::mouseMoveEvent(QMouseEvent*) { + QPoint diff = (frameGeometry().bottomRight() - QCursor::pos()); + QSize newDimensions = currentGridSize; + if (diff.x() > frameSize.width() * 1.5) { + newDimensions -= QSize(diff.x() / (frameSize.width() * 1.5), 0); + } + if (diff.y() > frameSize.height() * 1.5) { + newDimensions -= QSize(0, diff.y() / (frameSize.height() * 1.5)); + } + if (minGridSize.expandedTo(newDimensions).boundedTo(maxGridSize) != currentGridSize) { + currentGridSize = minGridSize.expandedTo(newDimensions).boundedTo(maxGridSize); + resize(sizeHint()); + } +} + +void QtGridSelectionDialog::leaveEvent(QEvent *) { + QPoint diff = (frameGeometry().bottomRight() - QCursor::pos()); + QSize newGridSize = currentGridSize; + if (diff.x() < 0) { + newGridSize += QSize(1,0); + } + if (diff.y() < 0) { + newGridSize += QSize(0,1); + } + if (minGridSize.expandedTo(newGridSize).boundedTo(maxGridSize) != currentGridSize) { + currentGridSize = minGridSize.expandedTo(newGridSize).boundedTo(maxGridSize); + resize(sizeHint()); + } +} + +} diff --git a/Swift/QtUI/Trellis/QtGridSelectionDialog.h b/Swift/QtUI/Trellis/QtGridSelectionDialog.h new file mode 100644 index 0000000..430189a --- /dev/null +++ b/Swift/QtUI/Trellis/QtGridSelectionDialog.h @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2014 Kevin Smith and Remko Tronçon + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt 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 = 0); + + 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; + + signals: + void currentGridSizeChanged(QSize); + void minGridSizeChanged(QSize); + void maxGridSizeChanged(QSize); + + protected: + void keyReleaseEvent(QKeyEvent* event); + void mousePressEvent(QMouseEvent* event); + void mouseMoveEvent(QMouseEvent* event); + void paintEvent(QPaintEvent* event); + void showEvent(QShowEvent* event); + void hideEvent(QHideEvent* event); + void leaveEvent(QEvent *event); + + private: + int padding; + int horizontalMargin; + int verticalMargin; + + QSize frameSize; + + QSize currentGridSize; + QSize minGridSize; + QSize maxGridSize; + }; +} -- cgit v0.10.2-6-g49f6