summaryrefslogtreecommitdiffstats
path: root/gui
diff options
context:
space:
mode:
Diffstat (limited to 'gui')
-rw-r--r--gui/ConsoleWindow.cpp18
-rw-r--r--gui/ConsoleWindow.h2
-rw-r--r--gui/MainWindow.cpp236
-rw-r--r--gui/MainWindow.h21
-rw-r--r--gui/MainWindow.ui60
-rw-r--r--gui/dialogs/AccountListDialog.cpp2
-rw-r--r--gui/dialogs/IconPickerDialog.cpp2
-rw-r--r--gui/dialogs/OneSixModEditDialog.cpp256
-rw-r--r--gui/dialogs/OneSixModEditDialog.h12
-rw-r--r--gui/dialogs/OneSixModEditDialog.ui65
-rw-r--r--gui/dialogs/VersionSelectDialog.cpp5
-rw-r--r--gui/dialogs/VersionSelectDialog.h1
-rw-r--r--gui/dialogs/VersionSelectDialog.ui9
-rw-r--r--gui/groupview/Group.cpp269
-rw-r--r--gui/groupview/Group.h69
-rw-r--r--gui/groupview/GroupView.cpp931
-rw-r--r--gui/groupview/GroupView.h143
-rw-r--r--gui/groupview/GroupedProxyModel.cpp26
-rw-r--r--gui/groupview/GroupedProxyModel.h15
-rw-r--r--gui/groupview/InstanceDelegate.cpp (renamed from gui/widgets/InstanceDelegate.cpp)32
-rw-r--r--gui/groupview/InstanceDelegate.h (renamed from gui/widgets/InstanceDelegate.h)8
-rw-r--r--gui/widgets/Common.cpp27
-rw-r--r--gui/widgets/Common.h6
-rw-r--r--gui/widgets/VersionListView.cpp150
-rw-r--r--gui/widgets/VersionListView.h43
25 files changed, 2146 insertions, 262 deletions
diff --git a/gui/ConsoleWindow.cpp b/gui/ConsoleWindow.cpp
index dc36a8ff..ccc037f2 100644
--- a/gui/ConsoleWindow.cpp
+++ b/gui/ConsoleWindow.cpp
@@ -84,7 +84,7 @@ void ConsoleWindow::iconActivated(QSystemTrayIcon::ActivationReason reason)
}
}
-void ConsoleWindow::writeColor(QString text, const char *color)
+void ConsoleWindow::writeColor(QString text, const char *color, const char * background)
{
// append a paragraph
QString newtext;
@@ -92,6 +92,8 @@ void ConsoleWindow::writeColor(QString text, const char *color)
{
if (color)
newtext += QString("color:") + color + ";";
+ if (background)
+ newtext += QString("background-color:") + background + ";";
newtext += "font-family: monospace;";
}
newtext += "\">";
@@ -127,26 +129,26 @@ void ConsoleWindow::write(QString data, MessageLevel::Enum mode)
QListIterator<QString> iter(paragraphs);
if (mode == MessageLevel::MultiMC)
while (iter.hasNext())
- writeColor(iter.next(), "blue");
+ writeColor(iter.next(), "blue", 0);
else if (mode == MessageLevel::Error)
while (iter.hasNext())
- writeColor(iter.next(), "red");
+ writeColor(iter.next(), "red", 0);
else if (mode == MessageLevel::Warning)
while (iter.hasNext())
- writeColor(iter.next(), "orange");
+ writeColor(iter.next(), "orange", 0);
else if (mode == MessageLevel::Fatal)
while (iter.hasNext())
- writeColor(iter.next(), "pink");
+ writeColor(iter.next(), "red", "black");
else if (mode == MessageLevel::Debug)
while (iter.hasNext())
- writeColor(iter.next(), "green");
+ writeColor(iter.next(), "green", 0);
else if (mode == MessageLevel::PrePost)
while (iter.hasNext())
- writeColor(iter.next(), "grey");
+ writeColor(iter.next(), "grey", 0);
// TODO: implement other MessageLevels
else
while (iter.hasNext())
- writeColor(iter.next());
+ writeColor(iter.next(), 0, 0);
if(isVisible())
{
if (m_scroll_active)
diff --git a/gui/ConsoleWindow.h b/gui/ConsoleWindow.h
index 9291320e..7fe90c52 100644
--- a/gui/ConsoleWindow.h
+++ b/gui/ConsoleWindow.h
@@ -47,7 +47,7 @@ private:
* this will only insert a single paragraph.
* \n are ignored. a real \n is always appended.
*/
- void writeColor(QString data, const char *color = nullptr);
+ void writeColor(QString text, const char *color, const char *background);
signals:
void isClosing();
diff --git a/gui/MainWindow.cpp b/gui/MainWindow.cpp
index ee9c3fad..29f7c8e8 100644
--- a/gui/MainWindow.cpp
+++ b/gui/MainWindow.cpp
@@ -26,6 +26,7 @@
#include <QInputDialog>
#include <QDesktopServices>
+#include <QKeyEvent>
#include <QUrl>
#include <QDir>
#include <QFileInfo>
@@ -37,12 +38,12 @@
#include "userutils.h"
#include "pathutils.h"
-#include "categorizedview.h"
-#include "categorydrawer.h"
+#include "gui/groupview/GroupView.h"
+#include "gui/groupview/InstanceDelegate.h"
#include "gui/Platform.h"
-#include "gui/widgets/InstanceDelegate.h"
+
#include "gui/widgets/LabeledToolButton.h"
#include "gui/dialogs/SettingsDialog.h"
@@ -71,7 +72,6 @@
#include "logic/auth/flows/AuthenticateTask.h"
#include "logic/auth/flows/RefreshTask.h"
-#include "logic/auth/flows/ValidateTask.h"
#include "logic/updater/DownloadUpdateTask.h"
@@ -132,28 +132,30 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi
newsLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
newsLabel->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
ui->newsToolBar->insertWidget(ui->actionMoreNews, newsLabel);
- QObject::connect(newsLabel, &QAbstractButton::clicked, this, &MainWindow::newsButtonClicked);
- QObject::connect(MMC->newsChecker().get(), &NewsChecker::newsLoaded, this, &MainWindow::updateNewsLabel);
+ QObject::connect(newsLabel, &QAbstractButton::clicked, this,
+ &MainWindow::newsButtonClicked);
+ QObject::connect(MMC->newsChecker().get(), &NewsChecker::newsLoaded, this,
+ &MainWindow::updateNewsLabel);
updateNewsLabel();
}
// Create the instance list widget
{
- view = new KCategorizedView(ui->centralWidget);
- drawer = new KCategoryDrawer(view);
+ view = new GroupView(ui->centralWidget);
+
view->setSelectionMode(QAbstractItemView::SingleSelection);
- view->setCategoryDrawer(drawer);
- view->setCollapsibleBlocks(true);
- view->setViewMode(QListView::IconMode);
- view->setFlow(QListView::LeftToRight);
- view->setWordWrap(true);
- view->setMouseTracking(true);
- view->viewport()->setAttribute(Qt::WA_Hover);
+ // view->setCategoryDrawer(drawer);
+ // view->setCollapsibleBlocks(true);
+ // view->setViewMode(QListView::IconMode);
+ // view->setFlow(QListView::LeftToRight);
+ // view->setWordWrap(true);
+ // view->setMouseTracking(true);
+ // view->viewport()->setAttribute(Qt::WA_Hover);
auto delegate = new ListViewDelegate();
view->setItemDelegate(delegate);
- view->setSpacing(10);
- view->setUniformItemWidths(true);
+ // view->setSpacing(10);
+ // view->setUniformItemWidths(true);
// do not show ugly blue border on the mac
view->setAttribute(Qt::WA_MacShowFocusRect, false);
@@ -173,8 +175,8 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi
view->setModel(proxymodel);
view->setContextMenuPolicy(Qt::CustomContextMenu);
- connect(view, SIGNAL(customContextMenuRequested(const QPoint&)),
- this, SLOT(showInstanceContextMenu(const QPoint&)));
+ connect(view, SIGNAL(customContextMenuRequested(const QPoint &)), this,
+ SLOT(showInstanceContextMenu(const QPoint &)));
ui->horizontalLayout->addWidget(view);
}
@@ -213,8 +215,10 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi
// Start status checker
{
- connect(MMC->statusChecker().get(), &StatusChecker::statusLoaded, this, &MainWindow::updateStatusUI);
- connect(MMC->statusChecker().get(), &StatusChecker::statusLoadingFailed, this, &MainWindow::updateStatusFailedUI);
+ connect(MMC->statusChecker().get(), &StatusChecker::statusLoaded, this,
+ &MainWindow::updateStatusUI);
+ connect(MMC->statusChecker().get(), &StatusChecker::statusLoadingFailed, this,
+ &MainWindow::updateStatusFailedUI);
connect(m_statusRefresh, &QAbstractButton::clicked, this, &MainWindow::reloadStatus);
connect(&statusTimer, &QTimer::timeout, this, &MainWindow::reloadStatus);
@@ -313,8 +317,9 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi
if (MMC->settings()->get("AutoUpdate").toBool())
on_actionCheckUpdate_triggered();
- connect(MMC->notificationChecker().get(), &NotificationChecker::notificationCheckFinished,
- this, &MainWindow::notificationsChanged);
+ connect(MMC->notificationChecker().get(),
+ &NotificationChecker::notificationCheckFinished, this,
+ &MainWindow::notificationsChanged);
}
setSelectedInstanceById(MMC->settings()->get("SelectedInstance").toString());
@@ -327,12 +332,11 @@ MainWindow::~MainWindow()
{
delete ui;
delete proxymodel;
- delete drawer;
}
-void MainWindow::showInstanceContextMenu(const QPoint& pos)
+void MainWindow::showInstanceContextMenu(const QPoint &pos)
{
- if(!view->indexAt(pos).isValid())
+ if (!view->indexAt(pos).isValid())
{
return;
}
@@ -522,9 +526,12 @@ static QString convertStatus(const QString &status)
{
QString ret = "?";
- if(status == "green") ret = "↑";
- else if(status == "yellow") ret = "-";
- else if(status == "red") ret="↓";
+ if (status == "green")
+ ret = "↑";
+ else if (status == "yellow")
+ ret = "-";
+ else if (status == "red")
+ ret = "↓";
return "<span style=\"font-size:11pt; font-weight:600;\">" + ret + "</span>";
}
@@ -533,7 +540,7 @@ void MainWindow::reloadStatus()
{
m_statusRefresh->setChecked(true);
MMC->statusChecker()->reloadStatus();
- //updateStatusUI();
+ // updateStatusUI();
}
static QString makeStatusString(const QMap<QString, QString> statuses)
@@ -632,7 +639,8 @@ void MainWindow::notificationsChanged()
}
QMessageBox box(icon, tr("Notification"), entry.message, QMessageBox::Close, this);
- QPushButton *dontShowAgainButton = box.addButton(tr("Don't show again"), QMessageBox::AcceptRole);
+ QPushButton *dontShowAgainButton =
+ box.addButton(tr("Don't show again"), QMessageBox::AcceptRole);
box.setDefaultButton(QMessageBox::Close);
box.exec();
if (box.clickedButton() == dontShowAgainButton)
@@ -657,9 +665,9 @@ void MainWindow::downloadUpdates(QString repo, int versionId, bool installOnExit
if (updateDlg.exec(&updateTask))
{
UpdateFlags baseFlags = None;
- #ifdef MultiMC_UPDATER_DRY_RUN
- baseFlags |= DryRun;
- #endif
+#ifdef MultiMC_UPDATER_DRY_RUN
+ baseFlags |= DryRun;
+#endif
if (installOnExit)
MMC->installUpdates(updateTask.updateFilesDir(), baseFlags | OnExit);
else
@@ -677,7 +685,7 @@ void MainWindow::setCatBackground(bool enabled)
{
if (enabled)
{
- view->setStyleSheet("QListView"
+ view->setStyleSheet("GroupView"
"{"
"background-image: url(:/backgrounds/kitteh);"
"background-attachment: fixed;"
@@ -751,7 +759,7 @@ void MainWindow::on_actionAddInstance_triggered()
if (MMC->accounts()->anyAccountIsValid())
{
ProgressDialog loadDialog(this);
- auto update = newInstance->doUpdate(false);
+ auto update = newInstance->doUpdate();
connect(update.get(), &Task::failed, [this](QString reason)
{
QString error = QString("Instance load failed: %1").arg(reason);
@@ -837,7 +845,7 @@ void MainWindow::on_actionChangeInstIcon_triggered()
void MainWindow::iconUpdated(QString icon)
{
- if(icon == m_currentInstIcon)
+ if (icon == m_currentInstIcon)
{
ui->actionChangeInstIcon->setIcon(MMC->icons()->getBigIcon(m_currentInstIcon));
}
@@ -860,7 +868,8 @@ void MainWindow::setSelectedInstanceById(const QString &id)
selectionIndex = proxymodel->mapFromSource(index);
}
}
- view->selectionModel()->setCurrentIndex(selectionIndex, QItemSelectionModel::ClearAndSelect);
+ view->selectionModel()->setCurrentIndex(selectionIndex,
+ QItemSelectionModel::ClearAndSelect);
}
void MainWindow::on_actionChangeInstGroup_triggered()
@@ -1060,7 +1069,16 @@ void MainWindow::on_actionLaunchInstance_triggered()
}
}
-void MainWindow::doLaunch()
+void MainWindow::on_actionLaunchInstanceOffline_triggered()
+{
+ if (m_selectedInstance)
+ {
+ NagUtils::checkJVMArgs(m_selectedInstance->settings().get("JvmArgs").toString(), this);
+ doLaunch(false);
+ }
+}
+
+void MainWindow::doLaunch(bool online)
{
if (!m_selectedInstance)
return;
@@ -1104,89 +1122,111 @@ void MainWindow::doLaunch()
if (!account.get())
return;
+ // we try empty password first :)
+ QString password;
+ // we loop until the user succeeds in logging in or gives up
+ bool tryagain = true;
+ // the failure. the default failure.
QString failReason = tr("Your account is currently not logged in. Please enter "
"your password to log in again.");
- // do the login. if the account has an access token, try to refresh it first.
- if (account->accountStatus() != NotVerified)
+
+ while (tryagain)
{
- // We'll need to validate the access token to make sure the account is still logged in.
- ProgressDialog progDialog(this);
- progDialog.setSkipButton(true, tr("Play Offline"));
- auto task = account->login();
- progDialog.exec(task.get());
-
- auto status = account->accountStatus();
- if (status != NotVerified)
- {
- updateInstance(m_selectedInstance, account);
- }
- else
+ AuthSessionPtr session(new AuthSession());
+ session->wants_online = online;
+ auto task = account->login(session, password);
+ if (task)
{
+ // We'll need to validate the access token to make sure the account
+ // is still logged in.
+ ProgressDialog progDialog(this);
+ if (online)
+ progDialog.setSkipButton(true, tr("Play Offline"));
+ progDialog.exec(task.get());
if (!task->successful())
{
failReason = task->failReason();
}
- if (loginWithPassword(account, failReason))
- updateInstance(m_selectedInstance, account);
}
- // in any case, revert from online to verified.
- account->downgrade();
- }
- else
- {
- if (loginWithPassword(account, failReason))
+ switch (session->status)
+ {
+ case AuthSession::Undetermined:
{
- updateInstance(m_selectedInstance, account);
- account->downgrade();
+ QLOG_ERROR() << "Received undetermined session status during login. Bye.";
+ tryagain = false;
+ break;
}
- // in any case, revert from online to verified.
- account->downgrade();
- }
-}
-
-bool MainWindow::loginWithPassword(MojangAccountPtr account, const QString &errorMsg)
-{
- EditAccountDialog passDialog(errorMsg, this, EditAccountDialog::PasswordField);
- if (passDialog.exec() == QDialog::Accepted)
- {
- // To refresh the token, we just create an authenticate task with the given account and
- // the user's password.
- ProgressDialog progDialog(this);
- auto task = account->login(passDialog.password());
- progDialog.exec(task.get());
- if (task->successful())
- return true;
- else
+ case AuthSession::RequiresPassword:
{
- // If the authentication task failed, recurse with the task's error message.
- return loginWithPassword(account, task->failReason());
+ EditAccountDialog passDialog(failReason, this, EditAccountDialog::PasswordField);
+ if (passDialog.exec() == QDialog::Accepted)
+ {
+ password = passDialog.password();
+ }
+ else
+ {
+ tryagain = false;
+ }
+ break;
+ }
+ case AuthSession::PlayableOffline:
+ {
+ // we ask the user for a player name
+ bool ok = false;
+ QString usedname = session->player_name;
+ QString name = QInputDialog::getText(this, tr("Player name"),
+ tr("Choose your offline mode player name."),
+ QLineEdit::Normal, session->player_name, &ok);
+ if (!ok)
+ {
+ tryagain = false;
+ break;
+ }
+ if (name.length())
+ {
+ usedname = name;
+ }
+ session->MakeOffline(usedname);
+ // offline flavored game from here :3
+ }
+ case AuthSession::PlayableOnline:
+ {
+ // update first if the server actually responded
+ if (session->auth_server_online)
+ {
+ updateInstance(m_selectedInstance, session);
+ }
+ else
+ {
+ launchInstance(m_selectedInstance, session);
+ }
+ tryagain = false;
+ }
}
}
- return false;
}
-void MainWindow::updateInstance(BaseInstance *instance, MojangAccountPtr account)
+void MainWindow::updateInstance(BaseInstance *instance, AuthSessionPtr session)
{
- bool only_prepare = account->accountStatus() != Online;
- auto updateTask = instance->doUpdate(only_prepare);
+ auto updateTask = instance->doUpdate();
if (!updateTask)
{
- launchInstance(instance, account);
+ launchInstance(instance, session);
return;
}
ProgressDialog tDialog(this);
- connect(updateTask.get(), &Task::succeeded, [this, instance, account]
- { launchInstance(instance, account); });
+ connect(updateTask.get(), &Task::succeeded, [this, instance, session]
+ { launchInstance(instance, session); });
connect(updateTask.get(), SIGNAL(failed(QString)), SLOT(onGameUpdateError(QString)));
tDialog.exec(updateTask.get());
}
-void MainWindow::launchInstance(BaseInstance *instance, MojangAccountPtr account)
+void MainWindow::launchInstance(BaseInstance *instance, AuthSessionPtr session)
{
Q_ASSERT_X(instance != NULL, "launchInstance", "instance is NULL");
- Q_ASSERT_X(account.get() != nullptr, "launchInstance", "account is NULL");
+ Q_ASSERT_X(session.get() != nullptr, "launchInstance", "session is NULL");
- proc = instance->prepareForLaunch(account);
+ proc = instance->prepareForLaunch(session);
if (!proc)
return;
@@ -1195,7 +1235,7 @@ void MainWindow::launchInstance(BaseInstance *instance, MojangAccountPtr account
console = new ConsoleWindow(proc);
connect(console, SIGNAL(isClosing()), this, SLOT(instanceEnded()));
- proc->setLogin(account);
+ proc->setLogin(session);
proc->launch();
}
@@ -1258,7 +1298,7 @@ void MainWindow::on_actionChangeInstMCVersion_triggered()
VersionSelectDialog vselect(m_selectedInstance->versionList().get(),
tr("Change Minecraft version"), this);
vselect.setFilter(1, "OneSix");
- if(!vselect.exec() || !vselect.selectedVersion())
+ if (!vselect.exec() || !vselect.selectedVersion())
return;
if (!MMC->accounts()->anyAccountIsValid())
@@ -1276,7 +1316,7 @@ void MainWindow::on_actionChangeInstMCVersion_triggered()
auto result = CustomMessageBox::selectable(
this, tr("Are you sure?"),
tr("This will remove any library/version customization you did previously. "
- "This includes things like Forge install and similar."),
+ "This includes things like Forge install and similar."),
QMessageBox::Warning, QMessageBox::Ok | QMessageBox::Abort,
QMessageBox::Abort)->exec();
@@ -1285,7 +1325,7 @@ void MainWindow::on_actionChangeInstMCVersion_triggered()
}
m_selectedInstance->setIntendedVersionId(vselect.selectedVersion()->descriptor());
- auto updateTask = m_selectedInstance->doUpdate(false);
+ auto updateTask = m_selectedInstance->doUpdate();
if (!updateTask)
{
return;
@@ -1384,7 +1424,7 @@ void MainWindow::instanceEnded()
void MainWindow::checkMigrateLegacyAssets()
{
int legacyAssets = AssetsUtils::findLegacyAssets();
- if(legacyAssets > 0)
+ if (legacyAssets > 0)
{
ProgressDialog migrateDlg(this);
AssetsMigrateTask migrateTask(legacyAssets, &migrateDlg);
diff --git a/gui/MainWindow.h b/gui/MainWindow.h
index eb478776..4d9e165d 100644
--- a/gui/MainWindow.h
+++ b/gui/MainWindow.h
@@ -27,9 +27,6 @@
class QToolButton;
class LabeledToolButton;
class QLabel;
-class InstanceProxyModel;
-class KCategorizedView;
-class KCategoryDrawer;
class MinecraftProcess;
class ConsoleWindow;
@@ -96,6 +93,8 @@ slots:
void on_actionLaunchInstance_triggered();
+ void on_actionLaunchInstanceOffline_triggered();
+
void on_actionDeleteInstance_triggered();
void on_actionRenameInstance_triggered();
@@ -112,25 +111,18 @@ slots:
* Launches the currently selected instance with the default account.
* If no default account is selected, prompts the user to pick an account.
*/
- void doLaunch();
-
- /*!
- * Opens an input dialog, allowing the user to input their password and refresh its access token.
- * This function will execute the proper Yggdrasil task to refresh the access token.
- * Returns true if successful. False if the user cancelled.
- */
- bool loginWithPassword(MojangAccountPtr account, const QString& errorMsg="");
+ void doLaunch(bool online = true);
/*!
* Launches the given instance with the given account.
* This function assumes that the given account has a valid, usable access token.
*/
- void launchInstance(BaseInstance* instance, MojangAccountPtr account);
+ void launchInstance(BaseInstance *instance, AuthSessionPtr session);
/*!
* Prepares the given instance for launch with the given account.
*/
- void updateInstance(BaseInstance* instance, MojangAccountPtr account);
+ void updateInstance(BaseInstance *instance, AuthSessionPtr account);
void onGameUpdateError(QString error);
@@ -190,8 +182,7 @@ protected:
private:
Ui::MainWindow *ui;
- KCategoryDrawer *drawer;
- KCategorizedView *view;
+ class GroupView *view;
InstanceProxyModel *proxymodel;
MinecraftProcess *proc;
ConsoleWindow *console;
diff --git a/gui/MainWindow.ui b/gui/MainWindow.ui
index 25af6f60..8cf26d18 100644
--- a/gui/MainWindow.ui
+++ b/gui/MainWindow.ui
@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
- <width>688</width>
- <height>460</height>
+ <width>694</width>
+ <height>563</height>
</rect>
</property>
<property name="windowTitle">
@@ -107,6 +107,7 @@
</attribute>
<addaction name="actionChangeInstIcon"/>
<addaction name="actionLaunchInstance"/>
+ <addaction name="actionLaunchInstanceOffline"/>
<addaction name="separator"/>
<addaction name="actionEditInstNotes"/>
<addaction name="actionChangeInstGroup"/>
@@ -152,7 +153,9 @@
</widget>
<action name="actionAddInstance">
<property name="icon">
- <iconset theme="new"/>
+ <iconset theme="new">
+ <normaloff/>
+ </iconset>
</property>
<property name="text">
<string>Add Instance</string>
@@ -166,7 +169,9 @@
</action>
<action name="actionViewInstanceFolder">
<property name="icon">
- <iconset theme="viewfolder"/>
+ <iconset theme="viewfolder">
+ <normaloff/>
+ </iconset>
</property>
<property name="text">
<string>View Instance Folder</string>
@@ -180,7 +185,9 @@
</action>
<action name="actionRefresh">
<property name="icon">
- <iconset theme="refresh"/>
+ <iconset theme="refresh">
+ <normaloff/>
+ </iconset>
</property>
<property name="text">
<string>Refresh</string>
@@ -194,7 +201,9 @@
</action>
<action name="actionViewCentralModsFolder">
<property name="icon">
- <iconset theme="centralmods"/>
+ <iconset theme="centralmods">
+ <normaloff/>
+ </iconset>
</property>
<property name="text">
<string>View Central Mods Folder</string>
@@ -208,7 +217,9 @@
</action>
<action name="actionCheckUpdate">
<property name="icon">
- <iconset theme="checkupdate"/>
+ <iconset theme="checkupdate">
+ <normaloff/>
+ </iconset>
</property>
<property name="text">
<string>Check for Updates</string>
@@ -222,7 +233,9 @@
</action>
<action name="actionSettings">
<property name="icon">
- <iconset theme="settings"/>
+ <iconset theme="settings">
+ <normaloff/>
+ </iconset>
</property>
<property name="text">
<string>Settings</string>
@@ -239,7 +252,9 @@
</action>
<action name="actionReportBug">
<property name="icon">
- <iconset theme="bug"/>
+ <iconset theme="bug">
+ <normaloff/>
+ </iconset>
</property>
<property name="text">
<string>Report a Bug</string>
@@ -253,7 +268,9 @@
</action>
<action name="actionMoreNews">
<property name="icon">
- <iconset theme="news"/>
+ <iconset theme="news">
+ <normaloff/>
+ </iconset>
</property>
<property name="text">
<string>More News</string>
@@ -270,7 +287,9 @@
</action>
<action name="actionAbout">
<property name="icon">
- <iconset theme="about"/>
+ <iconset theme="about">
+ <normaloff/>
+ </iconset>
</property>
<property name="text">
<string>About MultiMC</string>
@@ -463,7 +482,9 @@
<bool>true</bool>
</property>
<property name="icon">
- <iconset theme="cat"/>
+ <iconset theme="cat">
+ <normaloff/>
+ </iconset>
</property>
<property name="text">
<string>Meow</string>
@@ -474,7 +495,9 @@
</action>
<action name="actionCopyInstance">
<property name="icon">
- <iconset theme="copy"/>
+ <iconset theme="copy">
+ <normaloff/>
+ </iconset>
</property>
<property name="text">
<string>Copy Instance</string>
@@ -494,6 +517,17 @@
<string>Manage your Mojang or Minecraft accounts.</string>
</property>
</action>
+ <action name="actionLaunchInstanceOffline">
+ <property name="text">
+ <string>Play Offline</string>
+ </property>
+ <property name="toolTip">
+ <string>Launch the selected instance in offline mode.</string>
+ </property>
+ <property name="statusTip">
+ <string>Launch the selected instance.</string>
+ </property>
+ </action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<resources>
diff --git a/gui/dialogs/AccountListDialog.cpp b/gui/dialogs/AccountListDialog.cpp
index 1712e352..a38035a6 100644
--- a/gui/dialogs/AccountListDialog.cpp
+++ b/gui/dialogs/AccountListDialog.cpp
@@ -126,7 +126,7 @@ void AccountListDialog::addAccount(const QString& errMsg)
MojangAccountPtr account = MojangAccount::createFromUsername(username);
ProgressDialog progDialog(this);
- auto task = account->login(password);
+ auto task = account->login(nullptr, password);
progDialog.exec(task.get());
if(task->successful())
{
diff --git a/gui/dialogs/IconPickerDialog.cpp b/gui/dialogs/IconPickerDialog.cpp
index f7970b37..9b1c26ff 100644
--- a/gui/dialogs/IconPickerDialog.cpp
+++ b/gui/dialogs/IconPickerDialog.cpp
@@ -23,7 +23,7 @@
#include "ui_IconPickerDialog.h"
#include "gui/Platform.h"
-#include "gui/widgets/InstanceDelegate.h"
+#include "gui/groupview/InstanceDelegate.h"
#include "logic/icons/IconList.h"
diff --git a/gui/dialogs/OneSixModEditDialog.cpp b/gui/dialogs/OneSixModEditDialog.cpp
index 3982f17d..9e585de5 100644
--- a/gui/dialogs/OneSixModEditDialog.cpp
+++ b/gui/dialogs/OneSixModEditDialog.cpp
@@ -39,6 +39,18 @@
#include "logic/lists/ForgeVersionList.h"
#include "logic/ForgeInstaller.h"
#include "logic/LiteLoaderInstaller.h"
+#include "logic/OneSixVersionBuilder.h"
+
+template<typename A, typename B>
+QMap<A, B> invert(const QMap<B, A> &in)
+{
+ QMap<A, B> out;
+ for (auto it = in.begin(); it != in.end(); ++it)
+ {
+ out.insert(it.value(), it.key());
+ }
+ return out;
+}
OneSixModEditDialog::OneSixModEditDialog(OneSixInstance *inst, QWidget *parent)
: QDialog(parent), ui(new Ui::OneSixModEditDialog), m_inst(inst)
@@ -55,7 +67,8 @@ OneSixModEditDialog::OneSixModEditDialog(OneSixInstance *inst, QWidget *parent)
main_model->setSourceModel(m_version.get());
ui->libraryTreeView->setModel(main_model);
ui->libraryTreeView->installEventFilter(this);
- ui->mainClassEdit->setText(m_version->mainClass);
+ connect(ui->libraryTreeView->selectionModel(), &QItemSelectionModel::currentChanged,
+ this, &OneSixModEditDialog::versionCurrent);
updateVersionControls();
}
else
@@ -81,6 +94,8 @@ OneSixModEditDialog::OneSixModEditDialog(OneSixInstance *inst, QWidget *parent)
ui->resPackTreeView->installEventFilter(this);
m_resourcepacks->startWatching();
}
+
+ connect(m_inst, &OneSixInstance::versionReloaded, this, &OneSixModEditDialog::updateVersionControls);
}
OneSixModEditDialog::~OneSixModEditDialog()
@@ -92,95 +107,138 @@ OneSixModEditDialog::~OneSixModEditDialog()
void OneSixModEditDialog::updateVersionControls()
{
- bool customVersion = m_inst->versionIsCustom();
- ui->customizeBtn->setEnabled(!customVersion);
- ui->revertBtn->setEnabled(customVersion);
ui->forgeBtn->setEnabled(true);
- ui->liteloaderBtn->setEnabled(LiteLoaderInstaller(m_inst->intendedVersionId()).canApply());
- ui->customEditorBtn->setEnabled(customVersion);
+ ui->liteloaderBtn->setEnabled(LiteLoaderInstaller().canApply(m_inst));
+ ui->mainClassEdit->setText(m_version->mainClass);
}
void OneSixModEditDialog::disableVersionControls()
{
- ui->customizeBtn->setEnabled(false);
- ui->revertBtn->setEnabled(false);
ui->forgeBtn->setEnabled(false);
ui->liteloaderBtn->setEnabled(false);
- ui->customEditorBtn->setEnabled(false);
+ ui->reloadLibrariesBtn->setEnabled(false);
+ ui->removeLibraryBtn->setEnabled(false);
+ ui->mainClassEdit->setText("");
}
-void OneSixModEditDialog::on_customizeBtn_clicked()
+void OneSixModEditDialog::on_reloadLibrariesBtn_clicked()
{
- if (m_inst->customizeVersion())
- {
- m_version = m_inst->getFullVersion();
- main_model->setSourceModel(m_version.get());
- updateVersionControls();
- }
+ m_inst->reloadVersion(this);
}
-void OneSixModEditDialog::on_revertBtn_clicked()
+void OneSixModEditDialog::on_removeLibraryBtn_clicked()
{
- auto response = CustomMessageBox::selectable(
- this, tr("Revert?"), tr("Do you want to revert the "
- "version of this instance to its original configuration?"),
- QMessageBox::Question, QMessageBox::Yes | QMessageBox::No)->exec();
- if (response == QMessageBox::Yes)
+ if (ui->libraryTreeView->currentIndex().isValid())
{
- if (m_inst->revertCustomVersion())
+ if (!m_version->remove(ui->libraryTreeView->currentIndex().row()))
+ {
+ QMessageBox::critical(this, tr("Error"), tr("Couldn't remove file"));
+ }
+ else
{
- m_version = m_inst->getFullVersion();
- main_model->setSourceModel(m_version.get());
- updateVersionControls();
+ m_inst->reloadVersion(this);
}
}
}
-void OneSixModEditDialog::on_customEditorBtn_clicked()
+void OneSixModEditDialog::on_resetLibraryOrderBtn_clicked()
{
- if (m_inst->versionIsCustom())
+ QDir(m_inst->instanceRoot()).remove("order.json");
+ m_inst->reloadVersion(this);
+}
+void OneSixModEditDialog::on_moveLibraryUpBtn_clicked()
+{
+
+ QMap<QString, int> order = getExistingOrder();
+ if (order.size() < 2 || ui->libraryTreeView->selectionModel()->selectedIndexes().isEmpty())
{
- if (!MMC->openJsonEditor(m_inst->instanceRoot() + "/custom.json"))
- {
- QMessageBox::warning(this, tr("Error"), tr("Unable to open custom.json, check the settings"));
- }
+ return;
+ }
+ const int ourRow = ui->libraryTreeView->selectionModel()->selectedIndexes().first().row();
+ const QString ourId = m_version->versionFileId(ourRow);
+ const int ourOrder = order[ourId];
+ if (ourId.isNull() || ourId.startsWith("org.multimc."))
+ {
+ return;
+ }
+
+ QMap<int, QString> sortedOrder = invert(order);
+
+ QList<int> sortedOrders = sortedOrder.keys();
+ const int ourIndex = sortedOrders.indexOf(ourOrder);
+ if (ourIndex <= 0)
+ {
+ return;
+ }
+ const int ourNewOrder = sortedOrders.at(ourIndex - 1);
+ order[ourId] = ourNewOrder;
+ order[sortedOrder[sortedOrders[ourIndex - 1]]] = ourOrder;
+
+ if (!OneSixVersionBuilder::writeOverrideOrders(order, m_inst))
+ {
+ QMessageBox::critical(this, tr("Error"), tr("Couldn't save the new order"));
+ }
+ else
+ {
+ m_inst->reloadVersion(this);
+ ui->libraryTreeView->selectionModel()->select(m_version->index(ourRow - 1), QItemSelectionModel::SelectCurrent);
+ }
+}
+void OneSixModEditDialog::on_moveLibraryDownBtn_clicked()
+{
+ QMap<QString, int> order = getExistingOrder();
+ if (order.size() < 2 || ui->libraryTreeView->selectionModel()->selectedIndexes().isEmpty())
+ {
+ return;
+ }
+ const int ourRow = ui->libraryTreeView->selectionModel()->selectedIndexes().first().row();
+ const QString ourId = m_version->versionFileId(ourRow);
+ const int ourOrder = order[ourId];
+ if (ourId.isNull() || ourId.startsWith("org.multimc."))
+ {
+ return;
+ }
+
+ QMap<int, QString> sortedOrder = invert(order);
+
+ QList<int> sortedOrders = sortedOrder.keys();
+ const int ourIndex = sortedOrders.indexOf(ourOrder);
+ if ((ourIndex + 1) >= sortedOrders.size())
+ {
+ return;
+ }
+ const int ourNewOrder = sortedOrders.at(ourIndex + 1);
+ order[ourId] = ourNewOrder;
+ order[sortedOrder[sortedOrders[ourIndex + 1]]] = ourOrder;
+
+ if (!OneSixVersionBuilder::writeOverrideOrders(order, m_inst))
+ {
+ QMessageBox::critical(this, tr("Error"), tr("Couldn't save the new order"));
+ }
+ else
+ {
+ m_inst->reloadVersion(this);
+ ui->libraryTreeView->selectionModel()->select(m_version->index(ourRow + 1), QItemSelectionModel::SelectCurrent);
}
}
void OneSixModEditDialog::on_forgeBtn_clicked()
{
+ if (QDir(m_inst->instanceRoot()).exists("custom.json"))
+ {
+ if (QMessageBox::question(this, tr("Revert?"), tr("This action will remove your custom.json. Continue?")) != QMessageBox::Yes)
+ {
+ return;
+ }
+ QDir(m_inst->instanceRoot()).remove("custom.json");
+ m_inst->reloadVersion(this);
+ }
VersionSelectDialog vselect(MMC->forgelist().get(), tr("Select Forge version"), this);
vselect.setFilter(1, m_inst->currentVersionId());
+ vselect.setEmptyString(tr("No Forge versions are currently available for Minecraft ") +
+ m_inst->currentVersionId());
if (vselect.exec() && vselect.selectedVersion())
{
- if (m_inst->versionIsCustom())
- {
- auto reply = QMessageBox::question(
- this, tr("Revert?"),
- tr("This will revert any "
- "changes you did to the version up to this point. Is that "
- "OK?"),
- QMessageBox::Yes | QMessageBox::No);
- if (reply == QMessageBox::Yes)
- {
- m_inst->revertCustomVersion();
- m_inst->customizeVersion();
- {
- m_version = m_inst->getFullVersion();
- main_model->setSourceModel(m_version.get());
- updateVersionControls();
- }
- }
- else
- return;
- }
- else
- {
- m_inst->customizeVersion();
- m_version = m_inst->getFullVersion();
- main_model->setSourceModel(m_version.get());
- updateVersionControls();
- }
ForgeVersionPtr forgeVersion =
std::dynamic_pointer_cast<ForgeVersion>(vselect.selectedVersion());
if (!forgeVersion)
@@ -197,9 +255,9 @@ void OneSixModEditDialog::on_forgeBtn_clicked()
// install
QString forgePath = entry->getFullPath();
ForgeInstaller forge(forgePath, forgeVersion->universal_url);
- if (!forge.apply(m_version))
+ if (!forge.add(m_inst))
{
- // failure notice
+ QLOG_ERROR() << "Failure installing forge";
}
}
else
@@ -212,18 +270,28 @@ void OneSixModEditDialog::on_forgeBtn_clicked()
// install
QString forgePath = entry->getFullPath();
ForgeInstaller forge(forgePath, forgeVersion->universal_url);
- if (!forge.apply(m_version))
+ if (!forge.add(m_inst))
{
- // failure notice
+ QLOG_ERROR() << "Failure installing forge";
}
}
}
+ m_inst->reloadVersion(this);
}
void OneSixModEditDialog::on_liteloaderBtn_clicked()
{
- LiteLoaderInstaller liteloader(m_inst->intendedVersionId());
- if (!liteloader.canApply())
+ if (QDir(m_inst->instanceRoot()).exists("custom.json"))
+ {
+ if (QMessageBox::question(this, tr("Revert?"), tr("This action will remove your custom.json. Continue?")) != QMessageBox::Yes)
+ {
+ return;
+ }
+ QDir(m_inst->instanceRoot()).remove("custom.json");
+ m_inst->reloadVersion(this);
+ }
+ LiteLoaderInstaller liteloader;
+ if (!liteloader.canApply(m_inst))
{
QMessageBox::critical(
this, tr("LiteLoader"),
@@ -231,18 +299,15 @@ void OneSixModEditDialog::on_liteloaderBtn_clicked()
"into this version of Minecraft"));
return;
}
- if (!m_inst->versionIsCustom())
+ if (!liteloader.add(m_inst))
{
- m_inst->customizeVersion();
- m_version = m_inst->getFullVersion();
- main_model->setSourceModel(m_version.get());
- updateVersionControls();
+ QMessageBox::critical(this, tr("LiteLoader"),
+ tr("For reasons unknown, the LiteLoader installation failed. "
+ "Check your MultiMC log files for details."));
}
- if (!liteloader.apply(m_version))
+ else
{
- QMessageBox::critical(
- this, tr("LiteLoader"),
- tr("For reasons unknown, the LiteLoader installation failed. Check your MultiMC log files for details."));
+ m_inst->reloadVersion(this);
}
}
@@ -278,6 +343,35 @@ bool OneSixModEditDialog::resourcePackListFilter(QKeyEvent *keyEvent)
return QDialog::eventFilter(ui->resPackTreeView, keyEvent);
}
+QMap<QString, int> OneSixModEditDialog::getExistingOrder() const
+{
+
+ QMap<QString, int> order;
+ // default
+ {
+ for (OneSixVersion::VersionFile file : m_version->versionFiles)
+ {
+ if (file.id.startsWith("org.multimc."))
+ {
+ continue;
+ }
+ order.insert(file.id, file.order);
+ }
+ }
+ // overriden
+ {
+ QMap<QString, int> overridenOrder = OneSixVersionBuilder::readOverrideOrders(m_inst);
+ for (auto id : order.keys())
+ {
+ if (overridenOrder.contains(id))
+ {
+ order[id] = overridenOrder[id];
+ }
+ }
+ }
+ return order;
+}
+
bool OneSixModEditDialog::eventFilter(QObject *obj, QEvent *ev)
{
if (ev->type() != QEvent::KeyPress)
@@ -362,3 +456,15 @@ void OneSixModEditDialog::loaderCurrent(QModelIndex current, QModelIndex previou
Mod &m = m_mods->operator[](row);
ui->frame->updateWithMod(m);
}
+
+void OneSixModEditDialog::versionCurrent(const QModelIndex &current, const QModelIndex &previous)
+{
+ if (!current.isValid())
+ {
+ ui->removeLibraryBtn->setDisabled(true);
+ }
+ else
+ {
+ ui->removeLibraryBtn->setEnabled(m_version->canRemove(current.row()));
+ }
+}
diff --git a/gui/dialogs/OneSixModEditDialog.h b/gui/dialogs/OneSixModEditDialog.h
index 2510c59c..f44b336b 100644
--- a/gui/dialogs/OneSixModEditDialog.h
+++ b/gui/dialogs/OneSixModEditDialog.h
@@ -45,9 +45,11 @@ slots:
void on_buttonBox_rejected();
void on_forgeBtn_clicked();
void on_liteloaderBtn_clicked();
- void on_customizeBtn_clicked();
- void on_revertBtn_clicked();
- void on_customEditorBtn_clicked();
+ void on_reloadLibrariesBtn_clicked();
+ void on_removeLibraryBtn_clicked();
+ void on_resetLibraryOrderBtn_clicked();
+ void on_moveLibraryUpBtn_clicked();
+ void on_moveLibraryDownBtn_clicked();
void updateVersionControls();
void disableVersionControls();
@@ -63,7 +65,11 @@ private:
std::shared_ptr<ModList> m_resourcepacks;
EnabledItemFilter *main_model;
OneSixInstance *m_inst;
+
+ QMap<QString, int> getExistingOrder() const;
+
public
slots:
void loaderCurrent(QModelIndex current, QModelIndex previous);
+ void versionCurrent(const QModelIndex &current, const QModelIndex &previous);
};
diff --git a/gui/dialogs/OneSixModEditDialog.ui b/gui/dialogs/OneSixModEditDialog.ui
index 899e0cbf..eaf8f7fd 100644
--- a/gui/dialogs/OneSixModEditDialog.ui
+++ b/gui/dialogs/OneSixModEditDialog.ui
@@ -26,7 +26,7 @@
</size>
</property>
<property name="currentIndex">
- <number>1</number>
+ <number>0</number>
</property>
<widget class="QWidget" name="libTab">
<attribute name="title">
@@ -43,6 +43,9 @@
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
+ <attribute name="headerVisible">
+ <bool>false</bool>
+ </attribute>
</widget>
</item>
<item>
@@ -85,61 +88,30 @@
</widget>
</item>
<item>
- <widget class="QPushButton" name="customizeBtn">
- <property name="toolTip">
- <string>Create an customized copy of the base version</string>
- </property>
- <property name="text">
- <string>Customize</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="revertBtn">
- <property name="enabled">
- <bool>false</bool>
- </property>
- <property name="toolTip">
- <string>Revert to original base version</string>
- </property>
- <property name="text">
- <string>Revert</string>
- </property>
- </widget>
- </item>
- <item>
<widget class="Line" name="line">
- <property name="frameShadow">
- <enum>QFrame::Sunken</enum>
- </property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
- <widget class="QPushButton" name="addLibraryBtn">
- <property name="enabled">
- <bool>false</bool>
- </property>
- <property name="toolTip">
- <string>Add new libraries</string>
- </property>
+ <widget class="QPushButton" name="reloadLibrariesBtn">
<property name="text">
- <string>&amp;Add</string>
+ <string>Reload</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="removeLibraryBtn">
- <property name="enabled">
- <bool>false</bool>
- </property>
- <property name="toolTip">
- <string>Remove selected libraries</string>
+ <property name="text">
+ <string>Remove</string>
</property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="resetLibraryOrderBtn">
<property name="text">
- <string>&amp;Remove</string>
+ <string>Reset order</string>
</property>
</widget>
</item>
@@ -151,9 +123,16 @@
</widget>
</item>
<item>
- <widget class="QPushButton" name="customEditorBtn">
+ <widget class="QPushButton" name="moveLibraryUpBtn">
+ <property name="text">
+ <string>Move up</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="moveLibraryDownBtn">
<property name="text">
- <string>Open custom.json</string>
+ <string>Move down</string>
</property>
</widget>
</item>
diff --git a/gui/dialogs/VersionSelectDialog.cpp b/gui/dialogs/VersionSelectDialog.cpp
index d6efe3c0..0f379f56 100644
--- a/gui/dialogs/VersionSelectDialog.cpp
+++ b/gui/dialogs/VersionSelectDialog.cpp
@@ -51,6 +51,11 @@ VersionSelectDialog::VersionSelectDialog(BaseVersionList *vlist, QString title,
}
}
+void VersionSelectDialog::setEmptyString(QString emptyString)
+{
+ ui->listView->setEmptyString(emptyString);
+}
+
VersionSelectDialog::~VersionSelectDialog()
{
delete ui;
diff --git a/gui/dialogs/VersionSelectDialog.h b/gui/dialogs/VersionSelectDialog.h
index e36341db..61fa8ab6 100644
--- a/gui/dialogs/VersionSelectDialog.h
+++ b/gui/dialogs/VersionSelectDialog.h
@@ -44,6 +44,7 @@ public:
BaseVersionPtr selectedVersion() const;
void setFilter(int column, QString filter);
+ void setEmptyString(QString emptyString);
void setResizeOn(int column);
private
diff --git a/gui/dialogs/VersionSelectDialog.ui b/gui/dialogs/VersionSelectDialog.ui
index 58264f24..07e9e73e 100644
--- a/gui/dialogs/VersionSelectDialog.ui
+++ b/gui/dialogs/VersionSelectDialog.ui
@@ -15,7 +15,7 @@
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
- <widget class="QTreeView" name="listView">
+ <widget class="VersionListView" name="listView">
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
@@ -65,6 +65,13 @@
</item>
</layout>
</widget>
+ <customwidgets>
+ <customwidget>
+ <class>VersionListView</class>
+ <extends>QTreeView</extends>
+ <header>gui/widgets/VersionListView.h</header>
+ </customwidget>
+ </customwidgets>
<resources/>
<connections>
<connection>
diff --git a/gui/groupview/Group.cpp b/gui/groupview/Group.cpp
new file mode 100644
index 00000000..51aa6658
--- /dev/null
+++ b/gui/groupview/Group.cpp
@@ -0,0 +1,269 @@
+#include "Group.h"
+
+#include <QModelIndex>
+#include <QPainter>
+#include <QtMath>
+#include <QApplication>
+
+#include "GroupView.h"
+
+Group::Group(const QString &text, GroupView *view) : view(view), text(text), collapsed(false)
+{
+}
+
+Group::Group(const Group *other)
+ : view(other->view), text(other->text), collapsed(other->collapsed)
+{
+}
+
+void Group::update()
+{
+ firstItemIndex = firstItem().row();
+
+ rowHeights = QVector<int>(numRows());
+ for (int i = 0; i < numRows(); ++i)
+ {
+ rowHeights[i] = view->categoryRowHeight(
+ view->model()->index(i * view->itemsPerRow() + firstItemIndex, 0));
+ }
+}
+
+Group::HitResults Group::hitScan(const QPoint &pos) const
+{
+ Group::HitResults results = Group::NoHit;
+ int y_start = verticalPosition();
+ int body_start = y_start + headerHeight();
+ int body_end = body_start + contentHeight() + 5; // FIXME: wtf is this 5?
+ int y = pos.y();
+ // int x = pos.x();
+ if (y < y_start)
+ {
+ results = Group::NoHit;
+ }
+ else if (y < body_start)
+ {
+ results = Group::HeaderHit;
+ int collapseSize = headerHeight() - 4;
+
+ // the icon
+ QRect iconRect = QRect(view->m_leftMargin + 2, 2 + y_start, collapseSize, collapseSize);
+ if (iconRect.contains(pos))
+ {
+ results |= Group::CheckboxHit;
+ }
+ }
+ else if (y < body_end)
+ {
+ results |= Group::BodyHit;
+ }
+ return results;
+}
+
+void Group::drawHeader(QPainter *painter, const QStyleOptionViewItem &option)
+{
+ painter->setRenderHint(QPainter::Antialiasing);
+
+ const QRect optRect = option.rect;
+ QFont font(QApplication::font());
+ font.setBold(true);
+ const QFontMetrics fontMetrics = QFontMetrics(font);
+
+ QColor outlineColor = option.palette.text().color();
+ outlineColor.setAlphaF(0.35);
+
+ //BEGIN: top left corner
+ {
+ painter->save();
+ painter->setPen(outlineColor);
+ const QPointF topLeft(optRect.topLeft());
+ QRectF arc(topLeft, QSizeF(4, 4));
+ arc.translate(0.5, 0.5);
+ painter->drawArc(arc, 1440, 1440);
+ painter->restore();
+ }
+ //END: top left corner
+
+ //BEGIN: left vertical line
+ {
+ QPoint start(optRect.topLeft());
+ start.ry() += 3;
+ QPoint verticalGradBottom(optRect.topLeft());
+ verticalGradBottom.ry() += fontMetrics.height() + 5;
+ QLinearGradient gradient(start, verticalGradBottom);
+ gradient.setColorAt(0, outlineColor);
+ gradient.setColorAt(1, Qt::transparent);
+ painter->fillRect(QRect(start, QSize(1, fontMetrics.height() + 5)), gradient);
+ }
+ //END: left vertical line
+
+ //BEGIN: horizontal line
+ {
+ QPoint start(optRect.topLeft());
+ start.rx() += 3;
+ QPoint horizontalGradTop(optRect.topLeft());
+ horizontalGradTop.rx() += optRect.width() - 6;
+ painter->fillRect(QRect(start, QSize(optRect.width() - 6, 1)), outlineColor);
+ }
+ //END: horizontal line
+
+ //BEGIN: top right corner
+ {
+ painter->save();
+ painter->setPen(outlineColor);
+ QPointF topRight(optRect.topRight());
+ topRight.rx() -= 4;
+ QRectF arc(topRight, QSizeF(4, 4));
+ arc.translate(0.5, 0.5);
+ painter->drawArc(arc, 0, 1440);
+ painter->restore();
+ }
+ //END: top right corner
+
+ //BEGIN: right vertical line
+ {
+ QPoint start(optRect.topRight());
+ start.ry() += 3;
+ QPoint verticalGradBottom(optRect.topRight());
+ verticalGradBottom.ry() += fontMetrics.height() + 5;
+ QLinearGradient gradient(start, verticalGradBottom);
+ gradient.setColorAt(0, outlineColor);
+ gradient.setColorAt(1, Qt::transparent);
+ painter->fillRect(QRect(start, QSize(1, fontMetrics.height() + 5)), gradient);
+ }
+ //END: right vertical line
+
+ //BEGIN: checkboxy thing
+ {
+ painter->save();
+ painter->setRenderHint(QPainter::Antialiasing, false);
+ painter->setFont(font);
+ QColor penColor(option.palette.text().color());
+ penColor.setAlphaF(0.6);
+ painter->setPen(penColor);
+ QRect iconSubRect(option.rect);
+ iconSubRect.setTop(iconSubRect.top() + 7);
+ iconSubRect.setLeft(iconSubRect.left() + 7);
+
+ int sizing = fontMetrics.height();
+ int even = ( (sizing - 1) % 2 );
+
+ iconSubRect.setHeight(sizing - even);
+ iconSubRect.setWidth(sizing - even);
+ painter->drawRect(iconSubRect);
+
+
+ /*
+ if(collapsed)
+ painter->drawText(iconSubRect, Qt::AlignHCenter | Qt::AlignVCenter, "+");
+ else
+ painter->drawText(iconSubRect, Qt::AlignHCenter | Qt::AlignVCenter, "-");
+ */
+ painter->setBrush(option.palette.text());
+ painter->fillRect(iconSubRect.x(), iconSubRect.y() + iconSubRect.height() / 2,
+ iconSubRect.width(), 2, penColor);
+ if (collapsed)
+ {
+ painter->fillRect(iconSubRect.x() + iconSubRect.width() / 2, iconSubRect.y(), 2,
+ iconSubRect.height(), penColor);
+ }
+
+ painter->restore();
+ }
+ //END: checkboxy thing
+
+ //BEGIN: text
+ {
+ QRect textRect(option.rect);
+ textRect.setTop(textRect.top() + 7);
+ textRect.setLeft(textRect.left() + 7 + fontMetrics.height() + 7);
+ textRect.setHeight(fontMetrics.height());
+ textRect.setRight(textRect.right() - 7);
+
+ painter->save();
+ painter->setFont(font);
+ QColor penColor(option.palette.text().color());
+ penColor.setAlphaF(0.6);
+ painter->setPen(penColor);
+ painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, text);
+ painter->restore();
+ }
+ //END: text
+}
+
+int Group::totalHeight() const
+{
+ return headerHeight() + 5 + contentHeight(); // FIXME: wtf is that '5'?
+}
+
+int Group::headerHeight() const
+{
+ QFont font(QApplication::font());
+ font.setBold(true);
+ QFontMetrics fontMetrics(font);
+
+ const int height = fontMetrics.height() + 1 /* 1 pixel-width gradient */
+ + 11 /* top and bottom separation */;
+ return height;
+ /*
+ int raw = view->viewport()->fontMetrics().height() + 4;
+ // add english. maybe. depends on font height.
+ if (raw % 2 == 0)
+ raw++;
+ return std::min(raw, 25);
+ */
+}
+
+int Group::contentHeight() const
+{
+ if (collapsed)
+ {
+ return 0;
+ }
+ int result = 0;
+ for (int i = 0; i < rowHeights.size(); ++i)
+ {
+ result += rowHeights[i];
+ }
+ return result;
+}
+
+int Group::numRows() const
+{
+ return qMax(1, qCeil((qreal)numItems() / (qreal)view->itemsPerRow()));
+}
+
+int Group::verticalPosition() const
+{
+ return m_verticalPosition;
+}
+
+QList<QModelIndex> Group::items() const
+{
+ QList<QModelIndex> indices;
+ for (int i = 0; i < view->model()->rowCount(); ++i)
+ {
+ const QModelIndex index = view->model()->index(i, 0);
+ if (index.data(GroupViewRoles::GroupRole).toString() == text)
+ {
+ indices.append(index);
+ }
+ }
+ return indices;
+}
+
+int Group::numItems() const
+{
+ return items().size();
+}
+
+QModelIndex Group::firstItem() const
+{
+ QList<QModelIndex> indices = items();
+ return indices.isEmpty() ? QModelIndex() : indices.first();
+}
+
+QModelIndex Group::lastItem() const
+{
+ QList<QModelIndex> indices = items();
+ return indices.isEmpty() ? QModelIndex() : indices.last();
+}
diff --git a/gui/groupview/Group.h b/gui/groupview/Group.h
new file mode 100644
index 00000000..3b797f4c
--- /dev/null
+++ b/gui/groupview/Group.h
@@ -0,0 +1,69 @@
+#pragma once
+
+#include <QString>
+#include <QRect>
+#include <QVector>
+#include <QStyleOption>
+
+class GroupView;
+class QPainter;
+class QModelIndex;
+
+struct Group
+{
+/* constructors */
+ Group(const QString &text, GroupView *view);
+ Group(const Group *other);
+
+/* data */
+ GroupView *view = nullptr;
+ QString text;
+ bool collapsed = false;
+ QVector<int> rowHeights;
+ int firstItemIndex = 0;
+ int m_verticalPosition = 0;
+
+/* logic */
+ /// do stuff. and things. TODO: redo.
+ void update();
+
+ /// draw the header at y-position.
+ void drawHeader(QPainter *painter, const QStyleOptionViewItem &option);
+
+ /// height of the group, in total. includes a small bit of padding.
+ int totalHeight() const;
+
+ /// height of the group header, in pixels
+ int headerHeight() const;
+
+ /// height of the group content, in pixels
+ int contentHeight() const;
+
+ /// the number of visual rows this group has
+ int numRows() const;
+
+ /// the height at which this group starts, in pixels
+ int verticalPosition() const;
+
+ enum HitResult
+ {
+ NoHit = 0x0,
+ TextHit = 0x1,
+ CheckboxHit = 0x2,
+ HeaderHit = 0x4,
+ BodyHit = 0x8
+ };
+ Q_DECLARE_FLAGS(HitResults, HitResult)
+
+ /// shoot! BANG! what did we hit?
+ HitResults hitScan (const QPoint &pos) const;
+
+ /// super derpy thing.
+ QList<QModelIndex> items() const;
+ /// I don't even
+ int numItems() const;
+ QModelIndex firstItem() const;
+ QModelIndex lastItem() const;
+};
+
+Q_DECLARE_OPERATORS_FOR_FLAGS(Group::HitResults)
diff --git a/gui/groupview/GroupView.cpp b/gui/groupview/GroupView.cpp
new file mode 100644
index 00000000..25042d02
--- /dev/null
+++ b/gui/groupview/GroupView.cpp
@@ -0,0 +1,931 @@
+#include "GroupView.h"
+
+#include <QPainter>
+#include <QApplication>
+#include <QtMath>
+#include <QDebug>
+#include <QMouseEvent>
+#include <QListView>
+#include <QPersistentModelIndex>
+#include <QDrag>
+#include <QMimeData>
+#include <QScrollBar>
+
+#include "Group.h"
+
+template <typename T> bool listsIntersect(const QList<T> &l1, const QList<T> t2)
+{
+ for (auto &item : l1)
+ {
+ if (t2.contains(item))
+ {
+ return true;
+ }
+ }
+ return false;
+}
+
+GroupView::GroupView(QWidget *parent)
+ : QAbstractItemView(parent), m_leftMargin(5), m_rightMargin(5), m_bottomMargin(5),
+ m_categoryMargin(5) //, m_updatesDisabled(false), m_categoryEditor(0), m_editedCategory(0)
+{
+ // setViewMode(IconMode);
+ // setMovement(Snap);
+ setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
+ // setWordWrap(true);
+ // setDragDropMode(QListView::InternalMove);
+ setAcceptDrops(true);
+ m_spacing = 5;
+}
+
+GroupView::~GroupView()
+{
+ qDeleteAll(m_groups);
+ m_groups.clear();
+}
+
+void GroupView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight,
+ const QVector<int> &roles)
+{
+ scheduleDelayedItemsLayout();
+}
+void GroupView::rowsInserted(const QModelIndex &parent, int start, int end)
+{
+ scheduleDelayedItemsLayout();
+}
+
+void GroupView::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end)
+{
+ scheduleDelayedItemsLayout();
+}
+
+void GroupView::updateGeometries()
+{
+ int previousScroll = verticalScrollBar()->value();
+
+ QMap<QString, Group *> cats;
+
+ for (int i = 0; i < model()->rowCount(); ++i)
+ {
+ const QString groupName =
+ model()->index(i, 0).data(GroupViewRoles::GroupRole).toString();
+ if (!cats.contains(groupName))
+ {
+ Group *old = this->category(groupName);
+ if (old)
+ {
+ cats.insert(groupName, new Group(old));
+ }
+ else
+ {
+ cats.insert(groupName, new Group(groupName, this));
+ }
+ }
+ }
+
+ /*if (m_editedCategory)
+ {
+ m_editedCategory = cats[m_editedCategory->text];
+ }*/
+
+ qDeleteAll(m_groups);
+ m_groups = cats.values();
+
+ for (auto cat : m_groups)
+ {
+ cat->update();
+ }
+
+ if (m_groups.isEmpty())
+ {
+ verticalScrollBar()->setRange(0, 0);
+ }
+ else
+ {
+ int totalHeight = 0;
+ // top margin
+ totalHeight += m_categoryMargin;
+ int itemScroll = 0;
+ for (auto category : m_groups)
+ {
+ category->m_verticalPosition = totalHeight;
+ totalHeight += category->totalHeight() + m_categoryMargin;
+ if(!itemScroll && category->totalHeight() != 0)
+ {
+ itemScroll = category->contentHeight() / category->numRows();
+ }
+ }
+ // do not divide by zero
+ if(itemScroll == 0)
+ itemScroll = 64;
+
+ totalHeight += m_bottomMargin;
+ verticalScrollBar()->setSingleStep ( itemScroll );
+ const int rowsPerPage = qMax ( viewport()->height() / itemScroll, 1 );
+ verticalScrollBar()->setPageStep ( rowsPerPage * itemScroll );
+
+ verticalScrollBar()->setRange(0, totalHeight - height());
+ }
+
+ verticalScrollBar()->setValue(qMin(previousScroll, verticalScrollBar()->maximum()));
+
+ viewport()->update();
+}
+
+bool GroupView::isIndexHidden(const QModelIndex &index) const
+{
+ Group *cat = category(index);
+ if (cat)
+ {
+ return cat->collapsed;
+ }
+ else
+ {
+ return false;
+ }
+}
+
+Group *GroupView::category(const QModelIndex &index) const
+{
+ return category(index.data(GroupViewRoles::GroupRole).toString());
+}
+
+Group *GroupView::category(const QString &cat) const
+{
+ for (auto group : m_groups)
+ {
+ if (group->text == cat)
+ {
+ return group;
+ }
+ }
+ return nullptr;
+}
+
+Group *GroupView::categoryAt(const QPoint &pos) const
+{
+ for (auto group : m_groups)
+ {
+ if(group->hitScan(pos) & Group::CheckboxHit)
+ {
+ return group;
+ }
+ }
+ return nullptr;
+}
+
+int GroupView::itemsPerRow() const
+{
+ return qFloor((qreal)(contentWidth()) / (qreal)(itemWidth() + m_spacing));
+}
+
+int GroupView::contentWidth() const
+{
+ return width() - m_leftMargin - m_rightMargin;
+}
+
+int GroupView::itemWidth() const
+{
+ return itemDelegate()
+ ->sizeHint(viewOptions(), model()->index(model()->rowCount() - 1, 0))
+ .width();
+}
+
+int GroupView::categoryRowHeight(const QModelIndex &index) const
+{
+ QModelIndexList indices;
+ int internalRow = categoryInternalPosition(index).second;
+ for (auto &i : category(index)->items())
+ {
+ if (categoryInternalPosition(i).second == internalRow)
+ {
+ indices.append(i);
+ }
+ }
+
+ int largestHeight = 0;
+ for (auto &i : indices)
+ {
+ largestHeight =
+ qMax(largestHeight, itemDelegate()->sizeHint(viewOptions(), i).height());
+ }
+ return largestHeight + m_spacing;
+}
+
+QPair<int, int> GroupView::categoryInternalPosition(const QModelIndex &index) const
+{
+ QList<QModelIndex> indices = category(index)->items();
+ int x = 0;
+ int y = 0;
+ const int perRow = itemsPerRow();
+ for (int i = 0; i < indices.size(); ++i)
+ {
+ if (indices.at(i) == index)
+ {
+ break;
+ }
+ ++x;
+ if (x == perRow)
+ {
+ x = 0;
+ ++y;
+ }
+ }
+ return qMakePair(x, y);
+}
+
+int GroupView::categoryInternalRowTop(const QModelIndex &index) const
+{
+ Group *cat = category(index);
+ int categoryInternalRow = categoryInternalPosition(index).second;
+ int result = 0;
+ for (int i = 0; i < categoryInternalRow; ++i)
+ {
+ result += cat->rowHeights.at(i);
+ }
+ return result;
+}
+
+int GroupView::itemHeightForCategoryRow(const Group *category, const int internalRow) const
+{
+ for (auto &i : category->items())
+ {
+ QPair<int, int> pos = categoryInternalPosition(i);
+ if (pos.second == internalRow)
+ {
+ return categoryRowHeight(i);
+ }
+ }
+ return -1;
+}
+
+void GroupView::mousePressEvent(QMouseEvent *event)
+{
+ // endCategoryEditor();
+
+ QPoint visualPos = event->pos();
+ QPoint geometryPos = event->pos() + offset();
+
+ QPersistentModelIndex index = indexAt(visualPos);
+
+ m_pressedIndex = index;
+ m_pressedAlreadySelected = selectionModel()->isSelected(m_pressedIndex);
+ QItemSelectionModel::SelectionFlags selection_flags = selectionCommand(index, event);
+ m_pressedPosition = geometryPos;
+
+ m_pressedCategory = categoryAt(geometryPos);
+ if (m_pressedCategory)
+ {
+ setState(m_pressedCategory->collapsed ? ExpandingState : CollapsingState);
+ event->accept();
+ return;
+ }
+
+ if (index.isValid() && (index.flags() & Qt::ItemIsEnabled))
+ {
+ // we disable scrollTo for mouse press so the item doesn't change position
+ // when the user is interacting with it (ie. clicking on it)
+ bool autoScroll = hasAutoScroll();
+ setAutoScroll(false);
+ selectionModel()->setCurrentIndex(index, QItemSelectionModel::NoUpdate);
+ setAutoScroll(autoScroll);
+ QRect rect(geometryPos, geometryPos);
+ setSelection(rect, QItemSelectionModel::ClearAndSelect);
+
+ // signal handlers may change the model
+ emit pressed(index);
+ }
+ else
+ {
+ // Forces a finalize() even if mouse is pressed, but not on a item
+ selectionModel()->select(QModelIndex(), QItemSelectionModel::Select);
+ }
+}
+
+void GroupView::mouseMoveEvent(QMouseEvent *event)
+{
+ QPoint topLeft;
+ QPoint visualPos = event->pos();
+ QPoint geometryPos = event->pos() + offset();
+
+ if (state() == ExpandingState || state() == CollapsingState)
+ {
+ return;
+ }
+
+ if (state() == DraggingState)
+ {
+ topLeft = m_pressedPosition - offset();
+ if ((topLeft - event->pos()).manhattanLength() > QApplication::startDragDistance())
+ {
+ m_pressedIndex = QModelIndex();
+ startDrag(model()->supportedDragActions());
+ setState(NoState);
+ stopAutoScroll();
+ }
+ return;
+ }
+
+ if (selectionMode() != SingleSelection)
+ {
+ topLeft = m_pressedPosition - offset();
+ }
+ else
+ {
+ topLeft = geometryPos;
+ }
+
+ if (m_pressedIndex.isValid() && (state() != DragSelectingState) &&
+ (event->buttons() != Qt::NoButton) && !selectedIndexes().isEmpty())
+ {
+ setState(DraggingState);
+ return;
+ }
+
+ if ((event->buttons() & Qt::LeftButton) && selectionModel())
+ {
+ setState(DragSelectingState);
+
+ setSelection(QRect(geometryPos, geometryPos), QItemSelectionModel::ClearAndSelect);
+ QModelIndex index = indexAt(visualPos);
+
+ // set at the end because it might scroll the view
+ if (index.isValid() && (index != selectionModel()->currentIndex()) &&
+ (index.flags() & Qt::ItemIsEnabled))
+ {
+ selectionModel()->setCurrentIndex(index, QItemSelectionModel::NoUpdate);
+ }
+ }
+}
+
+void GroupView::mouseReleaseEvent(QMouseEvent *event)
+{
+ QPoint visualPos = event->pos();
+ QPoint geometryPos = event->pos() + offset();
+ QPersistentModelIndex index = indexAt(visualPos);
+
+ bool click = (index == m_pressedIndex && index.isValid()) ||
+ (m_pressedCategory && m_pressedCategory == categoryAt(geometryPos));
+
+ if (click && m_pressedCategory)
+ {
+ if (state() == ExpandingState)
+ {
+ m_pressedCategory->collapsed = false;
+ updateGeometries();
+ viewport()->update();
+ event->accept();
+ return;
+ }
+ else if (state() == CollapsingState)
+ {
+ m_pressedCategory->collapsed = true;
+ updateGeometries();
+ viewport()->update();
+ event->accept();
+ return;
+ }
+ }
+
+ m_ctrlDragSelectionFlag = QItemSelectionModel::NoUpdate;
+
+ setState(NoState);
+
+ if (click)
+ {
+ if (event->button() == Qt::LeftButton)
+ {
+ emit clicked(index);
+ }
+ QStyleOptionViewItem option = viewOptions();
+ if (m_pressedAlreadySelected)
+ {
+ option.state |= QStyle::State_Selected;
+ }
+ if ((model()->flags(index) & Qt::ItemIsEnabled) &&
+ style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, &option, this))
+ {
+ emit activated(index);
+ }
+ }
+}
+
+void GroupView::mouseDoubleClickEvent(QMouseEvent *event)
+{
+ QModelIndex index = indexAt(event->pos());
+ if (!index.isValid() || !(index.flags() & Qt::ItemIsEnabled) || (m_pressedIndex != index))
+ {
+ QMouseEvent me(QEvent::MouseButtonPress, event->localPos(), event->windowPos(),
+ event->screenPos(), event->button(), event->buttons(),
+ event->modifiers());
+ mousePressEvent(&me);
+ return;
+ }
+ // signal handlers may change the model
+ QPersistentModelIndex persistent = index;
+ emit doubleClicked(persistent);
+}
+
+void GroupView::paintEvent(QPaintEvent *event)
+{
+ QPainter painter(this->viewport());
+
+ QStyleOptionViewItemV4 option(viewOptions());
+ option.widget = this;
+
+ int wpWidth = viewport()->width();
+ option.rect.setWidth(wpWidth);
+ for (int i = 0; i < m_groups.size(); ++i)
+ {
+ Group *category = m_groups.at(i);
+ int y = category->verticalPosition();
+ y -= verticalOffset();
+ QRect backup = option.rect;
+ int height = category->totalHeight();
+ option.rect.setTop(y);
+ option.rect.setHeight(height);
+ option.rect.setLeft(m_leftMargin);
+ option.rect.setRight(wpWidth - m_rightMargin);
+ category->drawHeader(&painter, option);
+ y += category->totalHeight() + m_categoryMargin;
+ option.rect = backup;
+ }
+
+ for (int i = 0; i < model()->rowCount(); ++i)
+ {
+ const QModelIndex index = model()->index(i, 0);
+ if (isIndexHidden(index))
+ {
+ continue;
+ }
+ Qt::ItemFlags flags = index.flags();
+ option.rect = visualRect(index);
+ option.features |=
+ QStyleOptionViewItemV2::WrapText; // FIXME: what is the meaning of this anyway?
+ if (flags & Qt::ItemIsSelectable && selectionModel()->isSelected(index))
+ {
+ option.state |= selectionModel()->isSelected(index) ? QStyle::State_Selected
+ : QStyle::State_None;
+ }
+ else
+ {
+ option.state &= ~QStyle::State_Selected;
+ }
+ option.state |= (index == currentIndex()) ? QStyle::State_HasFocus : QStyle::State_None;
+ if (!(flags & Qt::ItemIsEnabled))
+ {
+ option.state &= ~QStyle::State_Enabled;
+ }
+ itemDelegate()->paint(&painter, option, index);
+ }
+
+ /*
+ * Drop indicators for manual reordering...
+ */
+#if 0
+ if (!m_lastDragPosition.isNull())
+ {
+ QPair<Group *, int> pair = rowDropPos(m_lastDragPosition);
+ Group *category = pair.first;
+ int row = pair.second;
+ if (category)
+ {
+ int internalRow = row - category->firstItemIndex;
+ QLine line;
+ if (internalRow >= category->numItems())
+ {
+ QRect toTheRightOfRect = visualRect(category->lastItem());
+ line = QLine(toTheRightOfRect.topRight(), toTheRightOfRect.bottomRight());
+ }
+ else
+ {
+ QRect toTheLeftOfRect = visualRect(model()->index(row, 0));
+ line = QLine(toTheLeftOfRect.topLeft(), toTheLeftOfRect.bottomLeft());
+ }
+ painter.save();
+ painter.setPen(QPen(Qt::black, 3));
+ painter.drawLine(line);
+ painter.restore();
+ }
+ }
+#endif
+}
+
+void GroupView::resizeEvent(QResizeEvent *event)
+{
+ // QListView::resizeEvent(event);
+
+ // if (m_categoryEditor)
+ // {
+ // m_categoryEditor->resize(qMax(contentWidth() / 2,
+ // m_editedCategory->textRect.width()),
+ // m_categoryEditor->height());
+ // }
+
+ updateGeometries();
+}
+
+void GroupView::dragEnterEvent(QDragEnterEvent *event)
+{
+ if (!isDragEventAccepted(event))
+ {
+ return;
+ }
+ m_lastDragPosition = event->pos() + offset();
+ viewport()->update();
+ event->accept();
+}
+
+void GroupView::dragMoveEvent(QDragMoveEvent *event)
+{
+ if (!isDragEventAccepted(event))
+ {
+ return;
+ }
+ m_lastDragPosition = event->pos() + offset();
+ viewport()->update();
+ event->accept();
+}
+
+void GroupView::dragLeaveEvent(QDragLeaveEvent *event)
+{
+ m_lastDragPosition = QPoint();
+ viewport()->update();
+}
+
+void GroupView::dropEvent(QDropEvent *event)
+{
+ m_lastDragPosition = QPoint();
+
+ stopAutoScroll();
+ setState(NoState);
+
+ if (event->source() != this || !(event->possibleActions() & Qt::MoveAction))
+ {
+ return;
+ }
+
+ QPair<Group *, int> dropPos = rowDropPos(event->pos() + offset());
+ const Group *category = dropPos.first;
+ const int row = dropPos.second;
+
+ if (row == -1)
+ {
+ viewport()->update();
+ return;
+ }
+
+ const QString categoryText = category->text;
+ if (model()->dropMimeData(event->mimeData(), Qt::MoveAction, row, 0, QModelIndex()))
+ {
+ model()->setData(model()->index(row, 0), categoryText,
+ GroupViewRoles::GroupRole);
+ event->setDropAction(Qt::MoveAction);
+ event->accept();
+ }
+ updateGeometries();
+ viewport()->update();
+}
+
+void GroupView::startDrag(Qt::DropActions supportedActions)
+{
+ QModelIndexList indexes = selectionModel()->selectedIndexes();
+ if (indexes.count() > 0)
+ {
+ QMimeData *data = model()->mimeData(indexes);
+ if (!data)
+ {
+ return;
+ }
+ QRect rect;
+ QPixmap pixmap = renderToPixmap(indexes, &rect);
+ //rect.translate(offset());
+ // rect.adjust(horizontalOffset(), verticalOffset(), 0, 0);
+ QDrag *drag = new QDrag(this);
+ drag->setPixmap(pixmap);
+ drag->setMimeData(data);
+ Qt::DropAction defaultDropAction = Qt::IgnoreAction;
+ if (this->defaultDropAction() != Qt::IgnoreAction &&
+ (supportedActions & this->defaultDropAction()))
+ {
+ defaultDropAction = this->defaultDropAction();
+ }
+ if (drag->exec(supportedActions, defaultDropAction) == Qt::MoveAction)
+ {
+ const QItemSelection selection = selectionModel()->selection();
+
+ for (auto it = selection.constBegin(); it != selection.constEnd(); ++it)
+ {
+ QModelIndex parent = (*it).parent();
+ if ((*it).left() != 0)
+ {
+ continue;
+ }
+ if ((*it).right() != (model()->columnCount(parent) - 1))
+ {
+ continue;
+ }
+ int count = (*it).bottom() - (*it).top() + 1;
+ model()->removeRows((*it).top(), count, parent);
+ }
+ }
+ }
+}
+
+QRect GroupView::visualRect(const QModelIndex &index) const
+{
+ return geometryRect(index).translated(-offset());
+}
+
+QRect GroupView::geometryRect(const QModelIndex &index) const
+{
+ if (!index.isValid() || isIndexHidden(index) || index.column() > 0)
+ {
+ return QRect();
+ }
+
+ const Group *cat = category(index);
+ QPair<int, int> pos = categoryInternalPosition(index);
+ int x = pos.first;
+ // int y = pos.second;
+
+ QRect out;
+ out.setTop(cat->verticalPosition() + cat->headerHeight() + 5 + categoryInternalRowTop(index));
+ out.setLeft(m_spacing + x * (itemWidth() + m_spacing));
+ out.setSize(itemDelegate()->sizeHint(viewOptions(), index));
+
+ return out;
+}
+
+/*
+void CategorizedView::startCategoryEditor(Category *category)
+{
+ if (m_categoryEditor != 0)
+ {
+ return;
+ }
+ m_editedCategory = category;
+ m_categoryEditor = new QLineEdit(m_editedCategory->text, this);
+ QRect rect = m_editedCategory->textRect;
+ rect.setWidth(qMax(contentWidth() / 2, rect.width()));
+ m_categoryEditor->setGeometry(rect);
+ m_categoryEditor->show();
+ m_categoryEditor->setFocus();
+ connect(m_categoryEditor, &QLineEdit::returnPressed, this,
+&CategorizedView::endCategoryEditor);
+}
+
+void CategorizedView::endCategoryEditor()
+{
+ if (m_categoryEditor == 0)
+ {
+ return;
+ }
+ m_editedCategory->text = m_categoryEditor->text();
+ m_updatesDisabled = true;
+ foreach (const QModelIndex &index, itemsForCategory(m_editedCategory))
+ {
+ const_cast<QAbstractItemModel *>(index.model())->setData(index,
+m_categoryEditor->text(), CategoryRole);
+ }
+ m_updatesDisabled = false;
+ delete m_categoryEditor;
+ m_categoryEditor = 0;
+ m_editedCategory = 0;
+ updateGeometries();
+}
+*/
+
+QModelIndex GroupView::indexAt(const QPoint &point) const
+{
+ for (int i = 0; i < model()->rowCount(); ++i)
+ {
+ QModelIndex index = model()->index(i, 0);
+ if (visualRect(index).contains(point))
+ {
+ return index;
+ }
+ }
+ return QModelIndex();
+}
+
+// FIXME: is rect supposed to be geometry or visual coords?
+void GroupView::setSelection(const QRect &rect,
+ const QItemSelectionModel::SelectionFlags commands)
+{
+ for (int i = 0; i < model()->rowCount(); ++i)
+ {
+ QModelIndex index = model()->index(i, 0);
+ QRect itemRect = geometryRect(index);
+ if (itemRect.intersects(rect))
+ {
+ selectionModel()->select(index, commands);
+ update(itemRect.translated(-offset()));
+ }
+ }
+
+}
+
+QPixmap GroupView::renderToPixmap(const QModelIndexList &indices, QRect *r) const
+{
+ Q_ASSERT(r);
+ auto paintPairs = draggablePaintPairs(indices, r);
+ if (paintPairs.isEmpty())
+ {
+ return QPixmap();
+ }
+ QPixmap pixmap(r->size());
+ pixmap.fill(Qt::transparent);
+ QPainter painter(&pixmap);
+ QStyleOptionViewItem option = viewOptions();
+ option.state |= QStyle::State_Selected;
+ for (int j = 0; j < paintPairs.count(); ++j)
+ {
+ option.rect = paintPairs.at(j).first.translated(-r->topLeft());
+ const QModelIndex &current = paintPairs.at(j).second;
+ itemDelegate()->paint(&painter, option, current);
+ }
+ return pixmap;
+}
+
+QList<QPair<QRect, QModelIndex>> GroupView::draggablePaintPairs(const QModelIndexList &indices,
+ QRect *r) const
+{
+ Q_ASSERT(r);
+ QRect &rect = *r;
+ QList<QPair<QRect, QModelIndex>> ret;
+ for (int i = 0; i < indices.count(); ++i)
+ {
+ const QModelIndex &index = indices.at(i);
+ const QRect current = geometryRect(index);
+ ret += qMakePair(current, index);
+ rect |= current;
+ }
+ return ret;
+}
+
+bool GroupView::isDragEventAccepted(QDropEvent *event)
+{
+ if (event->source() != this)
+ {
+ return false;
+ }
+ if (!listsIntersect(event->mimeData()->formats(), model()->mimeTypes()))
+ {
+ return false;
+ }
+ if (!model()->canDropMimeData(event->mimeData(), event->dropAction(),
+ rowDropPos(event->pos()).second, 0, QModelIndex()))
+ {
+ return false;
+ }
+ return true;
+}
+
+QPair<Group *, int> GroupView::rowDropPos(const QPoint &pos)
+{
+ // check that we aren't on a category header and calculate which category we're in
+ Group *category = 0;
+ {
+ int y = 0;
+ for (auto cat : m_groups)
+ {
+ if (pos.y() > y && pos.y() < (y + cat->headerHeight()))
+ {
+ return qMakePair<Group*, int>(nullptr, -1);
+ }
+ y += cat->totalHeight() + m_categoryMargin;
+ if (pos.y() < y)
+ {
+ category = cat;
+ break;
+ }
+ }
+ if (category == 0)
+ {
+ return qMakePair<Group*, int>(nullptr, -1);
+ }
+ }
+
+ QList<QModelIndex> indices = category->items();
+
+ // calculate the internal column
+ int internalColumn = -1;
+ {
+ const int itemWidth = this->itemWidth();
+ if (pos.x() >= (itemWidth * itemsPerRow()))
+ {
+ internalColumn = itemsPerRow();
+ }
+ else
+ {
+ for (int i = 0, c = 0; i < contentWidth(); i += itemWidth + 10 /*spacing()*/, ++c)
+ {
+ if (pos.x() > (i - itemWidth / 2) && pos.x() <= (i + itemWidth / 2))
+ {
+ internalColumn = c;
+ break;
+ }
+ }
+ }
+ if (internalColumn == -1)
+ {
+ return qMakePair<Group*, int>(nullptr, -1);
+ }
+ }
+
+ // calculate the internal row
+ int internalRow = -1;
+ {
+ // FIXME rework the drag and drop code
+ const int top = category->verticalPosition();
+ for (int r = 0, h = top; r < category->numRows();
+ h += itemHeightForCategoryRow(category, r), ++r)
+ {
+ if (pos.y() > h && pos.y() < (h + itemHeightForCategoryRow(category, r)))
+ {
+ internalRow = r;
+ break;
+ }
+ }
+ if (internalRow == -1)
+ {
+ return qMakePair<Group*, int>(nullptr, -1);
+ }
+ // this happens if we're in the margin between a one category and another
+ // categories header
+ if (internalRow > (indices.size() / itemsPerRow()))
+ {
+ return qMakePair<Group*, int>(nullptr, -1);
+ }
+ }
+
+ // flaten the internalColumn/internalRow to one row
+ int categoryRow = internalRow * itemsPerRow() + internalColumn;
+
+ // this is used if we're past the last item
+ if (categoryRow >= indices.size())
+ {
+ return qMakePair(category, indices.last().row() + 1);
+ }
+
+ return qMakePair(category, indices.at(categoryRow).row());
+}
+
+QPoint GroupView::offset() const
+{
+ return QPoint(horizontalOffset(), verticalOffset());
+}
+
+QRegion GroupView::visualRegionForSelection(const QItemSelection &selection) const
+{
+ QRegion region;
+ for (auto &range : selection)
+ {
+ int start_row = range.top();
+ int end_row = range.bottom();
+ for (int row = start_row; row <= end_row; ++row)
+ {
+ int start_column = range.left();
+ int end_column = range.right();
+ for (int column = start_column; column <= end_column; ++column)
+ {
+ QModelIndex index = model()->index(row, column, rootIndex());
+ region += visualRect(index); // OK
+ }
+ }
+ }
+ return region;
+}
+QModelIndex GroupView::moveCursor(QAbstractItemView::CursorAction cursorAction,
+ Qt::KeyboardModifiers modifiers)
+{
+ auto current = currentIndex();
+ if(!current.isValid())
+ {
+ qDebug() << "model row: invalid";
+ return current;
+ }
+ qDebug() << "model row: " << current.row();
+ auto cat = category(current);
+ int i = m_groups.indexOf(cat);
+ if(i >= 0)
+ {
+ // this is a pile of something foul
+ auto real_group = m_groups[i];
+ int beginning_row = 0;
+ for(auto group: m_groups)
+ {
+ if(group == real_group)
+ break;
+ beginning_row += group->numRows();
+ }
+ qDebug() << "category: " << real_group->text;
+ QPair<int, int> pos = categoryInternalPosition(current);
+ int row = beginning_row + pos.second;
+ qDebug() << "row: " << row;
+ qDebug() << "column: " << pos.first;
+ }
+ return current;
+}
diff --git a/gui/groupview/GroupView.h b/gui/groupview/GroupView.h
new file mode 100644
index 00000000..e8f9107c
--- /dev/null
+++ b/gui/groupview/GroupView.h
@@ -0,0 +1,143 @@
+#pragma once
+
+#include <QListView>
+#include <QLineEdit>
+#include <QScrollBar>
+
+struct GroupViewRoles
+{
+ enum
+ {
+ GroupRole = Qt::UserRole,
+ ProgressValueRole,
+ ProgressMaximumRole
+ };
+};
+
+struct Group;
+
+class GroupView : public QAbstractItemView
+{
+ Q_OBJECT
+
+public:
+ GroupView(QWidget *parent = 0);
+ ~GroupView();
+
+ /// return geometry rectangle occupied by the specified model item
+ QRect geometryRect(const QModelIndex &index) const;
+ /// return visual rectangle occupied by the specified model item
+ virtual QRect visualRect(const QModelIndex &index) const override;
+ /// get the model index at the specified visual point
+ virtual QModelIndex indexAt(const QPoint &point) const override;
+ void setSelection(const QRect &rect,
+ const QItemSelectionModel::SelectionFlags commands) override;
+
+ virtual int horizontalOffset() const override
+ {
+ return horizontalScrollBar()->value();
+ }
+
+ virtual int verticalOffset() const override
+ {
+ return verticalScrollBar()->value();
+ }
+
+ virtual void scrollContentsBy(int dx, int dy) override
+ {
+ scrollDirtyRegion(dx, dy);
+ viewport()->scroll(dx, dy);
+ }
+
+ /*
+ * TODO!
+ */
+ virtual void scrollTo(const QModelIndex &index, ScrollHint hint = EnsureVisible) override
+ {
+ return;
+ }
+
+ virtual QModelIndex moveCursor(CursorAction cursorAction,
+ Qt::KeyboardModifiers modifiers) override;
+
+ virtual QRegion visualRegionForSelection(const QItemSelection &selection) const override;
+
+protected
+slots:
+ virtual void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight,
+ const QVector<int> &roles) override;
+ virtual void rowsInserted(const QModelIndex &parent, int start, int end) override;
+ virtual void rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) override;
+ virtual void updateGeometries() override;
+
+protected:
+ virtual bool isIndexHidden(const QModelIndex &index) const override;
+ void mousePressEvent(QMouseEvent *event) override;
+ void mouseMoveEvent(QMouseEvent *event) override;
+ void mouseReleaseEvent(QMouseEvent *event) override;
+ void mouseDoubleClickEvent(QMouseEvent *event) override;
+ void paintEvent(QPaintEvent *event) override;
+ void resizeEvent(QResizeEvent *event) override;
+
+ void dragEnterEvent(QDragEnterEvent *event) override;
+ void dragMoveEvent(QDragMoveEvent *event) override;
+ void dragLeaveEvent(QDragLeaveEvent *event) override;
+ void dropEvent(QDropEvent *event) override;
+
+ void startDrag(Qt::DropActions supportedActions) override;
+
+private:
+ friend struct Group;
+
+ QList<Group *> m_groups;
+
+ int m_leftMargin;
+ int m_rightMargin;
+ int m_bottomMargin;
+ int m_categoryMargin;
+
+ // bool m_updatesDisabled;
+
+ Group *category(const QModelIndex &index) const;
+ Group *category(const QString &cat) const;
+ Group *categoryAt(const QPoint &pos) const;
+
+ int itemsPerRow() const;
+ int contentWidth() const;
+
+private:
+ int itemWidth() const;
+ int categoryRowHeight(const QModelIndex &index) const;
+
+ /*QLineEdit *m_categoryEditor;
+ Category *m_editedCategory;
+ void startCategoryEditor(Category *category);
+
+private slots:
+ void endCategoryEditor();*/
+
+private: /* variables */
+ /// point where the currently active mouse action started in geometry coordinates
+ QPoint m_pressedPosition;
+ QPersistentModelIndex m_pressedIndex;
+ bool m_pressedAlreadySelected;
+ Group *m_pressedCategory;
+ QItemSelectionModel::SelectionFlag m_ctrlDragSelectionFlag;
+ QPoint m_lastDragPosition;
+ int m_spacing = 5;
+
+private: /* methods */
+ QPair<int, int> categoryInternalPosition(const QModelIndex &index) const;
+ int categoryInternalRowTop(const QModelIndex &index) const;
+ int itemHeightForCategoryRow(const Group *category, const int internalRow) const;
+
+ QPixmap renderToPixmap(const QModelIndexList &indices, QRect *r) const;
+ QList<QPair<QRect, QModelIndex>> draggablePaintPairs(const QModelIndexList &indices,
+ QRect *r) const;
+
+ bool isDragEventAccepted(QDropEvent *event);
+
+ QPair<Group *, int> rowDropPos(const QPoint &pos);
+
+ QPoint offset() const;
+};
diff --git a/gui/groupview/GroupedProxyModel.cpp b/gui/groupview/GroupedProxyModel.cpp
new file mode 100644
index 00000000..d9d6ac78
--- /dev/null
+++ b/gui/groupview/GroupedProxyModel.cpp
@@ -0,0 +1,26 @@
+#include "GroupedProxyModel.h"
+
+#include "GroupView.h"
+
+GroupedProxyModel::GroupedProxyModel(QObject *parent) : QSortFilterProxyModel(parent)
+{
+}
+
+bool GroupedProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
+{
+ const QString leftCategory = left.data(GroupViewRoles::GroupRole).toString();
+ const QString rightCategory = right.data(GroupViewRoles::GroupRole).toString();
+ if (leftCategory == rightCategory)
+ {
+ return subSortLessThan(left, right);
+ }
+ else
+ {
+ return leftCategory < rightCategory;
+ }
+}
+
+bool GroupedProxyModel::subSortLessThan(const QModelIndex &left, const QModelIndex &right) const
+{
+ return left.row() < right.row();
+}
diff --git a/gui/groupview/GroupedProxyModel.h b/gui/groupview/GroupedProxyModel.h
new file mode 100644
index 00000000..12edee0f
--- /dev/null
+++ b/gui/groupview/GroupedProxyModel.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include <QSortFilterProxyModel>
+
+class GroupedProxyModel : public QSortFilterProxyModel
+{
+ Q_OBJECT
+
+public:
+ GroupedProxyModel(QObject *parent = 0);
+
+protected:
+ virtual bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
+ virtual bool subSortLessThan(const QModelIndex &left, const QModelIndex &right) const;
+};
diff --git a/gui/widgets/InstanceDelegate.cpp b/gui/groupview/InstanceDelegate.cpp
index 5020b8b6..8a273758 100644
--- a/gui/widgets/InstanceDelegate.cpp
+++ b/gui/groupview/InstanceDelegate.cpp
@@ -20,6 +20,8 @@
#include <QApplication>
#include <QtCore/qmath.h>
+#include "GroupView.h"
+
// Origin: Qt
static void viewItemTextLayout(QTextLayout &textLayout, int lineWidth, qreal &height,
qreal &widthUsed)
@@ -85,6 +87,27 @@ void drawFocusRect(QPainter *painter, const QStyleOptionViewItemV4 &option, cons
painter->setRenderHint(QPainter::Antialiasing);
}
+// TODO this can be made a lot prettier
+void drawProgressOverlay(QPainter *painter, const QStyleOptionViewItemV4 &option,
+ const int value, const int maximum)
+{
+ if (maximum == 0 || value == maximum)
+ {
+ return;
+ }
+
+ painter->save();
+
+ qreal percent = (qreal)value / (qreal)maximum;
+ QColor color = option.palette.color(QPalette::Dark);
+ color.setAlphaF(0.70f);
+ painter->setBrush(color);
+ painter->setPen(QPen(QBrush(), 0));
+ painter->drawPie(option.rect, 90 * 16, -percent * 360 * 16);
+
+ painter->restore();
+}
+
static QSize viewItemTextSize(const QStyleOptionViewItemV4 *option)
{
QStyle *style = option->widget ? option->widget->style() : QApplication::style();
@@ -150,6 +173,7 @@ void ListViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opti
opt2.palette.setCurrentColorGroup(cg);
// fill in background, if any
+
if (opt.backgroundBrush.style() != Qt::NoBrush)
{
QPointF oldBO = painter->brushOrigin();
@@ -158,6 +182,9 @@ void ListViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opti
painter->setBrushOrigin(oldBO);
}
+ drawSelectionRect(painter, opt2, textHighlightRect);
+
+ /*
if (opt.showDecorationSelected)
{
drawSelectionRect(painter, opt2, opt.rect);
@@ -177,6 +204,7 @@ void ListViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opti
drawFocusRect(painter, opt2, textHighlightRect);
}
}
+ */
}
// draw the icon
@@ -229,6 +257,10 @@ void ListViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opti
line.draw(painter, position);
}
+ drawProgressOverlay(painter, opt,
+ index.data(GroupViewRoles::ProgressValueRole).toInt(),
+ index.data(GroupViewRoles::ProgressMaximumRole).toInt());
+
painter->restore();
}
diff --git a/gui/widgets/InstanceDelegate.h b/gui/groupview/InstanceDelegate.h
index 6f924405..de2f429b 100644
--- a/gui/widgets/InstanceDelegate.h
+++ b/gui/groupview/InstanceDelegate.h
@@ -20,8 +20,10 @@
class ListViewDelegate : public QStyledItemDelegate
{
public:
- explicit ListViewDelegate ( QObject* parent = 0 );
+ explicit ListViewDelegate(QObject *parent = 0);
+
protected:
- void paint ( QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index ) const;
- QSize sizeHint ( const QStyleOptionViewItem & option, const QModelIndex & index ) const;
+ void paint(QPainter *painter, const QStyleOptionViewItem &option,
+ const QModelIndex &index) const;
+ QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const;
};
diff --git a/gui/widgets/Common.cpp b/gui/widgets/Common.cpp
new file mode 100644
index 00000000..9b730d6c
--- /dev/null
+++ b/gui/widgets/Common.cpp
@@ -0,0 +1,27 @@
+#include "Common.h"
+
+// Origin: Qt
+QStringList viewItemTextLayout(QTextLayout &textLayout, int lineWidth, qreal &height,
+ qreal &widthUsed)
+{
+ QStringList lines;
+ height = 0;
+ widthUsed = 0;
+ textLayout.beginLayout();
+ QString str = textLayout.text();
+ while (true)
+ {
+ QTextLine line = textLayout.createLine();
+ if (!line.isValid())
+ break;
+ if (line.textLength() == 0)
+ break;
+ line.setLineWidth(lineWidth);
+ line.setPosition(QPointF(0, height));
+ height += line.height();
+ lines.append(str.mid(line.textStart(), line.textLength()));
+ widthUsed = qMax(widthUsed, line.naturalTextWidth());
+ }
+ textLayout.endLayout();
+ return lines;
+}
diff --git a/gui/widgets/Common.h b/gui/widgets/Common.h
new file mode 100644
index 00000000..fc46e08f
--- /dev/null
+++ b/gui/widgets/Common.h
@@ -0,0 +1,6 @@
+#pragma once
+#include <QStringList>
+#include <QTextLayout>
+
+QStringList viewItemTextLayout(QTextLayout &textLayout, int lineWidth, qreal &height,
+ qreal &widthUsed); \ No newline at end of file
diff --git a/gui/widgets/VersionListView.cpp b/gui/widgets/VersionListView.cpp
new file mode 100644
index 00000000..b7f45f27
--- /dev/null
+++ b/gui/widgets/VersionListView.cpp
@@ -0,0 +1,150 @@
+/* Copyright 2013 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <QHeaderView>
+#include <QApplication>
+#include <QMouseEvent>
+#include <QDrag>
+#include <QPainter>
+#include "VersionListView.h"
+#include "Common.h"
+
+VersionListView::VersionListView(QWidget *parent)
+ :QTreeView ( parent )
+{
+ m_emptyString = tr("No versions are currently available.");
+}
+
+void VersionListView::rowsInserted(const QModelIndex &parent, int start, int end)
+{
+ if(!m_itemCount)
+ viewport()->update();
+ m_itemCount += end-start+1;
+ QTreeView::rowsInserted(parent, start, end);
+}
+
+
+void VersionListView::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end)
+{
+ m_itemCount -= end-start+1;
+ if(!m_itemCount)
+ viewport()->update();
+ QTreeView::rowsInserted(parent, start, end);
+}
+
+void VersionListView::setModel(QAbstractItemModel *model)
+{
+ m_itemCount = model->rowCount();
+ if(!m_itemCount)
+ viewport()->update();
+ QTreeView::setModel(model);
+}
+
+void VersionListView::reset()
+{
+ if(model())
+ {
+ m_itemCount = model()->rowCount();
+ }
+ viewport()->update();
+ QTreeView::reset();
+}
+
+void VersionListView::setEmptyString(QString emptyString)
+{
+ m_emptyString = emptyString;
+ if(!m_itemCount)
+ {
+ viewport()->update();
+ }
+}
+
+void VersionListView::paintEvent(QPaintEvent *event)
+{
+ if(m_itemCount)
+ {
+ QTreeView::paintEvent(event);
+ }
+ else
+ {
+ paintInfoLabel(event);
+ }
+}
+
+void VersionListView::paintInfoLabel(QPaintEvent *event)
+{
+ int scrollInterval = 500;
+
+ //calculate the rect for the overlay
+ QPainter painter(viewport());
+ painter.setRenderHint(QPainter::Antialiasing, true);
+ const QChar letter = 'Q';
+ QFont font("sans", 20);
+ font.setBold(true);
+
+ QRect bounds = viewport()->geometry();
+ bounds.moveTop(0);
+ QTextLayout layout(m_emptyString, font);
+ qreal height = 0.0;
+ qreal widthUsed = 0.0;
+ QStringList lines = viewItemTextLayout(layout, bounds.width() - 20, height, widthUsed);
+ QRect rect (0,0, widthUsed, height);
+ rect.setWidth(rect.width()+20);
+ rect.setHeight(rect.height()+20);
+ rect.moveCenter(bounds.center());
+ //check if we are allowed to draw in our area
+ if (!event->rect().intersects(rect)) {
+ return;
+ }
+ //draw the letter of the topmost item semitransparent in the middle
+ QColor background = QApplication::palette().color(QPalette::Foreground);
+ QColor foreground = QApplication::palette().color(QPalette::Base);
+ /*
+ background.setAlpha(128 - scrollFade);
+ foreground.setAlpha(128 - scrollFade);
+ */
+ painter.setBrush(QBrush(background));
+ painter.setPen(foreground);
+ painter.drawRoundedRect(rect, 5.0, 5.0);
+ foreground.setAlpha(190);
+ painter.setPen(foreground);
+ painter.setFont(font);
+ painter.drawText(rect, Qt::AlignCenter, lines.join("\n"));
+
+}
+
+/*
+void ModListView::setModel ( QAbstractItemModel* model )
+{
+ QTreeView::setModel ( model );
+ auto head = header();
+ head->setStretchLastSection(false);
+ // HACK: this is true for the checkbox column of mod lists
+ auto string = model->headerData(0,head->orientation()).toString();
+ if(!string.size())
+ {
+ head->setSectionResizeMode(0, QHeaderView::ResizeToContents);
+ head->setSectionResizeMode(1, QHeaderView::Stretch);
+ for(int i = 2; i < head->count(); i++)
+ head->setSectionResizeMode(i, QHeaderView::ResizeToContents);
+ }
+ else
+ {
+ head->setSectionResizeMode(0, QHeaderView::Stretch);
+ for(int i = 1; i < head->count(); i++)
+ head->setSectionResizeMode(i, QHeaderView::ResizeToContents);
+ }
+}
+*/ \ No newline at end of file
diff --git a/gui/widgets/VersionListView.h b/gui/widgets/VersionListView.h
new file mode 100644
index 00000000..af9b1f6a
--- /dev/null
+++ b/gui/widgets/VersionListView.h
@@ -0,0 +1,43 @@
+/* Copyright 2013 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+#include <QTreeView>
+
+class Mod;
+
+class VersionListView : public QTreeView
+{
+ Q_OBJECT
+public:
+ explicit VersionListView(QWidget *parent = 0);
+ virtual void paintEvent(QPaintEvent *event) override;
+ void setEmptyString(QString emptyString);
+ virtual void setModel ( QAbstractItemModel* model );
+
+public slots:
+ virtual void reset() override;
+
+protected slots:
+ virtual void rowsAboutToBeRemoved(const QModelIndex & parent, int start, int end) override;
+ virtual void rowsInserted(const QModelIndex &parent, int start, int end) override;
+
+private: /* methods */
+ void paintInfoLabel(QPaintEvent *event);
+
+private: /* variables */
+ int m_itemCount = 0;
+ QString m_emptyString;
+};