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