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 "document.h"
52#include "commands.h"
53
54#include <QPainter>
55#include <QPaintEvent>
56#include <QTextStream>
57#include <QUndoStack>
58
59static constexpr int resizeHandleWidth = 6;
60
61/******************************************************************************
62** Shape
63*/
64
65const QSize Shape::minSize(80, 50);
66
67Shape::Shape(Type type, const QColor &color, const QRect &rect)
68 : m_type(type), m_rect(rect), m_color(color)
69{
70}
71
72Shape::Type Shape::type() const
73{
74 return m_type;
75}
76
77QRect Shape::rect() const
78{
79 return m_rect;
80}
81
82QColor Shape::color() const
83{
84 return m_color;
85}
86
87QString Shape::name() const
88{
89 return m_name;
90}
91
92QRect Shape::resizeHandle() const
93{
94 QPoint br = m_rect.bottomRight();
95 return QRect(br - QPoint(resizeHandleWidth, resizeHandleWidth), br);
96}
97
98QString Shape::typeToString(Type type)
99{
100 switch (type) {
101 case Rectangle:
102 return QLatin1String("Rectangle");
103 case Circle:
104 return QLatin1String("Circle");
105 case Triangle:
106 return QLatin1String("Triangle");
107 }
108
109 return QString();
110}
111
112Shape::Type Shape::stringToType(const QString &s, bool *ok)
113{
114 if (ok != nullptr)
115 *ok = true;
116
117 if (s == QLatin1String("Rectangle"))
118 return Rectangle;
119 if (s == QLatin1String("Circle"))
120 return Circle;
121 if (s == QLatin1String("Triangle"))
122 return Triangle;
123
124 if (ok != nullptr)
125 *ok = false;
126 return Rectangle;
127}
128
129/******************************************************************************
130** Document
131*/
132
133Document::Document(QWidget *parent)
134 : QWidget(parent), m_undoStack(new QUndoStack(this))
135{
136 setAutoFillBackground(true);
137 setBackgroundRole(QPalette::Base);
138
139 QPalette pal = palette();
140 pal.setBrush(QPalette::Base, QPixmap(":/icons/background.png"));
141 pal.setColor(QPalette::HighlightedText, Qt::red);
142 setPalette(pal);
143}
144
145QString Document::addShape(const Shape &shape)
146{
147 QString name = Shape::typeToString(shape.type());
148 name = uniqueName(name);
149
150 m_shapeList.append(shape);
151 m_shapeList[m_shapeList.count() - 1].m_name = name;
152 setCurrentShape(m_shapeList.count() - 1);
153
154 return name;
155}
156
157void Document::deleteShape(const QString &shapeName)
158{
159 int index = indexOf(shapeName);
160 if (index == -1)
161 return;
162
163 update(m_shapeList.at(index).rect());
164
165 m_shapeList.removeAt(index);
166
167 if (index <= m_currentIndex) {
168 m_currentIndex = -1;
169 if (index == m_shapeList.count())
170 --index;
171 setCurrentShape(index);
172 }
173}
174
175Shape Document::shape(const QString &shapeName) const
176{
177 int index = indexOf(shapeName);
178 if (index == -1)
179 return Shape();
180 return m_shapeList.at(index);
181}
182
183void Document::setShapeRect(const QString &shapeName, const QRect &rect)
184{
185 int index = indexOf(shapeName);
186 if (index == -1)
187 return;
188
189 Shape &shape = m_shapeList[index];
190
191 update(shape.rect());
192 update(rect);
193
194 shape.m_rect = rect;
195}
196
197void Document::setShapeColor(const QString &shapeName, const QColor &color)
198{
199
200 int index = indexOf(shapeName);
201 if (index == -1)
202 return;
203
204 Shape &shape = m_shapeList[index];
205 shape.m_color = color;
206
207 update(shape.rect());
208}
209
210QUndoStack *Document::undoStack() const
211{
212 return m_undoStack;
213}
214
215bool Document::load(QTextStream &stream)
216{
217 m_shapeList.clear();
218
219 while (!stream.atEnd()) {
220 QString shapeType, shapeName, colorName;
221 int left, top, width, height;
222 stream >> shapeType >> shapeName >> colorName >> left >> top >> width >> height;
223 if (stream.status() != QTextStream::Ok)
224 return false;
225 bool ok;
226 Shape::Type type = Shape::stringToType(shapeType, &ok);
227 if (!ok)
228 return false;
229 QColor color(colorName);
230 if (!color.isValid())
231 return false;
232
233 Shape shape(type);
234 shape.m_name = shapeName;
235 shape.m_color = color;
236 shape.m_rect = QRect(left, top, width, height);
237
238 m_shapeList.append(shape);
239 }
240
241 m_currentIndex = m_shapeList.isEmpty() ? -1 : 0;
242
243 return true;
244}
245
246void Document::save(QTextStream &stream)
247{
248 for (int i = 0; i < m_shapeList.count(); ++i) {
249 const Shape &shape = m_shapeList.at(i);
250 QRect r = shape.rect();
251 stream << Shape::typeToString(shape.type()) << QLatin1Char(' ')
252 << shape.name() << QLatin1Char(' ')
253 << shape.color().name() << QLatin1Char(' ')
254 << r.left() << QLatin1Char(' ')
255 << r.top() << QLatin1Char(' ')
256 << r.width() << QLatin1Char(' ')
257 << r.height();
258 if (i != m_shapeList.count() - 1)
259 stream << QLatin1Char('\n');
260 }
261 m_undoStack->setClean();
262}
263
264QString Document::fileName() const
265{
266 return m_fileName;
267}
268
269void Document::setFileName(const QString &fileName)
270{
271 m_fileName = fileName;
272}
273
274int Document::indexAt(const QPoint &pos) const
275{
276 for (int i = m_shapeList.count() - 1; i >= 0; --i) {
277 if (m_shapeList.at(i).rect().contains(pos))
278 return i;
279 }
280 return -1;
281}
282
283void Document::mousePressEvent(QMouseEvent *event)
284{
285 event->accept();
286 int index = indexAt(event->position().toPoint());;
287 if (index != -1) {
288 setCurrentShape(index);
289
290 const Shape &shape = m_shapeList.at(index);
291 m_resizeHandlePressed = shape.resizeHandle().contains(event->position().toPoint());
292
293 if (m_resizeHandlePressed)
294 m_mousePressOffset = shape.rect().bottomRight() - event->position().toPoint();
295 else
296 m_mousePressOffset = event->position().toPoint() - shape.rect().topLeft();
297 }
298 m_mousePressIndex = index;
299}
300
301void Document::mouseReleaseEvent(QMouseEvent *event)
302{
303 event->accept();
304 m_mousePressIndex = -1;
305}
306
307void Document::mouseMoveEvent(QMouseEvent *event)
308{
309 event->accept();
310
311 if (m_mousePressIndex == -1)
312 return;
313
314 const Shape &shape = m_shapeList.at(m_mousePressIndex);
315
316 QRect rect;
317 if (m_resizeHandlePressed) {
318 rect = QRect(shape.rect().topLeft(), event->position().toPoint() + m_mousePressOffset);
319 } else {
320 rect = shape.rect();
321 rect.moveTopLeft(event->position().toPoint() - m_mousePressOffset);
322 }
323
324 QSize size = rect.size().expandedTo(Shape::minSize);
325 rect.setSize(size);
326
327 m_undoStack->push(new SetShapeRectCommand(this, shape.name(), rect));
328}
329
330static QGradient gradient(const QColor &color, const QRect &rect)
331{
332 QColor c = color;
333 c.setAlpha(160);
334 QLinearGradient result(rect.topLeft(), rect.bottomRight());
335 result.setColorAt(0, c.darker(150));
336 result.setColorAt(0.5, c.lighter(200));
337 result.setColorAt(1, c.darker(150));
338 return result;
339}
340
341static QPolygon triangle(const QRect &rect)
342{
343 QPolygon result(3);
344 result.setPoint(0, rect.center().x(), rect.top());
345 result.setPoint(1, rect.right(), rect.bottom());
346 result.setPoint(2, rect.left(), rect.bottom());
347 return result;
348}
349
350void Document::paintEvent(QPaintEvent *event)
351{
352 QRegion paintRegion = event->region();
353 QPainter painter(this);
354 QPalette pal = palette();
355
356 for (int i = 0; i < m_shapeList.count(); ++i) {
357 const Shape &shape = m_shapeList.at(i);
358
359 if (!paintRegion.contains(shape.rect()))
360 continue;
361
362 QPen pen = pal.text().color();
363 pen.setWidth(i == m_currentIndex ? 2 : 1);
364 painter.setPen(pen);
365 painter.setBrush(gradient(shape.color(), shape.rect()));
366
367 QRect rect = shape.rect();
368 rect.adjust(1, 1, -resizeHandleWidth/2, -resizeHandleWidth/2);
369
370 // paint the shape
371 switch (shape.type()) {
372 case Shape::Rectangle:
373 painter.drawRect(rect);
374 break;
375 case Shape::Circle:
376 painter.setRenderHint(QPainter::Antialiasing);
377 painter.drawEllipse(rect);
378 painter.setRenderHint(QPainter::Antialiasing, false);
379 break;
380 case Shape::Triangle:
381 painter.setRenderHint(QPainter::Antialiasing);
382 painter.drawPolygon(triangle(rect));
383 painter.setRenderHint(QPainter::Antialiasing, false);
384 break;
385 }
386
387 // paint the resize handle
388 painter.setPen(pal.text().color());
389 painter.setBrush(Qt::white);
390 painter.drawRect(shape.resizeHandle().adjusted(0, 0, -1, -1));
391
392 // paint the shape name
393 painter.setBrush(pal.text());
394 if (shape.type() == Shape::Triangle)
395 rect.adjust(0, rect.height()/2, 0, 0);
396 painter.drawText(rect, Qt::AlignCenter, shape.name());
397 }
398}
399
400void Document::setCurrentShape(int index)
401{
402 QString currentName;
403
404 if (m_currentIndex != -1)
405 update(m_shapeList.at(m_currentIndex).rect());
406
407 m_currentIndex = index;
408
409 if (m_currentIndex != -1) {
410 const Shape &current = m_shapeList.at(m_currentIndex);
411 update(current.rect());
412 currentName = current.name();
413 }
414
415 emit currentShapeChanged(currentName);
416}
417
418int Document::indexOf(const QString &shapeName) const
419{
420 for (int i = 0; i < m_shapeList.count(); ++i) {
421 if (m_shapeList.at(i).name() == shapeName)
422 return i;
423 }
424 return -1;
425}
426
427QString Document::uniqueName(const QString &name) const
428{
429 QString unique;
430
431 for (int i = 0; ; ++i) {
432 unique = name;
433 if (i > 0)
434 unique += QString::number(i);
435 if (indexOf(unique) == -1)
436 break;
437 }
438
439 return unique;
440}
441
442QString Document::currentShapeName() const
443{
444 if (m_currentIndex == -1)
445 return QString();
446 return m_shapeList.at(m_currentIndex).name();
447}
448
449