diff options
Diffstat (limited to 'gui')
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 ¤t, 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 ¤t, 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>&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>&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 ¤t = 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; +}; |