From c8f71cb9e3f90d195ed528a139918ae96ef381a2 Mon Sep 17 00:00:00 2001 From: Jesse Qu Date: Wed, 8 Jul 2026 16:57:46 +0800 Subject: [PATCH] =?UTF-8?q?=E2=8F=BA=20feat:=20add=20Document=20layer=20wi?= =?UTF-8?q?th=20serialization,=20dirty=20tracking,=20and=20file=20actions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce a Document QObject that composes DesignerScene + QUndoStack, following "composition over inheritance" and unidirectional signal-slot communication per Qt Graphics View Framework conventions. New files: - include/document.h — Document class declaration - source/document.cpp — full implementation (~450 lines) Core capabilities: - JSON serialization/deserialization for rect, roundRect, polygon, group (uses dynamic_cast dispatch since subclasses don't override type()) - Two-path dirty tracking: 1. QUndoStack::indexChanged for undoable ops (add/delete/group/ungroup) 2. Explicit markDirty() via DesignerScene::notifyDocumentModified() called by selectors after successful move/scale/rotate/edit - save()/saveAs()/load()/clear() with maybeSave() prompt on close - File actions (New/Open/Save/Save As) wired to toolbar and File menu; window title tracks filename + modified indicator Integration changes: - DrawingPanel now receives scene via setScene() instead of creating it - CMainWindow routes all modifications through Document::execute() - DesignerScene gains notifyDocumentModified() for selector dirty marking - GraphicsRectItem gains isRound()/ratioX()/ratioY() accessors - CreatingSelector: fix missing signalAddItem emit for polygon completion - Scene lifecycle uses clear() instead of delete+recreate to avoid dangling view pointer Moved QUndoStack ownership from CMainWindow into Document. --- .gitignore | 1 + CMakeLists.txt | 2 + include/designerScene.h | 3 + include/document.h | 65 ++++ include/drawingPanel.h | 2 + include/graphicsItem/graphicsRectItem.h | 5 + include/mainwindow.h | 10 +- source/designerScene.cpp | 8 + source/document.cpp | 385 ++++++++++++++++++++++++ source/drawingPanel.cpp | 17 +- source/mainwindow.cpp | 155 ++++++++-- source/util/creatingSelector.cpp | 3 + source/util/editingSelector.cpp | 1 + source/util/movingSelector.cpp | 3 + source/util/rotationSelector.cpp | 4 +- source/util/scalingSelector.cpp | 1 + 16 files changed, 635 insertions(+), 30 deletions(-) create mode 100644 include/document.h create mode 100644 source/document.cpp diff --git a/.gitignore b/.gitignore index 4d6d6b6..f9c552d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ build/ .vscode/ .qtcreator/ .claude/ +docs/ # ---> CC CLAUDE.md diff --git a/CMakeLists.txt b/CMakeLists.txt index 838a0be..ffd4e0d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,6 +44,7 @@ set(H_HEADER_FILES include/designerScene.h include/designerView.h include/operationCommand.h + include/document.h include/propertyType/CustomGadget.h include/propertyType/CustomType.h @@ -71,6 +72,7 @@ set(CPP_SOURCE_FILES source/designerScene.cpp source/designerView.cpp source/operationCommand.cpp + source/document.cpp source/propertyType/PropertyTypeCustomization_CustomType.cpp diff --git a/include/designerScene.h b/include/designerScene.h index 5aad91b..79aa86f 100644 --- a/include/designerScene.h +++ b/include/designerScene.h @@ -21,6 +21,9 @@ public: GraphicsItemGroup* createGroup(); void destroyGroup(); + // Notify Document that a non-undo scene change occurred (move/scale/rotate/edit) + void notifyDocumentModified(); + signals: void signalAddItem(QGraphicsItem*); diff --git a/include/document.h b/include/document.h new file mode 100644 index 0000000..e633ad8 --- /dev/null +++ b/include/document.h @@ -0,0 +1,65 @@ +#ifndef DOCUMENT_H +#define DOCUMENT_H + +#include +#include + +class DesignerScene; +class QGraphicsItem; +class QUndoCommand; +class QUndoStack; + +class Document : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool modified READ isModified NOTIFY modifiedChanged) + +public: + explicit Document(QObject *parent = nullptr); + ~Document(); + + // Serialization + bool save(); + bool saveAs(const QString &filePath); + bool load(const QString &filePath); + + // State + bool isModified() const; + QString filePath() const; + + // Unified command entry point + void execute(QUndoCommand *cmd); + + // Accessors (read-only, no ownership transfer) + DesignerScene *scene() const; + QUndoStack *undoStack() const; + + // Reset to empty document + void clear(); + + // Called by selectors to mark dirty for non-undo operations + // (move, scale, rotate, edit-shape) + void markDirty(); + +signals: + void modifiedChanged(bool modified); + void filePathChanged(const QString &filePath); + void saved(); + void loaded(); + +private: + void setModified(bool modified); + void setupNewScene(); + + // Serialization helpers + QJsonObject serializeItem(QGraphicsItem *item) const; + QGraphicsItem *deserializeItem(const QJsonObject &obj); + + DesignerScene *m_pScene; + QUndoStack *m_pUndoStack; + bool m_bModified; + int m_nSavedIndex; + QString m_sFilePath; +}; + +#endif // DOCUMENT_H diff --git a/include/drawingPanel.h b/include/drawingPanel.h index c29b571..c556dbb 100644 --- a/include/drawingPanel.h +++ b/include/drawingPanel.h @@ -23,6 +23,8 @@ public: QGraphicsScene* getQGraphicsScene(); DesignerScene* getDesignerScene(); + void setScene(DesignerScene* scene); + void grahpicsViewZoomIn(); void grahpicsViewZoomOut(); void grahpicsViewZoomFit(); diff --git a/include/graphicsItem/graphicsRectItem.h b/include/graphicsItem/graphicsRectItem.h index 54e9601..bae9663 100644 --- a/include/graphicsItem/graphicsRectItem.h +++ b/include/graphicsItem/graphicsRectItem.h @@ -13,6 +13,11 @@ public: void resize(int,double, double, const QPointF&); void updateCoordinate(); + bool isRound() const { return m_bIsRound; } + double ratioX() const { return m_dRatioX; } + double ratioY() const { return m_dRatioY; } + void setRatioX(double rx) { m_dRatioX = rx; } + void setRatioY(double ry) { m_dRatioY = ry; } void move(const QPointF&); void editShape(int, const QPointF&); diff --git a/include/mainwindow.h b/include/mainwindow.h index 2c06fd3..5668d4b 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -18,7 +18,7 @@ namespace Ui { class CMainWindow; } QT_END_NAMESPACE class QGraphicsItem; -class QUndoStack; +class Document; class DrawingPanel; class GraphicElementsPanel; class DesignerScene; @@ -38,8 +38,14 @@ protected: private: void initializeDockUi(); void initializeAction(); + bool maybeSave(); + void updateWindowTitle(); private slots: + void onAction_new(); + void onAction_open(); + void onAction_save(); + void onAction_saveAs(); void onAction_zoomIn(); void onAction_zoomOut(); void onAction_zoomFit(); @@ -54,7 +60,7 @@ private: QWidgetAction* m_pPerspectiveListAction = nullptr; QComboBox* m_pPerspectiveComboBox = nullptr; - QUndoStack* m_pUndoStack; + Document* m_pDocument; Ui::CMainWindow *ui; diff --git a/source/designerScene.cpp b/source/designerScene.cpp index 7d31fd8..ba2c818 100644 --- a/source/designerScene.cpp +++ b/source/designerScene.cpp @@ -1,6 +1,7 @@ #include "designerScene.h" #include "util/selectorManager.h" #include "graphicsItem/graphicsItemGroup.h" +#include "document.h" #include #include @@ -174,3 +175,10 @@ void DesignerScene::destroyGroup() { } + +void DesignerScene::notifyDocumentModified() +{ + Document *doc = qobject_cast(parent()); + if (doc) + doc->markDirty(); +} diff --git a/source/document.cpp b/source/document.cpp new file mode 100644 index 0000000..4f21dec --- /dev/null +++ b/source/document.cpp @@ -0,0 +1,385 @@ +#include "document.h" +#include "designerScene.h" + +#include "graphicsItem/graphicsBaseItem.h" +#include "graphicsItem/graphicsRectItem.h" +#include "graphicsItem/graphicsPolygonItem.h" +#include "graphicsItem/graphicsItemGroup.h" +#include "graphicsItem/itemControlHandle.h" +#include "global.h" + +#include +#include +#include +#include + +#include +#include + +Document::Document(QObject *parent) + : QObject(parent) + , m_pScene(nullptr) + , m_pUndoStack(nullptr) + , m_bModified(false) + , m_nSavedIndex(0) +{ + setupNewScene(); + + m_pUndoStack = new QUndoStack(this); + connect(m_pUndoStack, &QUndoStack::indexChanged, this, [this](int idx) { + setModified(idx != m_nSavedIndex); + }); +} + +Document::~Document() +{ +} + +void Document::setupNewScene() +{ + if (m_pScene) { + // Clear items only — do NOT delete the scene. The view in DrawingPanel + // still references it, and deleting would leave a dangling pointer. + m_pScene->clear(); + } else { + m_pScene = new DesignerScene(this); + } +} + +DesignerScene *Document::scene() const +{ + return m_pScene; +} + +QUndoStack *Document::undoStack() const +{ + return m_pUndoStack; +} + +bool Document::isModified() const +{ + return m_bModified; +} + +QString Document::filePath() const +{ + return m_sFilePath; +} + +void Document::setModified(bool modified) +{ + if (m_bModified != modified) { + m_bModified = modified; + emit modifiedChanged(m_bModified); + } +} + +void Document::execute(QUndoCommand *cmd) +{ + m_pUndoStack->push(cmd); +} + +void Document::markDirty() +{ + setModified(true); +} + +void Document::clear() +{ + m_nSavedIndex = 0; + m_pUndoStack->clear(); + setupNewScene(); + m_sFilePath.clear(); + setModified(false); + emit filePathChanged(m_sFilePath); +} + +// -- Pen style string conversion ------------------------------------------ + +static QString penStyleToString(Qt::PenStyle style) +{ + switch (style) { + case Qt::NoPen: return "none"; + case Qt::SolidLine: return "solid"; + case Qt::DashLine: return "dash"; + case Qt::DotLine: return "dot"; + case Qt::DashDotLine: return "dashDot"; + case Qt::DashDotDotLine: return "dashDotDot"; + default: return "solid"; + } +} + +static Qt::PenStyle penStyleFromString(const QString &str) +{ + static const QHash map = { + {"none", Qt::NoPen}, + {"solid", Qt::SolidLine}, + {"dash", Qt::DashLine}, + {"dot", Qt::DotLine}, + {"dashDot", Qt::DashDotLine}, + {"dashDotDot", Qt::DashDotDotLine} + }; + return map.value(str, Qt::SolidLine); +} + +// -- Write helpers --------------------------------------------------------- + +static QJsonObject serializePen(const QPen &pen) +{ + QJsonObject obj; + obj["color"] = pen.color().name(QColor::HexArgb); + obj["width"] = pen.widthF(); + obj["style"] = penStyleToString(pen.style()); + return obj; +} + +static QJsonObject serializeBrush(const QBrush &brush) +{ + QJsonObject obj; + obj["color"] = brush.color().name(QColor::HexArgb); + return obj; +} + +static QJsonObject serializeCommon(QGraphicsItem *item, const QString &type) +{ + QJsonObject obj; + obj["type"] = type; + obj["pos"] = QJsonObject{{"x", item->pos().x()}, {"y", item->pos().y()}}; + obj["rotation"] = item->rotation(); + obj["zValue"] = item->zValue(); + return obj; +} + +// -- Item serialization ---------------------------------------------------- + +QJsonObject Document::serializeItem(QGraphicsItem *item) const +{ + // Groups use a different template instantiation (AbstractShapeType), + // so qgraphicsitem_cast will not match them. + // Check for groups first. + GraphicsItemGroup *group = dynamic_cast(item); + if (group) { + QJsonObject obj = serializeCommon(item, "group"); + obj["pen"] = serializePen(group->pen()); + obj["brush"] = serializeBrush(group->brush()); + + QJsonArray children; + const auto kids = group->childItems(); + for (QGraphicsItem *child : kids) { + if (qgraphicsitem_cast(child)) + continue; + QJsonObject childObj = serializeItem(child); + if (!childObj.isEmpty()) + children.append(childObj); + } + obj["children"] = children; + return obj; + } + + AbstractShape *shape = qgraphicsitem_cast(item); + if (!shape) + return {}; + + // Use dynamic_cast for type dispatch — GraphicsBaseItem subclasses do not + // override QGraphicsItem::type(), so the GIT_* enum values are never returned. + if (GraphicsRectItem *rectItem = dynamic_cast(item)) { + bool isRound = rectItem->isRound(); + QJsonObject obj = serializeCommon(item, isRound ? "roundRect" : "rect"); + obj["width"] = shape->width(); + obj["height"] = shape->height(); + if (isRound) { + obj["ratioX"] = rectItem->ratioX(); + obj["ratioY"] = rectItem->ratioY(); + } + obj["pen"] = serializePen(shape->pen()); + obj["brush"] = serializeBrush(shape->brush()); + return obj; + } + + if (GraphicPolygonItem *poly = dynamic_cast(item)) { + QJsonObject obj = serializeCommon(item, "polygon"); + QJsonArray pts; + for (const QPointF &pt : poly->getPoints()) { + pts.append(QJsonObject{{"x", pt.x()}, {"y", pt.y()}}); + } + obj["points"] = pts; + obj["pen"] = serializePen(shape->pen()); + obj["brush"] = serializeBrush(shape->brush()); + return obj; + } + + qWarning("Document::serializeItem: unrecognized item"); + return {}; +} + +// -- Item deserialization -------------------------------------------------- + +static void applyCommon(QGraphicsItem *item, const QJsonObject &obj) +{ + QJsonObject pos = obj["pos"].toObject(); + item->setPos(pos["x"].toDouble(), pos["y"].toDouble()); + item->setRotation(obj["rotation"].toDouble()); + item->setZValue(obj["zValue"].toDouble()); +} + +template +static void applyPen(AbstractShapeType *shape, const QJsonObject &penObj) +{ + QPen pen; + pen.setColor(QColor(penObj["color"].toString())); + pen.setWidthF(penObj["width"].toDouble()); + pen.setStyle(penStyleFromString(penObj["style"].toString())); + shape->setPen(pen); +} + +template +static void applyBrush(AbstractShapeType *shape, const QJsonObject &brushObj) +{ + shape->setBrushColor(QColor(brushObj["color"].toString())); +} + +QGraphicsItem *Document::deserializeItem(const QJsonObject &obj) +{ + // Note: deserializeItem creates the item but does NOT add it to the scene. + // The caller (load() or a parent group) is responsible for scene addition. + QString type = obj["type"].toString(); + + if (type == "group") { + GraphicsItemGroup *group = new GraphicsItemGroup(); + applyCommon(group, obj); + + QJsonArray children = obj["children"].toArray(); + QList childItems; + for (const QJsonValue &val : children) { + QGraphicsItem *child = deserializeItem(val.toObject()); + if (child) + childItems.append(child); + } + + // Child positions in JSON are group-relative, but addToGroup() + // treats child pos() as scene-absolute. Convert before grouping. + for (QGraphicsItem *child : childItems) + child->setPos(group->mapToScene(child->pos())); + + group->addItems(childItems); + applyPen(group, obj["pen"].toObject()); + applyBrush(group, obj["brush"].toObject()); + group->setHandleVisible(false); + return group; + } + + if (type == "rect" || type == "roundRect") { + double w = obj["width"].toDouble(); + double h = obj["height"].toDouble(); + bool isRound = (type == "roundRect"); + GraphicsRectItem *item = new GraphicsRectItem( + QRectF(-w / 2, -h / 2, w, h).toRect(), isRound); + applyCommon(item, obj); + applyPen(item, obj["pen"].toObject()); + applyBrush(item, obj["brush"].toObject()); + if (isRound) { + item->setRatioX(obj["ratioX"].toDouble(0.1)); + item->setRatioY(obj["ratioY"].toDouble(0.1)); + } + item->updateCoordinate(); + item->setHandleVisible(false); + return item; + } + + if (type == "polygon") { + GraphicPolygonItem *item = new GraphicPolygonItem(); + QJsonArray pts = obj["points"].toArray(); + for (const QJsonValue &val : pts) { + QJsonObject pt = val.toObject(); + item->addPoint(QPointF(pt["x"].toDouble(), pt["y"].toDouble())); + } + item->endDrawing(); + applyCommon(item, obj); + applyPen(item, obj["pen"].toObject()); + applyBrush(item, obj["brush"].toObject()); + item->updateCoordinate(); + item->setHandleVisible(false); + return item; + } + + return nullptr; +} + +// -- Save / Load ----------------------------------------------------------- + +bool Document::save() +{ + if (m_sFilePath.isEmpty()) + return false; + return saveAs(m_sFilePath); +} + +bool Document::saveAs(const QString &filePath) +{ + QJsonArray items; + const auto sceneItems = m_pScene->items(); + for (QGraphicsItem *item : sceneItems) { + // Only serialize top-level items (children of groups are serialized recursively) + if (item->parentItem()) + continue; + QJsonObject obj = serializeItem(item); + if (!obj.isEmpty()) + items.append(obj); + } + + QJsonObject root; + root["version"] = "1.0"; + root["items"] = items; + + QFile file(filePath); + if (!file.open(QIODevice::WriteOnly)) + return false; + + file.write(QJsonDocument(root).toJson()); + file.close(); + + m_sFilePath = filePath; + m_nSavedIndex = m_pUndoStack->index(); + setModified(false); + emit filePathChanged(m_sFilePath); + emit saved(); + return true; +} + +bool Document::load(const QString &filePath) +{ + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) + return false; + + QByteArray data = file.readAll(); + file.close(); + + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) + return false; + + QJsonObject root = doc.object(); + + // Clear current state without triggering undo + m_nSavedIndex = 0; + m_pUndoStack->clear(); + setupNewScene(); + + QJsonArray items = root["items"].toArray(); + for (const QJsonValue &val : items) { + QGraphicsItem *item = deserializeItem(val.toObject()); + if (item) + m_pScene->addItem(item); + } + + m_pScene->clearSelection(); + + m_sFilePath = filePath; + m_nSavedIndex = 0; + setModified(false); + emit filePathChanged(m_sFilePath); + emit loaded(); + return true; +} diff --git a/source/drawingPanel.cpp b/source/drawingPanel.cpp index e51e3d8..fc505b2 100644 --- a/source/drawingPanel.cpp +++ b/source/drawingPanel.cpp @@ -10,14 +10,9 @@ DrawingPanel::DrawingPanel(QWidget *parent) { ui->setupUi(this); - m_pGraphicsScene = new DesignerScene(this); - //设置场景大小.前两个参数为scene的坐标远点,设置到view的中心点后,无论view如何缩放,secne的坐标原点都不会动,方便后续的位置计算 - m_pGraphicsScene->setSceneRect(-g_dGriaphicsScene_Width / 2, -g_dGriaphicsScene_Height / 2, g_dGriaphicsScene_Width, g_dGriaphicsScene_Height); - m_pGraphicsScene->setGridVisible(true); + m_pGraphicsScene = nullptr; m_pGraphicsView = new DesignerView(this); - m_pGraphicsView->setScene(m_pGraphicsScene); - m_pGraphicsScene->setView(m_pGraphicsView); ui->mainLayout->addWidget(m_pGraphicsView); } @@ -69,3 +64,13 @@ void DrawingPanel::onSignal_addGraphicsItem(GraphicsItemType& itemType) SelectorManager::getInstance()->setDrawGraphicsItem(itemType); } } + +void DrawingPanel::setScene(DesignerScene* scene) +{ + m_pGraphicsScene = scene; + m_pGraphicsScene->setSceneRect(-g_dGriaphicsScene_Width / 2, -g_dGriaphicsScene_Height / 2, + g_dGriaphicsScene_Width, g_dGriaphicsScene_Height); + m_pGraphicsScene->setGridVisible(true); + m_pGraphicsView->setScene(m_pGraphicsScene); + m_pGraphicsScene->setView(m_pGraphicsView); +} diff --git a/source/mainwindow.cpp b/source/mainwindow.cpp index a8e28c2..9069fdf 100644 --- a/source/mainwindow.cpp +++ b/source/mainwindow.cpp @@ -9,6 +9,9 @@ #include #include #include +#include +#include +#include #include "DockAreaWidget.h" #include "DockAreaTitleBar.h" @@ -26,6 +29,7 @@ #include "designerScene.h" #include "graphicElementsPanel.h" #include "operationCommand.h" +#include "document.h" using namespace ads; @@ -35,7 +39,7 @@ CMainWindow::CMainWindow(QWidget *parent) , ui(new Ui::CMainWindow) { ui->setupUi(this); - m_pUndoStack = nullptr; + m_pDocument = new Document(this); initializeDockUi(); initializeAction(); @@ -51,10 +55,12 @@ CMainWindow::~CMainWindow() void CMainWindow::closeEvent(QCloseEvent* event) { - // Delete dock manager here to delete all floating widgets. This ensures - // that all top level windows of the dock manager are properly closed + if (!maybeSave()) { + event->ignore(); + return; + } m_pDockManager->deleteLater(); - QMainWindow::closeEvent(event); + QMainWindow::closeEvent(event); } void CMainWindow::changeEvent(QEvent* event) @@ -72,7 +78,8 @@ void CMainWindow::initializeDockUi() // Set central widget m_pDrawingPanel = new DrawingPanel(); - DesignerScene* designerScene = m_pDrawingPanel->getDesignerScene(); + m_pDrawingPanel->setScene(m_pDocument->scene()); + DesignerScene* designerScene = m_pDocument->scene(); connect(designerScene, SIGNAL(signalAddItem(QGraphicsItem*)), this, SLOT(onSignal_addItem(QGraphicsItem*))); connect(designerScene, SIGNAL(selectionChanged()), this, SLOT(onSignal_selectionChanged())); CDockWidget* CentralDockWidget = new CDockWidget("CentralWidget"); @@ -98,7 +105,7 @@ void CMainWindow::initializeDockUi() propertiesDockWidget->setMinimumSizeHintMode(CDockWidget::MinimumSizeHintFromDockWidget); propertiesDockWidget->resize(550, 150); propertiesDockWidget->setMinimumSize(500,150); - m_pPropertiesEditorView->setObject(m_pDrawingPanel->getQGraphicsScene()); + m_pPropertiesEditorView->setObject(m_pDocument->scene()); m_pDockManager->addDockWidget(DockWidgetArea::RightDockWidgetArea, propertiesDockWidget, centralDockArea); ui->menuView->addAction(propertiesDockWidget->toggleViewAction()); } @@ -106,17 +113,15 @@ void CMainWindow::initializeDockUi() void CMainWindow::initializeAction() { //撤销、重做 - m_pUndoStack = new QUndoStack(this); - ui->actionUndo = m_pUndoStack->createUndoAction(this, tr("撤销")); + QUndoStack *undoStack = m_pDocument->undoStack(); + ui->actionUndo = undoStack->createUndoAction(this, tr("撤销")); ui->actionUndo->setIcon(QIcon::fromTheme(QString::fromUtf8("edit-undo"))); ui->actionUndo->setShortcuts(QKeySequence::Undo); - ui->actionRedo = m_pUndoStack->createRedoAction(this, tr("重做")); + ui->actionRedo = undoStack->createRedoAction(this, tr("重做")); ui->actionRedo->setIcon(QIcon::fromTheme(QString::fromUtf8("edit-redo"))); ui->actionRedo->setShortcuts(QKeySequence::Redo); ui->toolBar->addAction(ui->actionUndo); ui->toolBar->addAction(ui->actionRedo); - ui->actionUndo->setEnabled(m_pUndoStack->canUndo()); - ui->actionRedo->setEnabled(m_pUndoStack->canRedo()); ui->actionDelete->setShortcut(QKeySequence::Delete); @@ -126,6 +131,30 @@ void CMainWindow::initializeAction() connect(ui->actionZoomFit, SIGNAL(triggered()), this, SLOT(onAction_zoomFit())); connect(ui->actionGroup, SIGNAL(triggered()), this, SLOT(onAction_createGroup())); connect(ui->actionUngroup, SIGNAL(triggered()), this, SLOT(onAction_destroyGroup())); + + // File actions + connect(ui->actionNew, SIGNAL(triggered()), this, SLOT(onAction_new())); + connect(ui->actionOpen, SIGNAL(triggered()), this, SLOT(onAction_open())); + connect(ui->actionSave, SIGNAL(triggered()), this, SLOT(onAction_save())); + + // Save As — created in code since not in .ui + QAction *actionSaveAs = new QAction(QIcon::fromTheme("document-save-as"), tr("另存为..."), this); + actionSaveAs->setShortcut(QKeySequence::SaveAs); + ui->toolBar->addAction(actionSaveAs); + connect(actionSaveAs, SIGNAL(triggered()), this, SLOT(onAction_saveAs())); + + // Populate File menu + ui->menuFile->addAction(ui->actionNew); + ui->menuFile->addAction(ui->actionOpen); + ui->menuFile->addAction(ui->actionSave); + ui->menuFile->addAction(actionSaveAs); + ui->menuFile->addSeparator(); + ui->menuFile->addAction(ui->actionDelete); + + // Document signals -> window title + connect(m_pDocument, &Document::modifiedChanged, this, [this]() { updateWindowTitle(); }); + connect(m_pDocument, &Document::filePathChanged, this, [this](const QString &) { updateWindowTitle(); }); + updateWindowTitle(); } void CMainWindow::onAction_zoomIn() @@ -148,18 +177,18 @@ void CMainWindow::onAction_createGroup() GraphicsItemGroup* group = m_pDrawingPanel->createItemGroup(); if(group) { - QGraphicsScene* scene = m_pDrawingPanel->getQGraphicsScene(); + QGraphicsScene* scene = m_pDocument->scene(); QUndoCommand* createItemGropu = new CreateItemGoupCommand(group, scene); - m_pUndoStack->push(createItemGropu); + m_pDocument->execute(createItemGropu); } } void CMainWindow::onAction_destroyGroup() { - QGraphicsScene* scene = m_pDrawingPanel->getQGraphicsScene(); + QGraphicsScene* scene = m_pDocument->scene(); QList listItem = scene->selectedItems(); if(listItem.count() != 1) - return; //只能选择一个解组 + return; QGraphicsItem* item = listItem.first(); if(!item) @@ -169,7 +198,7 @@ void CMainWindow::onAction_destroyGroup() if(group) { QUndoCommand* destroyItemGropu = new DestroyItemGoupCommand(group, scene); - m_pUndoStack->push(destroyItemGropu); + m_pDocument->execute(destroyItemGropu); } } @@ -178,27 +207,111 @@ void CMainWindow::onSignal_addItem(QGraphicsItem* item) if(item) { QUndoCommand* addItemCommand = new AddItemCommand(item, item->scene()); - m_pUndoStack->push(addItemCommand); + m_pDocument->execute(addItemCommand); } } void CMainWindow::onSignal_deleteItem() { - QGraphicsScene* scene = m_pDrawingPanel->getQGraphicsScene(); + QGraphicsScene* scene = m_pDocument->scene(); if (scene && scene->selectedItems().isEmpty()) return; QUndoCommand* deleteItemCommand = new DeleteItemCommand(scene); - m_pUndoStack->push(deleteItemCommand); //push时会自动调用一次command的redo函数 + m_pDocument->execute(deleteItemCommand); } void CMainWindow::onSignal_selectionChanged() { - QList selectedItems = m_pDrawingPanel->getQGraphicsScene()->selectedItems(); + QList selectedItems = m_pDocument->scene()->selectedItems(); if(selectedItems.count() != 1) { - m_pPropertiesEditorView->setObject(m_pDrawingPanel->getQGraphicsScene()); + m_pPropertiesEditorView->setObject(m_pDocument->scene()); return; } GraphicsBaseItem *item = static_cast(selectedItems.first()); m_pPropertiesEditorView->setObject(static_cast(item)); } + +// -- File operations ------------------------------------------------------- + +bool CMainWindow::maybeSave() +{ + if (!m_pDocument->isModified()) + return true; + + QMessageBox::StandardButton ret = QMessageBox::warning( + this, tr("PowerDesigner"), + tr("当前文档已修改,是否保存?"), + QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); + + switch (ret) { + case QMessageBox::Save: + if (m_pDocument->filePath().isEmpty()) + onAction_saveAs(); + else + m_pDocument->save(); + return !m_pDocument->isModified(); + case QMessageBox::Cancel: + return false; + default: // Discard + break; + } + return true; +} + +void CMainWindow::updateWindowTitle() +{ + QString title = "PowerDesigner"; + QString path = m_pDocument->filePath(); + if (!path.isEmpty()) { + title += " - " + QFileInfo(path).fileName(); + } else { + title += " - " + tr("未命名"); + } + if (m_pDocument->isModified()) + title += " [*]"; + setWindowTitle(title); + setWindowModified(m_pDocument->isModified()); // shows macOS dot indicator +} + +void CMainWindow::onAction_new() +{ + if (!maybeSave()) + return; + m_pDocument->clear(); +} + +void CMainWindow::onAction_open() +{ + if (!maybeSave()) + return; + + QString filePath = QFileDialog::getOpenFileName( + this, tr("打开文件"), QString(), + tr("JSON 文件 (*.json);;所有文件 (*)")); + + if (filePath.isEmpty()) + return; + + m_pDocument->load(filePath); +} + +void CMainWindow::onAction_save() +{ + if (m_pDocument->filePath().isEmpty()) + onAction_saveAs(); + else + m_pDocument->save(); +} + +void CMainWindow::onAction_saveAs() +{ + QString filePath = QFileDialog::getSaveFileName( + this, tr("另存为"), QString(), + tr("JSON 文件 (*.json);;所有文件 (*)")); + + if (filePath.isEmpty()) + return; + + m_pDocument->saveAs(filePath); +} diff --git a/source/util/creatingSelector.cpp b/source/util/creatingSelector.cpp index cbccdf2..5e9ed75 100644 --- a/source/util/creatingSelector.cpp +++ b/source/util/creatingSelector.cpp @@ -138,7 +138,10 @@ void CreatingSelector::mouseReleaseEvent(QGraphicsSceneMouseEvent* event, Design else if (m_pCreatingItem && m_creatingMethod == CM_click && event->button() == Qt::RightButton) //右键结束绘制 { if(m_pCreatingItem->endDrawing()) + { m_pCreatingItem->updateCoordinate(); + emit scene->signalAddItem(m_pCreatingItem); + } else { m_pCreatingItem->setSelected(false); diff --git a/source/util/editingSelector.cpp b/source/util/editingSelector.cpp index eef3dfa..7e6b401 100644 --- a/source/util/editingSelector.cpp +++ b/source/util/editingSelector.cpp @@ -44,6 +44,7 @@ void EditingSelector::mouseReleaseEvent(QGraphicsSceneMouseEvent* event, Designe if(item && s_ptMouseLast != s_ptMouseDown) { item->updateCoordinate(); + scene->notifyDocumentModified(); } } diff --git a/source/util/movingSelector.cpp b/source/util/movingSelector.cpp index 15a0b52..17988a3 100644 --- a/source/util/movingSelector.cpp +++ b/source/util/movingSelector.cpp @@ -41,6 +41,9 @@ void MovingSelector::mouseReleaseEvent(QGraphicsSceneMouseEvent* event, Designer item->removeOperationCopy(); } + if (s_ptMouseLast != s_ptMouseDown) + scene->notifyDocumentModified(); + setCursor(scene, Qt::ArrowCursor); scene->callParentEvent(event); emit setWorkingSelector(ST_base); diff --git a/source/util/rotationSelector.cpp b/source/util/rotationSelector.cpp index e0f820c..1c0dce1 100644 --- a/source/util/rotationSelector.cpp +++ b/source/util/rotationSelector.cpp @@ -58,9 +58,11 @@ void RotationSelector::mouseReleaseEvent(QGraphicsSceneMouseEvent* event, Design { item->removeOperationCopy(); } - } + if (s_ptMouseLast != s_ptMouseDown) + scene->notifyDocumentModified(); + s_nDragHandle = H_none; setCursor(scene, Qt::ArrowCursor); emit setWorkingSelector(ST_base); diff --git a/source/util/scalingSelector.cpp b/source/util/scalingSelector.cpp index 66d6ffc..9ac7cad 100644 --- a/source/util/scalingSelector.cpp +++ b/source/util/scalingSelector.cpp @@ -60,6 +60,7 @@ void ScalingSelector::mouseReleaseEvent(QGraphicsSceneMouseEvent* event, Designe if(item && s_ptMouseLast != s_ptMouseDown) { item->updateCoordinate(); + scene->notifyDocumentModified(); } }