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 | |
63 | MainWindow::MainWindow(QWidget *parent) |
64 | : QMainWindow(parent) |
65 | { |
66 | setupUi(this); |
67 | |
68 | QWidget *w = documentTabs->widget(index: 0); |
69 | documentTabs->removeTab(index: 0); |
70 | delete w; |
71 | |
72 | connect(sender: actionOpen, signal: &QAction::triggered, receiver: this, slot: &MainWindow::openDocument); |
73 | connect(sender: actionClose, signal: &QAction::triggered, receiver: this, slot: &MainWindow::closeDocument); |
74 | connect(sender: actionNew, signal: &QAction::triggered, receiver: this, slot: &MainWindow::newDocument); |
75 | connect(sender: actionSave, signal: &QAction::triggered, receiver: this, slot: &MainWindow::saveDocument); |
76 | connect(sender: actionExit, signal: &QAction::triggered, receiver: this, slot: &QWidget::close); |
77 | connect(sender: actionRed, signal: &QAction::triggered, receiver: this, slot: &MainWindow::setShapeColor); |
78 | connect(sender: actionGreen, signal: &QAction::triggered, receiver: this, slot: &MainWindow::setShapeColor); |
79 | connect(sender: actionBlue, signal: &QAction::triggered, receiver: this, slot: &MainWindow::setShapeColor); |
80 | connect(sender: actionAddCircle, signal: &QAction::triggered, receiver: this, slot: &MainWindow::addShape); |
81 | connect(sender: actionAddRectangle, signal: &QAction::triggered, receiver: this, slot: &MainWindow::addShape); |
82 | connect(sender: actionAddTriangle, signal: &QAction::triggered, receiver: this, slot: &MainWindow::addShape); |
83 | connect(sender: actionRemoveShape, signal: &QAction::triggered, receiver: this, slot: &MainWindow::removeShape); |
84 | connect(sender: actionAddRobot, signal: &QAction::triggered, receiver: this, slot: &MainWindow::addRobot); |
85 | connect(sender: actionAddSnowman, signal: &QAction::triggered, receiver: this, slot: &MainWindow::addSnowman); |
86 | connect(sender: actionAbout, signal: &QAction::triggered, receiver: this, slot: &MainWindow::about); |
87 | connect(sender: actionAboutQt, signal: &QAction::triggered, receiver: this, slot: &MainWindow::aboutQt); |
88 | |
89 | connect(sender: undoLimit, signal: QOverload<int>::of(ptr: &QSpinBox::valueChanged), receiver: this, slot: &MainWindow::updateActions); |
90 | connect(sender: documentTabs, signal: &QTabWidget::currentChanged, receiver: this, slot: &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(parent: this); |
110 | QAction *redoAction = m_undoGroup->createRedoAction(parent: this); |
111 | undoAction->setIcon(QIcon(":/icons/undo.png" )); |
112 | redoAction->setIcon(QIcon(":/icons/redo.png" )); |
113 | menuShape->insertAction(before: menuShape->actions().at(i: 0), action: undoAction); |
114 | menuShape->insertAction(before: undoAction, action: redoAction); |
115 | |
116 | toolBar->addAction(action: undoAction); |
117 | toolBar->addAction(action: redoAction); |
118 | |
119 | newDocument(); |
120 | updateActions(); |
121 | }; |
122 | |
123 | void 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(widget: doc); |
153 | Q_ASSERT(index != -1); |
154 | static const QIcon unsavedIcon(":/icons/filesave.png" ); |
155 | documentTabs->setTabIcon(index, icon: doc->undoStack()->isClean() ? QIcon() : unsavedIcon); |
156 | |
157 | if (doc->undoStack()->count() == 0) |
158 | doc->undoStack()->setUndoLimit(undoLimit->value()); |
159 | } |
160 | } |
161 | |
162 | void MainWindow::openDocument() |
163 | { |
164 | QString fileName = QFileDialog::getOpenFileName(parent: this); |
165 | if (fileName.isEmpty()) |
166 | return; |
167 | |
168 | QFile file(fileName); |
169 | if (!file.open(flags: QIODevice::ReadOnly)) { |
170 | QMessageBox::warning(parent: this, |
171 | title: tr(s: "File error" ), |
172 | text: tr(s: "Failed to open\n%1" ).arg(a: fileName)); |
173 | return; |
174 | } |
175 | QTextStream stream(&file); |
176 | |
177 | Document *doc = new Document(); |
178 | if (!doc->load(stream)) { |
179 | QMessageBox::warning(parent: this, |
180 | title: tr(s: "Parse error" ), |
181 | text: tr(s: "Failed to parse\n%1" ).arg(a: fileName)); |
182 | delete doc; |
183 | return; |
184 | } |
185 | |
186 | doc->setFileName(fileName); |
187 | addDocument(doc); |
188 | } |
189 | |
190 | QString MainWindow::fixedWindowTitle(const Document *doc) const |
191 | { |
192 | QString title = doc->fileName(); |
193 | |
194 | if (title.isEmpty()) |
195 | title = tr(s: "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(index: j); |
209 | if (widget == doc) |
210 | continue; |
211 | if (result == documentTabs->tabText(index: j)) { |
212 | unique = false; |
213 | break; |
214 | } |
215 | } |
216 | |
217 | if (unique) |
218 | break; |
219 | } |
220 | |
221 | return result; |
222 | } |
223 | |
224 | void MainWindow::addDocument(Document *doc) |
225 | { |
226 | if (documentTabs->indexOf(widget: doc) != -1) |
227 | return; |
228 | m_undoGroup->addStack(stack: doc->undoStack()); |
229 | documentTabs->addTab(widget: doc, fixedWindowTitle(doc)); |
230 | connect(sender: doc, signal: &Document::currentShapeChanged, receiver: this, slot: &MainWindow::updateActions); |
231 | connect(sender: doc->undoStack(), signal: &QUndoStack::indexChanged, receiver: this, slot: &MainWindow::updateActions); |
232 | connect(sender: doc->undoStack(), signal: &QUndoStack::cleanChanged, receiver: this, slot: &MainWindow::updateActions); |
233 | |
234 | setCurrentDocument(doc); |
235 | } |
236 | |
237 | void MainWindow::setCurrentDocument(Document *doc) |
238 | { |
239 | documentTabs->setCurrentWidget(doc); |
240 | } |
241 | |
242 | Document *MainWindow::currentDocument() const |
243 | { |
244 | return qobject_cast<Document*>(object: documentTabs->currentWidget()); |
245 | } |
246 | |
247 | void MainWindow::removeDocument(Document *doc) |
248 | { |
249 | int index = documentTabs->indexOf(widget: doc); |
250 | if (index == -1) |
251 | return; |
252 | |
253 | documentTabs->removeTab(index); |
254 | m_undoGroup->removeStack(stack: doc->undoStack()); |
255 | disconnect(sender: doc, signal: &Document::currentShapeChanged, receiver: this, slot: &MainWindow::updateActions); |
256 | disconnect(sender: doc->undoStack(), signal: &QUndoStack::indexChanged, receiver: this, slot: &MainWindow::updateActions); |
257 | disconnect(sender: doc->undoStack(), signal: &QUndoStack::cleanChanged, receiver: this, slot: &MainWindow::updateActions); |
258 | |
259 | if (documentTabs->count() == 0) { |
260 | newDocument(); |
261 | updateActions(); |
262 | } |
263 | } |
264 | |
265 | void 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(parent: this); |
276 | if (fileName.isEmpty()) |
277 | break; |
278 | |
279 | QFile file(fileName); |
280 | if (!file.open(flags: QIODevice::WriteOnly)) { |
281 | QMessageBox::warning(parent: this, |
282 | title: tr(s: "File error" ), |
283 | text: tr(s: "Failed to open\n%1" ).arg(a: 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(widget: doc); |
291 | Q_ASSERT(index != -1); |
292 | documentTabs->setTabText(index, text: fixedWindowTitle(doc)); |
293 | |
294 | break; |
295 | } |
296 | } |
297 | } |
298 | |
299 | void 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(parent: this, |
308 | title: tr(s: "Unsaved changes" ), |
309 | text: tr(s: "Would you like to save this document?" ), |
310 | button0: QMessageBox::Yes, button1: QMessageBox::No); |
311 | if (button == QMessageBox::Yes) |
312 | saveDocument(); |
313 | } |
314 | |
315 | removeDocument(doc); |
316 | delete doc; |
317 | } |
318 | |
319 | void MainWindow::newDocument() |
320 | { |
321 | addDocument(doc: new Document()); |
322 | } |
323 | |
324 | static QColor randomColor() |
325 | { |
326 | int r = QRandomGenerator::global()->bounded(highest: 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 | |
338 | static QRect randomRect(const QSize &s) |
339 | { |
340 | QSize min = Shape::minSize; |
341 | |
342 | int left = qRound(d: (s.width() - min.width()) * (QRandomGenerator::global()->bounded(highest: 1.0))); |
343 | int top = qRound(d: (s.height() - min.height()) * (QRandomGenerator::global()->bounded(highest: 1.0))); |
344 | int width = qRound(d: (s.width() - left - min.width()) * (QRandomGenerator::global()->bounded(highest: 1.0))) + min.width(); |
345 | int height = qRound(d: (s.height() - top - min.height()) * (QRandomGenerator::global()->bounded(highest: 1.0))) + min.height(); |
346 | |
347 | return QRect(left, top, width, height); |
348 | } |
349 | |
350 | void 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(s: doc->size())); |
367 | doc->undoStack()->push(cmd: new AddShapeCommand(doc, newShape)); |
368 | } |
369 | |
370 | void 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(cmd: new RemoveShapeCommand(doc, shapeName)); |
381 | } |
382 | |
383 | void 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(cmd: new SetShapeColorCommand(doc, shapeName, color)); |
408 | } |
409 | |
410 | void 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(text: tr(s: "Add snowman" )); |
419 | doc->undoStack()->push(cmd: new AddShapeCommand(doc, |
420 | Shape(Shape::Circle, Qt::blue, QRect(51, 30, 97, 95)))); |
421 | doc->undoStack()->push(cmd: new AddShapeCommand(doc, |
422 | Shape(Shape::Circle, Qt::blue, QRect(27, 123, 150, 133)))); |
423 | doc->undoStack()->push(cmd: new AddShapeCommand(doc, |
424 | Shape(Shape::Circle, Qt::blue, QRect(11, 253, 188, 146)))); |
425 | doc->undoStack()->endMacro(); |
426 | } |
427 | |
428 | void 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(s: "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(cmd: parent); |
447 | } |
448 | |
449 | void MainWindow::about() |
450 | { |
451 | QMessageBox::about(parent: this, title: tr(s: "About Undo" ), text: tr(s: "The Undo demonstration shows how to use the Qt Undo framework." )); |
452 | } |
453 | |
454 | void MainWindow::aboutQt() |
455 | { |
456 | QMessageBox::aboutQt(parent: this, title: tr(s: "About Qt" )); |
457 | } |
458 | |