Files
giesCANne/dashboardwindow.cpp
2026-09-12 13:06:26 +02:00

443 lines
13 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "dashboardwindow.h"
#include <QApplication>
#include <QCloseEvent>
#include <QDrag>
#include <QDragEnterEvent>
#include <QDragLeaveEvent>
#include <QDropEvent>
#include <QFrame>
#include <QHBoxLayout>
#include <QLabel>
#include <QMimeData>
#include <QMouseEvent>
#include <QResizeEvent>
#include <QSettings>
#include <QSplitter>
#include <QToolButton>
#include <QUuid>
#include <QVBoxLayout>
#include <QtMath>
#include <functional>
namespace {
const char *TileMimeType = "application/x-giescan-dashboard-tile";
}
class DashboardTile : public QFrame
{
public:
explicit DashboardTile(QWidget *tool, const QString &toolId, QWidget *parent = nullptr) :
QFrame(parent),
tool(tool),
dropOverlay(nullptr),
identifier(QUuid::createUuid().toString(QUuid::WithoutBraces))
{
setAcceptDrops(true);
setFrameShape(QFrame::StyledPanel);
setMinimumSize(120, 90);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
setProperty("dashboardToolId", toolId);
QVBoxLayout *layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
layout->setSizeConstraint(QLayout::SetNoConstraint);
titleBar = new QWidget(this);
titleBar->setObjectName("dashboardTileTitleBar");
titleBar->setCursor(Qt::OpenHandCursor);
titleBar->setContextMenuPolicy(Qt::PreventContextMenu);
titleBar->installEventFilter(this);
QHBoxLayout *titleLayout = new QHBoxLayout(titleBar);
titleLayout->setContentsMargins(8, 2, 2, 2);
titleLayout->setSpacing(6);
QLabel *title = new QLabel(tool->windowTitle().isEmpty() ? toolId : tool->windowTitle(), titleBar);
title->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
title->setAttribute(Qt::WA_TransparentForMouseEvents);
titleLayout->addWidget(title);
QToolButton *closeButton = new QToolButton(titleBar);
closeButton->setText(QString::fromUtf8("×"));
closeButton->setToolTip(QObject::tr("Remove from dashboard"));
closeButton->setAutoRaise(true);
closeButton->setFixedSize(24, 24);
titleLayout->addWidget(closeButton);
layout->addWidget(titleBar);
tool->setWindowFlags(Qt::Widget);
tool->setMinimumSize(0, 0);
tool->setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX);
tool->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
layout->addWidget(tool, 1);
tool->show();
dropOverlay = new QFrame(this);
dropOverlay->setAttribute(Qt::WA_TransparentForMouseEvents);
dropOverlay->setStyleSheet(
"QFrame { background-color: rgba(40, 120, 210, 105); border: 3px solid palette(highlight); }"
"QLabel { color: white; background: transparent; border: none; font-weight: bold; }");
QVBoxLayout *overlayLayout = new QVBoxLayout(dropOverlay);
QLabel *dropLabel = new QLabel(QObject::tr("Drop here"), dropOverlay);
dropLabel->setAlignment(Qt::AlignCenter);
overlayLayout->addWidget(dropLabel);
dropOverlay->setGeometry(rect());
dropOverlay->hide();
connect(closeButton, &QToolButton::clicked, this, [this]() {
if (closeRequested)
closeRequested(this);
});
}
QWidget *toolWidget() const
{
return tool;
}
QWidget *takeToolWidget()
{
QWidget *result = tool;
if (result)
{
layout()->removeWidget(result);
result->setParent(nullptr);
}
tool = nullptr;
return result;
}
QString tileId() const
{
return identifier;
}
std::function<void(DashboardTile *)> closeRequested;
std::function<void(const QString &, DashboardTile *)> tileDropped;
protected:
bool eventFilter(QObject *watched, QEvent *event) override
{
if (watched != titleBar)
return QFrame::eventFilter(watched, event);
if (event->type() == QEvent::ContextMenu)
return true;
if (event->type() == QEvent::MouseButtonPress)
{
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
if (mouseEvent->button() == Qt::LeftButton)
{
dragStart = mouseEvent->pos();
titleBar->setCursor(Qt::ClosedHandCursor);
}
}
else if (event->type() == QEvent::MouseButtonRelease)
{
titleBar->setCursor(Qt::OpenHandCursor);
}
else if (event->type() == QEvent::MouseMove)
{
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
if ((mouseEvent->buttons() & Qt::LeftButton) &&
(mouseEvent->pos() - dragStart).manhattanLength() >= QApplication::startDragDistance())
{
QDrag *drag = new QDrag(this);
QMimeData *mimeData = new QMimeData;
mimeData->setData(TileMimeType, identifier.toUtf8());
drag->setMimeData(mimeData);
drag->exec(Qt::MoveAction);
titleBar->setCursor(Qt::OpenHandCursor);
return true;
}
}
return QFrame::eventFilter(watched, event);
}
void dragEnterEvent(QDragEnterEvent *event) override
{
if (event->mimeData()->hasFormat(TileMimeType))
{
dropOverlay->setGeometry(rect());
dropOverlay->raise();
dropOverlay->show();
event->acceptProposedAction();
}
}
void dragLeaveEvent(QDragLeaveEvent *event) override
{
dropOverlay->hide();
event->accept();
}
void dropEvent(QDropEvent *event) override
{
if (!event->mimeData()->hasFormat(TileMimeType))
return;
dropOverlay->hide();
if (tileDropped)
tileDropped(QString::fromUtf8(event->mimeData()->data(TileMimeType)), this);
event->acceptProposedAction();
}
void resizeEvent(QResizeEvent *event) override
{
QFrame::resizeEvent(event);
dropOverlay->setGeometry(rect());
}
private:
QWidget *tool;
QWidget *titleBar;
QFrame *dropOverlay;
QString identifier;
QPoint dragStart;
};
DashboardWindow::DashboardWindow(const QString &dashboardId, const QString &dashboardName, QWidget *parent) :
QMainWindow(parent),
id(dashboardId),
name(dashboardName),
workspace(new QWidget(this)),
workspaceLayout(new QVBoxLayout(workspace)),
outerSplitter(nullptr)
{
setAttribute(Qt::WA_DeleteOnClose, false);
workspaceLayout->setContentsMargins(2, 2, 2, 2);
workspaceLayout->setSpacing(0);
workspaceLayout->setSizeConstraint(QLayout::SetNoConstraint);
workspace->setMinimumSize(0, 0);
setCentralWidget(workspace);
setDashboardName(name);
QSettings settings;
settings.beginGroup("Dashboards/" + id);
const QByteArray geometry = settings.value("Geometry").toByteArray();
if (geometry.isEmpty())
resize(1200, 760);
else
restoreGeometry(geometry);
settings.endGroup();
}
DashboardWindow::~DashboardWindow()
{
saveLayout();
}
QString DashboardWindow::dashboardId() const
{
return id;
}
QString DashboardWindow::dashboardName() const
{
return name;
}
void DashboardWindow::setDashboardName(const QString &dashboardName)
{
name = dashboardName;
setWindowTitle(tr("Dashboard — %1").arg(name));
}
void DashboardWindow::addToolWindow(QWidget *widget, const QString &toolId, const QRect &savedGeometry)
{
Q_UNUSED(savedGeometry);
if (!widget)
return;
DashboardTile *tile = new DashboardTile(widget, toolId, workspace);
tile->closeRequested = [this](DashboardTile *requestedTile) {
closeTile(requestedTile);
};
tile->tileDropped = [this](const QString &sourceId, DashboardTile *target) {
moveTile(sourceId, target);
};
tiles.append(tile);
rebuildLayout();
}
bool DashboardWindow::removeToolWindow(QWidget *widget)
{
for (int i = 0; i < tiles.size(); ++i)
{
DashboardTile *tile = tiles.at(i);
if (tile->toolWidget() != widget)
continue;
tiles.removeAt(i);
tile->takeToolWidget();
tile->deleteLater();
widget->setWindowFlags(Qt::Window);
rebuildLayout();
return true;
}
return false;
}
QList<QWidget *> DashboardWindow::toolWindows(const QString &toolId) const
{
QList<QWidget *> result;
for (DashboardTile *tile : tiles)
{
if (tile->property("dashboardToolId").toString() == toolId && tile->toolWidget())
result.append(tile->toolWidget());
}
return result;
}
void DashboardWindow::closeTile(DashboardTile *tile)
{
if (!tile)
return;
QWidget *tool = tile->toolWidget();
tiles.removeOne(tile);
if (tool && tool->property("dashboardPersistent").toBool())
{
tile->takeToolWidget();
tool->setWindowFlags(Qt::Window);
tool->hide();
}
tile->deleteLater();
rebuildLayout();
}
void DashboardWindow::moveTile(const QString &sourceId, DashboardTile *target)
{
int sourceIndex = -1;
const int targetIndex = tiles.indexOf(target);
for (int i = 0; i < tiles.size(); ++i)
{
if (tiles.at(i)->tileId() == sourceId)
{
sourceIndex = i;
break;
}
}
if (sourceIndex < 0 || targetIndex < 0 || sourceIndex == targetIndex)
return;
tiles.swapItemsAt(sourceIndex, targetIndex);
rebuildLayout(true);
}
void DashboardWindow::rebuildLayout(bool preserveSizes)
{
QByteArray outerState;
QList<QByteArray> rowStates;
if (preserveSizes && outerSplitter)
{
outerState = outerSplitter->saveState();
for (QSplitter *row : rowSplitters)
rowStates.append(row->saveState());
}
for (DashboardTile *tile : tiles)
tile->setParent(workspace);
while (QLayoutItem *item = workspaceLayout->takeAt(0))
{
if (item->widget())
delete item->widget();
delete item;
}
outerSplitter = nullptr;
rowSplitters.clear();
if (tiles.isEmpty())
return;
outerSplitter = new QSplitter(Qt::Vertical, workspace);
outerSplitter->setChildrenCollapsible(false);
outerSplitter->setHandleWidth(5);
workspaceLayout->addWidget(outerSplitter);
const int columns = qCeil(qSqrt(static_cast<double>(tiles.size())));
const int rows = qCeil(static_cast<double>(tiles.size()) / columns);
int tileIndex = 0;
for (int rowIndex = 0; rowIndex < rows; ++rowIndex)
{
QSplitter *row = new QSplitter(Qt::Horizontal, outerSplitter);
row->setChildrenCollapsible(false);
row->setHandleWidth(5);
rowSplitters.append(row);
const int remaining = tiles.size() - tileIndex;
const int tilesInRow = qMin(columns, remaining);
for (int column = 0; column < tilesInRow; ++column)
{
DashboardTile *tile = tiles.at(tileIndex++);
row->addWidget(tile);
tile->show();
}
outerSplitter->addWidget(row);
}
if (preserveSizes)
{
outerSplitter->restoreState(outerState);
for (int i = 0; i < qMin(rowSplitters.size(), rowStates.size()); ++i)
rowSplitters.at(i)->restoreState(rowStates.at(i));
}
}
void DashboardWindow::restoreLayout()
{
if (!outerSplitter)
return;
QSettings settings;
settings.beginGroup("Dashboards/" + id);
outerSplitter->restoreState(settings.value("OuterSplitter").toByteArray());
const int count = settings.beginReadArray("RowSplitters");
for (int i = 0; i < qMin(count, rowSplitters.size()); ++i)
{
settings.setArrayIndex(i);
rowSplitters.at(i)->restoreState(settings.value("State").toByteArray());
}
settings.endArray();
settings.endGroup();
}
void DashboardWindow::saveLayout()
{
QSettings settings;
settings.beginGroup("Dashboards/" + id);
settings.setValue("Name", name);
settings.setValue("Geometry", saveGeometry());
if (outerSplitter)
settings.setValue("OuterSplitter", outerSplitter->saveState());
settings.beginWriteArray("RowSplitters", rowSplitters.size());
for (int i = 0; i < rowSplitters.size(); ++i)
{
settings.setArrayIndex(i);
settings.setValue("State", rowSplitters.at(i)->saveState());
}
settings.endArray();
settings.beginWriteArray("Windows", tiles.size());
for (int i = 0; i < tiles.size(); ++i)
{
settings.setArrayIndex(i);
settings.setValue("Tool", tiles.at(i)->property("dashboardToolId").toString());
}
settings.endArray();
settings.endGroup();
}
void DashboardWindow::closeEvent(QCloseEvent *event)
{
saveLayout();
QMainWindow::closeEvent(event);
}