1 /*
2 This file is part of Telegram Desktop,
3 the official desktop application for the Telegram messaging service.
4 
5 For license and copyright information please follow this link:
6 https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
7 */
8 #include "editor/scene/scene_item_base.h"
9 
10 #include "editor/scene/scene.h"
11 #include "lang/lang_keys.h"
12 #include "ui/widgets/popup_menu.h"
13 #include "styles/style_editor.h"
14 
15 #include <QGraphicsScene>
16 #include <QGraphicsSceneHoverEvent>
17 #include <QGraphicsSceneMouseEvent>
18 #include <QStyleOptionGraphicsItem>
19 
20 namespace Editor {
21 namespace {
22 
23 constexpr auto kSnapAngle = 45.;
24 
25 const auto kDuplicateSequence = QKeySequence("ctrl+d");
26 const auto kFlipSequence = QKeySequence("ctrl+s");
27 const auto kDeleteSequence = QKeySequence("delete");
28 
29 constexpr auto kMinSizeRatio = 0.05;
30 constexpr auto kMaxSizeRatio = 1.00;
31 
Normalized(float64 angle)32 auto Normalized(float64 angle) {
33 	return angle
34 		+ ((std::abs(angle) < 360) ? 0 : (-360 * (angle < 0 ? -1 : 1)));
35 }
36 
37 } // namespace
38 
type() const39 int NumberedItem::type() const {
40 	return NumberedItem::Type;
41 }
42 
number() const43 int NumberedItem::number() const {
44 	return _number;
45 }
46 
setNumber(int number)47 void NumberedItem::setNumber(int number) {
48 	_number = number;
49 }
50 
status() const51 NumberedItem::Status NumberedItem::status() const {
52 	return _status;
53 }
54 
isNormalStatus() const55 bool NumberedItem::isNormalStatus() const {
56 	return _status == Status::Normal;
57 }
58 
isUndidStatus() const59 bool NumberedItem::isUndidStatus() const {
60 	return _status == Status::Undid;
61 }
62 
isRemovedStatus() const63 bool NumberedItem::isRemovedStatus() const {
64 	return _status == Status::Removed;
65 }
66 
save(SaveState state)67 void NumberedItem::save(SaveState state) {
68 }
69 
restore(SaveState state)70 void NumberedItem::restore(SaveState state) {
71 }
72 
hasState(SaveState state) const73 bool NumberedItem::hasState(SaveState state) const {
74 	return false;
75 }
76 
setStatus(Status status)77 void NumberedItem::setStatus(Status status) {
78 	if (status != _status) {
79 		_status = status;
80 		setVisible(status == Status::Normal);
81 	}
82 }
83 
ItemBase(Data data)84 ItemBase::ItemBase(Data data)
85 : _lastZ(data.zPtr)
86 , _imageSize(data.imageSize)
87 , _horizontalSize(data.size) {
88 	setFlags(QGraphicsItem::ItemIsMovable
89 		| QGraphicsItem::ItemIsSelectable
90 		| QGraphicsItem::ItemIsFocusable);
91 	setAcceptHoverEvents(true);
92 	applyData(data);
93 }
94 
boundingRect() const95 QRectF ItemBase::boundingRect() const {
96 	return innerRect() + _scaledInnerMargins;
97 }
98 
contentRect() const99 QRectF ItemBase::contentRect() const {
100 	return innerRect() - _scaledInnerMargins;
101 }
102 
innerRect() const103 QRectF ItemBase::innerRect() const {
104 	const auto &hSize = _horizontalSize;
105 	const auto &vSize = _verticalSize;
106 	return QRectF(-hSize / 2, -vSize / 2, hSize, vSize);
107 }
108 
paint(QPainter * p,const QStyleOptionGraphicsItem * option,QWidget *)109 void ItemBase::paint(
110 		QPainter *p,
111 		const QStyleOptionGraphicsItem *option,
112 		QWidget *) {
113 	if (!(option->state & QStyle::State_Selected)) {
114 		return;
115 	}
116 	PainterHighQualityEnabler hq(*p);
117 	const auto hasFocus = (option->state & QStyle::State_HasFocus);
118 	p->setPen(hasFocus ? _pens.select : _pens.selectInactive);
119 	p->drawRect(innerRect());
120 
121 	p->setPen(hasFocus ? _pens.handle : _pens.handleInactive);
122 	p->setBrush(st::photoEditorItemBaseHandleFg);
123 	p->drawEllipse(rightHandleRect());
124 	p->drawEllipse(leftHandleRect());
125 }
126 
mouseMoveEvent(QGraphicsSceneMouseEvent * event)127 void ItemBase::mouseMoveEvent(QGraphicsSceneMouseEvent *event) {
128 	if (isHandling()) {
129 		const auto mousePos = event->pos();
130 		const auto shift = event->modifiers().testFlag(Qt::ShiftModifier);
131 		const auto isLeft = (_handle == HandleType::Left);
132 		if (!shift) {
133 			// Resize.
134 			const auto p = isLeft ? (mousePos * -1) : mousePos;
135 			const auto dx = int(2.0 * p.x());
136 			const auto dy = int(2.0 * p.y());
137 			prepareGeometryChange();
138 			_horizontalSize = std::clamp(
139 				(dx > dy ? dx : dy),
140 				_sizeLimits.min,
141 				_sizeLimits.max);
142 			updateVerticalSize();
143 		}
144 
145 		// Rotate.
146 		const auto origin = mapToScene(boundingRect().center());
147 		const auto pos = mapToScene(mousePos);
148 
149 		const auto diff = pos - origin;
150 		const auto angle = Normalized((isLeft ? 180 : 0)
151 			+ (std::atan2(diff.y(), diff.x()) * 180 / M_PI));
152 		setRotation(shift
153 			? (base::SafeRound(angle / kSnapAngle) * kSnapAngle)
154 			: angle);
155 	} else {
156 		QGraphicsItem::mouseMoveEvent(event);
157 	}
158 }
159 
hoverMoveEvent(QGraphicsSceneHoverEvent * event)160 void ItemBase::hoverMoveEvent(QGraphicsSceneHoverEvent *event) {
161 	setCursor(isHandling()
162 		? Qt::ClosedHandCursor
163 		: (handleType(event->pos()) != HandleType::None) && isSelected()
164 		? Qt::OpenHandCursor
165 		: Qt::ArrowCursor);
166 	QGraphicsItem::hoverMoveEvent(event);
167 }
168 
mousePressEvent(QGraphicsSceneMouseEvent * event)169 void ItemBase::mousePressEvent(QGraphicsSceneMouseEvent *event) {
170 	setZValue((*_lastZ)++);
171 	if (event->button() == Qt::LeftButton) {
172 		_handle = handleType(event->pos());
173 	}
174 	if (isHandling()) {
175 		setCursor(Qt::ClosedHandCursor);
176 	} else {
177 		QGraphicsItem::mousePressEvent(event);
178 	}
179 }
180 
mouseReleaseEvent(QGraphicsSceneMouseEvent * event)181 void ItemBase::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) {
182 	if ((event->button() == Qt::LeftButton) && isHandling()) {
183 		_handle = HandleType::None;
184 	} else {
185 		QGraphicsItem::mouseReleaseEvent(event);
186 	}
187 }
188 
contextMenuEvent(QGraphicsSceneContextMenuEvent * event)189 void ItemBase::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) {
190 	if (scene()) {
191 		scene()->clearSelection();
192 		setSelected(true);
193 	}
194 
195 	const auto add = [&](
196 			auto base,
197 			const QKeySequence &sequence,
198 			Fn<void()> callback) {
199 		// TODO: refactor.
200 		const auto sequenceText = QChar('\t')
201 			+ sequence.toString(QKeySequence::NativeText);
202 		_menu->addAction(base(tr::now) + sequenceText, std::move(callback));
203 	};
204 
205 	_menu = base::make_unique_q<Ui::PopupMenu>(nullptr);
206 	add(
207 		tr::lng_photo_editor_menu_delete,
208 		kDeleteSequence,
209 		[=] { actionDelete(); });
210 	add(
211 		tr::lng_photo_editor_menu_flip,
212 		kFlipSequence,
213 		[=] { actionFlip(); });
214 	add(
215 		tr::lng_photo_editor_menu_duplicate,
216 		kDuplicateSequence,
217 		[=] { actionDuplicate(); });
218 
219 	_menu->popup(event->screenPos());
220 }
221 
performForSelectedItems(Action action)222 void ItemBase::performForSelectedItems(Action action) {
223 	if (const auto s = scene()) {
224 		for (const auto item : s->selectedItems()) {
225 			if (const auto base = static_cast<ItemBase*>(item)) {
226 				(base->*action)();
227 			}
228 		}
229 	}
230 }
231 
actionFlip()232 void ItemBase::actionFlip() {
233 	setFlip(!flipped());
234 }
235 
actionDelete()236 void ItemBase::actionDelete() {
237 	if (const auto s = static_cast<Scene*>(scene())) {
238 		s->removeItem(this);
239 	}
240 }
241 
actionDuplicate()242 void ItemBase::actionDuplicate() {
243 	if (const auto s = static_cast<Scene*>(scene())) {
244 		auto data = generateData();
245 		data.x += int(_horizontalSize / 3);
246 		data.y += int(_verticalSize / 3);
247 		const auto newItem = duplicate(std::move(data));
248 		if (hasFocus()) {
249 			newItem->setFocus();
250 		}
251 		const auto selected = isSelected();
252 		newItem->setSelected(selected);
253 		setSelected(false);
254 		s->addItem(newItem);
255 	}
256 }
257 
keyPressEvent(QKeyEvent * e)258 void ItemBase::keyPressEvent(QKeyEvent *e) {
259 	if (e->key() == Qt::Key_Escape) {
260 		if (const auto s = scene()) {
261 			s->clearSelection();
262 			s->clearFocus();
263 			return;
264 		}
265 	}
266 	handleActionKey(e);
267 }
268 
handleActionKey(not_null<QKeyEvent * > e)269 void ItemBase::handleActionKey(not_null<QKeyEvent*> e) {
270 	const auto matches = [&](const QKeySequence &sequence) {
271 		const auto searchKey = (e->modifiers() | e->key())
272 			& ~(Qt::KeypadModifier | Qt::GroupSwitchModifier);
273 		const auto events = QKeySequence(searchKey);
274 		return sequence.matches(events) == QKeySequence::ExactMatch;
275 	};
276 	if (matches(kDuplicateSequence)) {
277 		performForSelectedItems(&ItemBase::actionDuplicate);
278 	} else if (matches(kDeleteSequence)) {
279 		performForSelectedItems(&ItemBase::actionDelete);
280 	} else if (matches(kFlipSequence)) {
281 		performForSelectedItems(&ItemBase::actionFlip);
282 	}
283 }
284 
rightHandleRect() const285 QRectF ItemBase::rightHandleRect() const {
286 	return QRectF(
287 		(_horizontalSize / 2) - (_scaledHandleSize / 2),
288 		0 - (_scaledHandleSize / 2),
289 		_scaledHandleSize,
290 		_scaledHandleSize);
291 }
292 
leftHandleRect() const293 QRectF ItemBase::leftHandleRect() const {
294 	return QRectF(
295 		(-_horizontalSize / 2) - (_scaledHandleSize / 2),
296 		0 - (_scaledHandleSize / 2),
297 		_scaledHandleSize,
298 		_scaledHandleSize);
299 }
300 
isHandling() const301 bool ItemBase::isHandling() const {
302 	return _handle != HandleType::None;
303 }
304 
size() const305 float64 ItemBase::size() const {
306 	return _horizontalSize;
307 }
308 
updateVerticalSize()309 void ItemBase::updateVerticalSize() {
310 	const auto verticalSize = _horizontalSize * _aspectRatio;
311 	_verticalSize = std::max(
312 		verticalSize,
313 		float64(_sizeLimits.min));
314 	if (verticalSize < _sizeLimits.min) {
315 		_horizontalSize = _verticalSize / _aspectRatio;
316 	}
317 }
318 
setAspectRatio(float64 aspectRatio)319 void ItemBase::setAspectRatio(float64 aspectRatio) {
320 	_aspectRatio = aspectRatio;
321 	updateVerticalSize();
322 }
323 
handleType(const QPointF & pos) const324 ItemBase::HandleType ItemBase::handleType(const QPointF &pos) const {
325 	return rightHandleRect().contains(pos)
326 		? HandleType::Right
327 		: leftHandleRect().contains(pos)
328 		? HandleType::Left
329 		: HandleType::None;
330 }
331 
flipped() const332 bool ItemBase::flipped() const {
333 	return _flipped;
334 }
335 
setFlip(bool value)336 void ItemBase::setFlip(bool value) {
337 	if (_flipped != value) {
338 		performFlip();
339 		_flipped = value;
340 	}
341 }
342 
type() const343 int ItemBase::type() const {
344 	return ItemBase::Type;
345 }
346 
updateZoom(float64 zoom)347 void ItemBase::updateZoom(float64 zoom) {
348 	_scaledHandleSize = st::photoEditorItemHandleSize / zoom;
349 	_scaledInnerMargins = QMarginsF(
350 		_scaledHandleSize,
351 		_scaledHandleSize,
352 		_scaledHandleSize,
353 		_scaledHandleSize) * 0.5;
354 
355 	const auto maxSide = std::max(
356 		_imageSize.width(),
357 		_imageSize.height());
358 	_sizeLimits = {
359 		.min = int(maxSide * kMinSizeRatio),
360 		.max = int(maxSide * kMaxSizeRatio),
361 	};
362 	_horizontalSize = std::clamp(
363 		_horizontalSize,
364 		float64(_sizeLimits.min),
365 		float64(_sizeLimits.max));
366 	updateVerticalSize();
367 
368 	updatePens(QPen(
369 		QBrush(),
370 		1 / zoom,
371 		Qt::DashLine,
372 		Qt::SquareCap,
373 		Qt::RoundJoin));
374 }
375 
performFlip()376 void ItemBase::performFlip() {
377 }
378 
updatePens(QPen pen)379 void ItemBase::updatePens(QPen pen) {
380 	_pens = {
381 		.select = pen,
382 		.selectInactive = pen,
383 		.handle = pen,
384 		.handleInactive = pen,
385 	};
386 	_pens.select.setColor(Qt::white);
387 	_pens.selectInactive.setColor(Qt::gray);
388 	_pens.handle.setColor(Qt::white);
389 	_pens.handleInactive.setColor(Qt::gray);
390 	_pens.handle.setStyle(Qt::SolidLine);
391 	_pens.handleInactive.setStyle(Qt::SolidLine);
392 }
393 
generateData() const394 ItemBase::Data ItemBase::generateData() const {
395 	return {
396 		.initialZoom = (st::photoEditorItemHandleSize / _scaledHandleSize),
397 		.zPtr = _lastZ,
398 		.size = int(_horizontalSize),
399 		.x = int(scenePos().x()),
400 		.y = int(scenePos().y()),
401 		.flipped = flipped(),
402 		.rotation = int(rotation()),
403 		.imageSize = _imageSize,
404 	};
405 }
406 
applyData(const Data & data)407 void ItemBase::applyData(const Data &data) {
408 	// _lastZ is const.
409 	// _imageSize is const.
410 	_horizontalSize = data.size;
411 	setPos(data.x, data.y);
412 	setZValue((*_lastZ)++);
413 	setFlip(data.flipped);
414 	setRotation(data.rotation);
415 	updateZoom(data.initialZoom);
416 	update();
417 }
418 
save(SaveState state)419 void ItemBase::save(SaveState state) {
420 	const auto z = zValue();
421 	auto &saved = (state == SaveState::Keep) ? _keeped : _saved;
422 	saved = {
423 		.data = generateData(),
424 		.zValue = z,
425 		.status = status(),
426 	};
427 }
428 
restore(SaveState state)429 void ItemBase::restore(SaveState state) {
430 	if (!hasState(state)) {
431 		return;
432 	}
433 	const auto &saved = (state == SaveState::Keep) ? _keeped : _saved;
434 	applyData(saved.data);
435 	setZValue(saved.zValue);
436 	setStatus(saved.status);
437 }
438 
hasState(SaveState state) const439 bool ItemBase::hasState(SaveState state) const {
440 	const auto &saved = (state == SaveState::Keep) ? _keeped : _saved;
441 	return saved.zValue;
442 }
443 
444 } // namespace Editor
445