summaryrefslogtreecommitdiffstats
path: root/gui
diff options
context:
space:
mode:
authorPetr Mrázek <peterix@gmail.com>2014-02-09 20:49:48 +0100
committerPetr Mrázek <peterix@gmail.com>2014-02-09 20:49:48 +0100
commit1f6a484cb2368a5704cdb4820ac06194ea6d7e1a (patch)
treef4ebfd97c25387b3e26d4a55c5c85d67782df25d /gui
parentf8df07c3272c0e02f31f46fda8a429292c7a446a (diff)
parent18f532b0d7d873280ec17218196db15fa64175a2 (diff)
downloadMultiMC-1f6a484cb2368a5704cdb4820ac06194ea6d7e1a.tar
MultiMC-1f6a484cb2368a5704cdb4820ac06194ea6d7e1a.tar.gz
MultiMC-1f6a484cb2368a5704cdb4820ac06194ea6d7e1a.tar.lz
MultiMC-1f6a484cb2368a5704cdb4820ac06194ea6d7e1a.tar.xz
MultiMC-1f6a484cb2368a5704cdb4820ac06194ea6d7e1a.zip
Merge branch 'integration_derpstances_groupview' into develop
Diffstat (limited to 'gui')
-rw-r--r--gui/MainWindow.cpp30
-rw-r--r--gui/MainWindow.h6
-rw-r--r--gui/dialogs/IconPickerDialog.cpp2
-rw-r--r--gui/dialogs/OneSixModEditDialog.cpp253
-rw-r--r--gui/dialogs/OneSixModEditDialog.h12
-rw-r--r--gui/dialogs/OneSixModEditDialog.ui65
-rw-r--r--gui/groupview/Group.cpp269
-rw-r--r--gui/groupview/Group.h69
-rw-r--r--gui/groupview/GroupView.cpp935
-rw-r--r--gui/groupview/GroupView.h139
-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)57
-rw-r--r--gui/groupview/InstanceDelegate.h (renamed from gui/widgets/InstanceDelegate.h)8
14 files changed, 1740 insertions, 146 deletions
diff --git a/gui/MainWindow.cpp b/gui/MainWindow.cpp
index 9977dc75..cd8ec926 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"
@@ -140,21 +141,21 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi
// 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);
@@ -331,7 +332,6 @@ MainWindow::~MainWindow()
{
delete ui;
delete proxymodel;
- delete drawer;
}
void MainWindow::showInstanceContextMenu(const QPoint &pos)
diff --git a/gui/MainWindow.h b/gui/MainWindow.h
index eeba2c26..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;
@@ -185,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/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 27315c69..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,98 +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)
@@ -200,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
@@ -215,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"),
@@ -234,19 +299,16 @@ void OneSixModEditDialog::on_liteloaderBtn_clicked()
"into this version of Minecraft"));
return;
}
- if (!m_inst->versionIsCustom())
- {
- m_inst->customizeVersion();
- m_version = m_inst->getFullVersion();
- main_model->setSourceModel(m_version.get());
- updateVersionControls();
- }
- if (!liteloader.apply(m_version))
+ if (!liteloader.add(m_inst))
{
QMessageBox::critical(this, tr("LiteLoader"),
tr("For reasons unknown, the LiteLoader installation failed. "
"Check your MultiMC log files for details."));
}
+ else
+ {
+ m_inst->reloadVersion(this);
+ }
}
bool OneSixModEditDialog::loaderListFilter(QKeyEvent *keyEvent)
@@ -281,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)
@@ -365,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/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..5ee44cbb
--- /dev/null
+++ b/gui/groupview/GroupView.cpp
@@ -0,0 +1,935 @@
+#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)
+{
+ /*
+ if (roles.contains(GroupViewRoles::GroupRole) || roles.contains(Qt::DisplayRole))
+ {
+ */
+ updateGeometries();
+ /*
+ }
+ */
+ viewport()->update();
+}
+void GroupView::rowsInserted(const QModelIndex &parent, int start, int end)
+{
+ updateGeometries();
+ viewport()->update();
+}
+
+void GroupView::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end)
+{
+ updateGeometries();
+ viewport()->update();
+}
+
+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;
+ for (auto category : m_groups)
+ {
+ category->m_verticalPosition = totalHeight;
+ totalHeight += category->totalHeight() + m_categoryMargin;
+ }
+ auto category = m_groups.last();
+ int itemScroll = category->contentHeight() / category->numRows();
+ /*
+ // remove the last margin (we don't want it)
+ totalHeight -= m_categoryMargin;
+ // add a margin on top...
+ totalHeight += m_categoryMargin;
+ */
+ 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 pos = event->pos() + offset();
+ QPersistentModelIndex index = indexAt(pos);
+
+ m_pressedIndex = index;
+ m_pressedAlreadySelected = selectionModel()->isSelected(m_pressedIndex);
+ QItemSelectionModel::SelectionFlags selection_flags = selectionCommand(index, event);
+ m_pressedPosition = pos;
+
+ m_pressedCategory = categoryAt(m_pressedPosition);
+ 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(m_pressedPosition, pos);
+ 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 pos = 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 = pos;
+ }
+
+ 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(pos, pos), QItemSelectionModel::ClearAndSelect);
+ QModelIndex index = indexAt(pos);
+
+ // 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 pos = event->pos() + offset();
+ QPersistentModelIndex index = indexAt(pos);
+
+ bool click = (index == m_pressedIndex && index.isValid()) ||
+ (m_pressedCategory && m_pressedCategory == categoryAt(pos));
+
+ 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 (geometryRect(index).contains(point))
+ {
+ return index;
+ }
+ }
+ return QModelIndex();
+}
+
+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(nullptr, -1);
+ }
+ y += cat->totalHeight() + m_categoryMargin;
+ if (pos.y() < y)
+ {
+ category = cat;
+ break;
+ }
+ }
+ if (category == 0)
+ {
+ return qMakePair(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(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(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(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..329a3503
--- /dev/null
+++ b/gui/groupview/GroupView.h
@@ -0,0 +1,139 @@
+#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();
+
+ QRect geometryRect(const QModelIndex &index) const;
+ virtual QRect visualRect(const QModelIndex &index) const override;
+ QModelIndex indexAt(const QPoint &point) const;
+ 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 */
+ 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 33da7130..8a273758 100644
--- a/gui/widgets/InstanceDelegate.cpp
+++ b/gui/groupview/InstanceDelegate.cpp
@@ -19,7 +19,32 @@
#include <QTextLayout>
#include <QApplication>
#include <QtCore/qmath.h>
-#include "Common.h"
+
+#include "GroupView.h"
+
+// Origin: Qt
+static void viewItemTextLayout(QTextLayout &textLayout, int lineWidth, qreal &height,
+ qreal &widthUsed)
+{
+ 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();
+ widthUsed = qMax(widthUsed, line.naturalTextWidth());
+ }
+ textLayout.endLayout();
+}
+
#define QFIXED_MAX (INT_MAX / 256)
ListViewDelegate::ListViewDelegate(QObject *parent) : QStyledItemDelegate(parent)
@@ -62,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();
@@ -127,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();
@@ -135,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);
@@ -154,6 +204,7 @@ void ListViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opti
drawFocusRect(painter, opt2, textHighlightRect);
}
}
+ */
}
// draw the icon
@@ -206,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;
};