1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the demonstration applications of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:BSD$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** BSD License Usage
18** Alternatively, you may use this file under the terms of the BSD license
19** as follows:
20**
21** "Redistribution and use in source and binary forms, with or without
22** modification, are permitted provided that the following conditions are
23** met:
24** * Redistributions of source code must retain the above copyright
25** notice, this list of conditions and the following disclaimer.
26** * Redistributions in binary form must reproduce the above copyright
27** notice, this list of conditions and the following disclaimer in
28** the documentation and/or other materials provided with the
29** distribution.
30** * Neither the name of The Qt Company Ltd nor the names of its
31** contributors may be used to endorse or promote products derived
32** from this software without specific prior written permission.
33**
34**
35** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
36** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
37** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
38** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
39** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
40** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
41** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
42** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
43** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
44** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
45** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
46**
47** $QT_END_LICENSE$
48**
49****************************************************************************/
50
51#include "mainwindow.h"
52#include "document.h"
53#include "commands.h"
54
55#include <QUndoGroup>
56#include <QUndoStack>
57#include <QFileDialog>
58#include <QMessageBox>
59#include <QRandomGenerator>
60#include <QTextStream>
61#include <QToolButton>
62
63MainWindow::MainWindow(QWidget *parent)
64 : QMainWindow(parent)
65{
66 setupUi(this);
67
68 QWidget *w = documentTabs->widget(0);
69 documentTabs->removeTab(0);
70 delete w;
71
72 connect(actionOpen, &QAction::triggered, this, &MainWindow::openDocument);
73 connect(actionClose, &QAction::triggered, this, &MainWindow::closeDocument);
74 connect(actionNew, &QAction::triggered, this, &MainWindow::newDocument);
75 connect(actionSave, &QAction::triggered, this, &MainWindow::saveDocument);
76 connect(actionExit, &QAction::triggered, this, &QWidget::close);
77 connect(actionRed, &QAction::triggered, this, &MainWindow::setShapeColor);
78 connect(actionGreen, &QAction::triggered, this, &MainWindow::setShapeColor);
79 connect(actionBlue, &QAction::triggered, this, &MainWindow::setShapeColor);
80 connect(actionAddCircle, &QAction::triggered, this, &MainWindow::addShape);
81 connect(actionAddRectangle, &QAction::triggered, this, &MainWindow::addShape);
82 connect(actionAddTriangle, &QAction::triggered, this, &MainWindow::addShape);
83 connect(actionRemoveShape, &QAction::triggered, this, &MainWindow::removeShape);
84 connect(actionAddRobot, &QAction::triggered, this, &MainWindow::addRobot);
85 connect(actionAddSnowman, &QAction::triggered, this, &MainWindow::addSnowman);
86 connect(actionAbout, &QAction::triggered, this, &MainWindow::about);
87 connect(actionAboutQt, &QAction::triggered, this, &MainWindow::aboutQt);
88
89 connect(undoLimit, &QSpinBox::valueChanged, this, &MainWindow::updateActions);
90 connect(documentTabs, &QTabWidget::currentChanged, this, &MainWindow::updateActions);
91
92 actionOpen->setShortcut(QString("Ctrl+O"));
93 actionClose->setShortcut(QString("Ctrl+W"));
94 actionNew->setShortcut(QString("Ctrl+N"));
95 actionSave->setShortcut(QString("Ctrl+S"));
96 actionExit->setShortcut(QString("Ctrl+Q"));
97 actionRemoveShape->setShortcut(QString("Del"));
98 actionRed->setShortcut(QString("Alt+R"));
99 actionGreen->setShortcut(QString("Alt+G"));
100 actionBlue->setShortcut(QString("Alt+B"));
101 actionAddCircle->setShortcut(QString("Alt+C"));
102 actionAddRectangle->setShortcut(QString("Alt+L"));
103 actionAddTriangle->setShortcut(QString("Alt+T"));
104
105 m_undoGroup = new QUndoGroup(this);
106 undoView->setGroup(m_undoGroup);
107 undoView->setCleanIcon(QIcon(":/icons/ok.png"));
108
109 QAction *undoAction = m_undoGroup->createUndoAction(this);
110 QAction *redoAction = m_undoGroup->createRedoAction(this);
111 undoAction->setIcon(QIcon(":/icons/undo.png"));
112 redoAction->setIcon(QIcon(":/icons/redo.png"));
113 menuShape->insertAction(menuShape->actions().at(0), undoAction);
114 menuShape->insertAction(undoAction, redoAction);
115
116 toolBar->addAction(undoAction);
117 toolBar->addAction(redoAction);
118
119 newDocument();
120 updateActions();
121};
122
123void MainWindow::updateActions()
124{
125 Document *doc = currentDocument();
126 m_undoGroup->setActiveStack(doc == nullptr ? nullptr : doc->undoStack());
127 QString shapeName = doc == nullptr ? QString() : doc->currentShapeName();
128
129 actionAddRobot->setEnabled(doc != nullptr);
130 actionAddSnowman->setEnabled(doc != nullptr);
131 actionAddCircle->setEnabled(doc != nullptr);
132 actionAddRectangle->setEnabled(doc != nullptr);
133 actionAddTriangle->setEnabled(doc != nullptr);
134 actionClose->setEnabled(doc != nullptr);
135 actionSave->setEnabled(doc != nullptr && !doc->undoStack()->isClean());
136 undoLimit->setEnabled(doc != nullptr && doc->undoStack()->count() == 0);
137
138 if (shapeName.isEmpty()) {
139 actionRed->setEnabled(false);
140 actionGreen->setEnabled(false);
141 actionBlue->setEnabled(false);
142 actionRemoveShape->setEnabled(false);
143 } else {
144 Shape shape = doc->shape(shapeName);
145 actionRed->setEnabled(shape.color() != Qt::red);
146 actionGreen->setEnabled(shape.color() != Qt::green);
147 actionBlue->setEnabled(shape.color() != Qt::blue);
148 actionRemoveShape->setEnabled(true);
149 }
150
151 if (doc != nullptr) {
152 int index = documentTabs->indexOf(doc);
153 Q_ASSERT(index != -1);
154 static const QIcon unsavedIcon(":/icons/filesave.png");
155 documentTabs->setTabIcon(index, doc->undoStack()->isClean() ? QIcon() : unsavedIcon);
156
157 if (doc->undoStack()->count() == 0)
158 doc->undoStack()->setUndoLimit(undoLimit->value());
159 }
160}
161
162void MainWindow::openDocument()
163{
164 QString fileName = QFileDialog::getOpenFileName(this);
165 if (fileName.isEmpty())
166 return;
167
168 QFile file(fileName);
169 if (!file.open(QIODevice::ReadOnly)) {
170 QMessageBox::warning(this,
171 tr("File error"),
172 tr("Failed to open\n%1").arg(fileName));
173 return;
174 }
175 QTextStream stream(&file);
176
177 Document *doc = new Document();
178 if (!doc->load(stream)) {
179 QMessageBox::warning(this,
180 tr("Parse error"),
181 tr("Failed to parse\n%1").arg(fileName));
182 delete doc;
183 return;
184 }
185
186 doc->setFileName(fileName);
187 addDocument(doc);
188}
189
190QString MainWindow::fixedWindowTitle(const Document *doc) const
191{
192 QString title = doc->fileName();
193
194 if (title.isEmpty())
195 title = tr("Unnamed");
196 else
197 title = QFileInfo(title).fileName();
198
199 QString result;
200
201 for (int i = 0; ; ++i) {
202 result = title;
203 if (i > 0)
204 result += QString::number(i);
205
206 bool unique = true;
207 for (int j = 0; j < documentTabs->count(); ++j) {
208 const QWidget *widget = documentTabs->widget(j);
209 if (widget == doc)
210 continue;
211 if (result == documentTabs->tabText(j)) {
212 unique = false;
213 break;
214 }
215 }
216
217 if (unique)
218 break;
219 }
220
221 return result;
222}
223
224void MainWindow::addDocument(Document *doc)
225{
226 if (documentTabs->indexOf(doc) != -1)
227 return;
228 m_undoGroup->addStack(doc->undoStack());
229 documentTabs->addTab(doc, fixedWindowTitle(doc));
230 connect(doc, &Document::currentShapeChanged, this, &MainWindow::updateActions);
231 connect(doc->undoStack(), &QUndoStack::indexChanged, this, &MainWindow::updateActions);
232 connect(doc->undoStack(), &QUndoStack::cleanChanged, this, &MainWindow::updateActions);
233
234 setCurrentDocument(doc);
235}
236
237void MainWindow::setCurrentDocument(Document *doc)
238{
239 documentTabs->setCurrentWidget(doc);
240}
241
242Document *MainWindow::currentDocument() const
243{
244 return qobject_cast<Document*>(documentTabs->currentWidget());
245}
246
247void MainWindow::removeDocument(Document *doc)
248{
249 int index = documentTabs->indexOf(doc);
250 if (index == -1)
251 return;
252
253 documentTabs->removeTab(index);
254 m_undoGroup->removeStack(doc->undoStack());
255 disconnect(doc, &Document::currentShapeChanged, this, &MainWindow::updateActions);
256 disconnect(doc->undoStack(), &QUndoStack::indexChanged, this, &MainWindow::updateActions);
257 disconnect(doc->undoStack(), &QUndoStack::cleanChanged, this, &MainWindow::updateActions);
258
259 if (documentTabs->count() == 0) {
260 newDocument();
261 updateActions();
262 }
263}
264
265void MainWindow::saveDocument()
266{
267 Document *doc = currentDocument();
268 if (doc == nullptr)
269 return;
270
271 for (;;) {
272 QString fileName = doc->fileName();
273
274 if (fileName.isEmpty())
275 fileName = QFileDialog::getSaveFileName(this);
276 if (fileName.isEmpty())
277 break;
278
279 QFile file(fileName);
280 if (!file.open(QIODevice::WriteOnly)) {
281 QMessageBox::warning(this,
282 tr("File error"),
283 tr("Failed to open\n%1").arg(fileName));
284 doc->setFileName(QString());
285 } else {
286 QTextStream stream(&file);
287 doc->save(stream);
288 doc->setFileName(fileName);
289
290 int index = documentTabs->indexOf(doc);
291 Q_ASSERT(index != -1);
292 documentTabs->setTabText(index, fixedWindowTitle(doc));
293
294 break;
295 }
296 }
297}
298
299void MainWindow::closeDocument()
300{
301 Document *doc = currentDocument();
302 if (doc == nullptr)
303 return;
304
305 if (!doc->undoStack()->isClean()) {
306 int button
307 = QMessageBox::warning(this,
308 tr("Unsaved changes"),
309 tr("Would you like to save this document?"),
310 QMessageBox::Yes, QMessageBox::No);
311 if (button == QMessageBox::Yes)
312 saveDocument();
313 }
314
315 removeDocument(doc);
316 delete doc;
317}
318
319void MainWindow::newDocument()
320{
321 addDocument(new Document());
322}
323
324static QColor randomColor()
325{
326 int r = QRandomGenerator::global()->bounded(3);
327 switch (r) {
328 case 0:
329 return Qt::red;
330 case 1:
331 return Qt::green;
332 default:
333 break;
334 }
335 return Qt::blue;
336}
337
338static QRect randomRect(const QSize &s)
339{
340 QSize min = Shape::minSize;
341
342 int left = qRound((s.width() - min.width()) * (QRandomGenerator::global()->bounded(1.0)));
343 int top = qRound((s.height() - min.height()) * (QRandomGenerator::global()->bounded(1.0)));
344 int width = qRound((s.width() - left - min.width()) * (QRandomGenerator::global()->bounded(1.0))) + min.width();
345 int height = qRound((s.height() - top - min.height()) * (QRandomGenerator::global()->bounded(1.0))) + min.height();
346
347 return QRect(left, top, width, height);
348}
349
350void MainWindow::addShape()
351{
352 Document *doc = currentDocument();
353 if (doc == nullptr)
354 return;
355
356 Shape::Type type;
357
358 if (sender() == actionAddCircle)
359 type = Shape::Circle;
360 else if (sender() == actionAddRectangle)
361 type = Shape::Rectangle;
362 else if (sender() == actionAddTriangle)
363 type = Shape::Triangle;
364 else return;
365
366 Shape newShape(type, randomColor(), randomRect(doc->size()));
367 doc->undoStack()->push(new AddShapeCommand(doc, newShape));
368}
369
370void MainWindow::removeShape()
371{
372 Document *doc = currentDocument();
373 if (doc == nullptr)
374 return;
375
376 QString shapeName = doc->currentShapeName();
377 if (shapeName.isEmpty())
378 return;
379
380 doc->undoStack()->push(new RemoveShapeCommand(doc, shapeName));
381}
382
383void MainWindow::setShapeColor()
384{
385 Document *doc = currentDocument();
386 if (doc == nullptr)
387 return;
388
389 QString shapeName = doc->currentShapeName();
390 if (shapeName.isEmpty())
391 return;
392
393 QColor color;
394
395 if (sender() == actionRed)
396 color = Qt::red;
397 else if (sender() == actionGreen)
398 color = Qt::green;
399 else if (sender() == actionBlue)
400 color = Qt::blue;
401 else
402 return;
403
404 if (color == doc->shape(shapeName).color())
405 return;
406
407 doc->undoStack()->push(new SetShapeColorCommand(doc, shapeName, color));
408}
409
410void MainWindow::addSnowman()
411{
412 Document *doc = currentDocument();
413 if (doc == nullptr)
414 return;
415
416 // Create a macro command using beginMacro() and endMacro()
417
418 doc->undoStack()->beginMacro(tr("Add snowman"));
419 doc->undoStack()->push(new AddShapeCommand(doc,
420 Shape(Shape::Circle, Qt::blue, QRect(51, 30, 97, 95))));
421 doc->undoStack()->push(new AddShapeCommand(doc,
422 Shape(Shape::Circle, Qt::blue, QRect(27, 123, 150, 133))));
423 doc->undoStack()->push(new AddShapeCommand(doc,
424 Shape(Shape::Circle, Qt::blue, QRect(11, 253, 188, 146))));
425 doc->undoStack()->endMacro();
426}
427
428void MainWindow::addRobot()
429{
430 Document *doc = currentDocument();
431 if (doc == nullptr)
432 return;
433
434 // Compose a macro command by explicitly adding children to a parent command
435
436 QUndoCommand *parent = new QUndoCommand(tr("Add robot"));
437
438 new AddShapeCommand(doc, Shape(Shape::Rectangle, Qt::green, QRect(115, 15, 81, 70)), parent);
439 new AddShapeCommand(doc, Shape(Shape::Rectangle, Qt::green, QRect(82, 89, 148, 188)), parent);
440 new AddShapeCommand(doc, Shape(Shape::Rectangle, Qt::green, QRect(76, 280, 80, 165)), parent);
441 new AddShapeCommand(doc, Shape(Shape::Rectangle, Qt::green, QRect(163, 280, 80, 164)), parent);
442 new AddShapeCommand(doc, Shape(Shape::Circle, Qt::blue, QRect(116, 25, 80, 50)), parent);
443 new AddShapeCommand(doc, Shape(Shape::Rectangle, Qt::green, QRect(232, 92, 80, 127)), parent);
444 new AddShapeCommand(doc, Shape(Shape::Rectangle, Qt::green, QRect(2, 92, 80, 125)), parent);
445
446 doc->undoStack()->push(parent);
447}
448
449void MainWindow::about()
450{
451 QMessageBox::about(this, tr("About Undo"), tr("The Undo demonstration shows how to use the Qt Undo framework."));
452}
453
454void MainWindow::aboutQt()
455{
456 QMessageBox::aboutQt(this, tr("About Qt"));
457}
458