init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
This commit is contained in:
328
Telegram/SourceFiles/editor/color_picker.cpp
Normal file
328
Telegram/SourceFiles/editor/color_picker.cpp
Normal file
@@ -0,0 +1,328 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "editor/color_picker.h"
|
||||
|
||||
#include "ui/painter.h"
|
||||
#include "ui/rp_widget.h"
|
||||
#include "styles/style_editor.h"
|
||||
|
||||
#include <QtGui/QLinearGradient>
|
||||
|
||||
namespace Editor {
|
||||
namespace {
|
||||
|
||||
constexpr auto kPrecision = 1000;
|
||||
constexpr auto kMinBrushSize = 0.1f;
|
||||
constexpr auto kMouseSkip = 1.4;
|
||||
|
||||
constexpr auto kMinInnerHeight = 0.2;
|
||||
constexpr auto kMaxInnerHeight = 0.8;
|
||||
|
||||
constexpr auto kCircleDuration = crl::time(200);
|
||||
|
||||
constexpr auto kMax = 1.0;
|
||||
|
||||
ColorPicker::OutlinedStop FindOutlinedStop(
|
||||
const QColor &color,
|
||||
const QGradientStops &stops,
|
||||
int width) {
|
||||
for (auto i = 0; i < stops.size(); i++) {
|
||||
const auto ¤t = stops[i];
|
||||
if (current.second == color) {
|
||||
const auto prev = ((i - 1) < 0)
|
||||
? std::nullopt
|
||||
: std::make_optional<int>(stops[i - 1].first * width);
|
||||
const auto next = ((i + 1) >= stops.size())
|
||||
? std::nullopt
|
||||
: std::make_optional<int>(stops[i + 1].first * width);
|
||||
return ColorPicker::OutlinedStop{
|
||||
.stopPos = (current.first * width),
|
||||
.prevStopPos = prev,
|
||||
.nextStopPos = next,
|
||||
};
|
||||
}
|
||||
}
|
||||
return ColorPicker::OutlinedStop();
|
||||
}
|
||||
|
||||
QGradientStops Colors() {
|
||||
return QGradientStops{
|
||||
{ 0.00f, QColor(234, 39, 57) },
|
||||
{ 0.14f, QColor(219, 58, 210) },
|
||||
{ 0.24f, QColor(48, 81, 227) },
|
||||
{ 0.39f, QColor(73, 197, 237) },
|
||||
{ 0.49f, QColor(128, 200, 100) },
|
||||
{ 0.62f, QColor(252, 222, 101) },
|
||||
{ 0.73f, QColor(252, 150, 77) },
|
||||
{ 0.85f, QColor(0, 0, 0) },
|
||||
{ 1.00f, QColor(255, 255, 255) } };
|
||||
}
|
||||
|
||||
QBrush GradientBrush(const QPoint &p, const QGradientStops &stops) {
|
||||
auto gradient = QLinearGradient(0, p.y(), p.x(), p.y());
|
||||
gradient.setStops(stops);
|
||||
return QBrush(std::move(gradient));
|
||||
}
|
||||
|
||||
float RatioPrecise(float a) {
|
||||
return int(a * kPrecision) / float(kPrecision);
|
||||
}
|
||||
|
||||
inline float64 InterpolateF(float a, float b, float64 b_ratio) {
|
||||
return a + float64(b - a) * b_ratio;
|
||||
};
|
||||
|
||||
inline float64 InterpolationRatio(int from, int to, int result) {
|
||||
return (result - from) / float64(to - from);
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
ColorPicker::ColorPicker(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const Brush &savedBrush)
|
||||
: _circleColor(Qt::white)
|
||||
, _width(st::photoEditorColorPickerWidth)
|
||||
, _lineHeight(st::photoEditorColorPickerLineHeight)
|
||||
, _colorLine(base::make_unique_q<Ui::RpWidget>(parent))
|
||||
, _canvasForCircle(base::make_unique_q<Ui::RpWidget>(parent))
|
||||
, _gradientStops(Colors())
|
||||
, _outlinedStop(FindOutlinedStop(_circleColor, _gradientStops, _width))
|
||||
, _gradientBrush(
|
||||
GradientBrush(QPoint(_width, _lineHeight / 2), _gradientStops))
|
||||
, _brush(Brush{
|
||||
.sizeRatio = (savedBrush.sizeRatio
|
||||
? savedBrush.sizeRatio
|
||||
: kMinBrushSize),
|
||||
.color = (savedBrush.color.isValid()
|
||||
? savedBrush.color
|
||||
: _gradientStops.front().second),
|
||||
}) {
|
||||
_colorLine->resize(_width, _lineHeight);
|
||||
_canvasForCircle->resize(
|
||||
_width + circleHeight(kMax),
|
||||
st::photoEditorColorPickerCanvasHeight);
|
||||
|
||||
_canvasForCircle->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
|
||||
_down.pos = QPoint(colorToPosition(savedBrush.color), 0);
|
||||
|
||||
_colorLine->paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
auto p = QPainter(_colorLine);
|
||||
PainterHighQualityEnabler hq(p);
|
||||
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(_gradientBrush);
|
||||
|
||||
const auto radius = _colorLine->height() / 2.;
|
||||
p.drawRoundedRect(_colorLine->rect(), radius, radius);
|
||||
}, _colorLine->lifetime());
|
||||
|
||||
_canvasForCircle->paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
auto p = QPainter(_canvasForCircle);
|
||||
paintCircle(p);
|
||||
}, _canvasForCircle->lifetime());
|
||||
|
||||
_colorLine->events(
|
||||
) | rpl::on_next([=](not_null<QEvent*> event) {
|
||||
const auto type = event->type();
|
||||
const auto isPress = (type == QEvent::MouseButtonPress)
|
||||
|| (type == QEvent::MouseButtonDblClick);
|
||||
const auto isMove = (type == QEvent::MouseMove);
|
||||
const auto isRelease = (type == QEvent::MouseButtonRelease);
|
||||
if (!isPress && !isMove && !isRelease) {
|
||||
return;
|
||||
}
|
||||
_down.pressed = !isRelease;
|
||||
|
||||
const auto progress = _circleAnimation.value(isPress ? 0. : 1.);
|
||||
if (!isMove) {
|
||||
const auto from = progress;
|
||||
const auto to = isPress ? 1. : 0.;
|
||||
_circleAnimation.stop();
|
||||
|
||||
_circleAnimation.start(
|
||||
[=] { _canvasForCircle->update(); },
|
||||
from,
|
||||
to,
|
||||
kCircleDuration * std::abs(to - from),
|
||||
anim::easeOutCirc);
|
||||
}
|
||||
const auto e = static_cast<QMouseEvent*>(event.get());
|
||||
updateMousePosition(e->pos(), progress);
|
||||
if (isRelease) {
|
||||
_saveBrushRequests.fire_copy(_brush);
|
||||
}
|
||||
|
||||
_canvasForCircle->update();
|
||||
}, _colorLine->lifetime());
|
||||
}
|
||||
|
||||
void ColorPicker::updateMousePosition(const QPoint &pos, float64 progress) {
|
||||
const auto mapped = _canvasForCircle->mapFromParent(
|
||||
_colorLine->mapToParent(pos));
|
||||
|
||||
const auto height = circleHeight(progress);
|
||||
const auto mappedY = int(mapped.y() - height * kMouseSkip);
|
||||
const auto bottom = _canvasForCircle->height() - circleHeight(kMax);
|
||||
const auto &skip = st::photoEditorColorPickerCircleSkip;
|
||||
|
||||
_down.pos = QPoint(
|
||||
std::clamp(pos.x(), 0, _width),
|
||||
std::clamp(mappedY, 0, bottom - skip));
|
||||
|
||||
// Convert Y to the brush size.
|
||||
const auto from = 0;
|
||||
const auto to = bottom - skip;
|
||||
|
||||
// Don't change the brush size when we are on the color line.
|
||||
if (mappedY <= to) {
|
||||
_brush.sizeRatio = std::clamp(
|
||||
float(1. - InterpolationRatio(from, to, _down.pos.y())),
|
||||
kMinBrushSize,
|
||||
1.f);
|
||||
}
|
||||
_brush.color = positionToColor(_down.pos.x());
|
||||
}
|
||||
|
||||
void ColorPicker::moveLine(const QPoint &position) {
|
||||
_colorLine->move(position
|
||||
- QPoint(_colorLine->width() / 2, _colorLine->height() / 2));
|
||||
|
||||
_canvasForCircle->move(
|
||||
_colorLine->x() - circleHeight(kMax) / 2,
|
||||
_colorLine->y()
|
||||
+ _colorLine->height()
|
||||
+ ((circleHeight() - _colorLine->height()) / 2)
|
||||
- _canvasForCircle->height());
|
||||
}
|
||||
|
||||
QColor ColorPicker::positionToColor(int x) const {
|
||||
const auto from = 0;
|
||||
const auto to = _width;
|
||||
const auto gradientRatio = InterpolationRatio(from, to, x);
|
||||
|
||||
for (auto i = 1; i < _gradientStops.size(); i++) {
|
||||
const auto &previous = _gradientStops[i - 1];
|
||||
const auto ¤t = _gradientStops[i];
|
||||
const auto &fromStop = previous.first;
|
||||
const auto &toStop = current.first;
|
||||
const auto &fromColor = previous.second;
|
||||
const auto &toColor = current.second;
|
||||
|
||||
if ((fromStop <= gradientRatio) && (toStop >= gradientRatio)) {
|
||||
const auto stopRatio = RatioPrecise(
|
||||
(gradientRatio - fromStop) / float64(toStop - fromStop));
|
||||
return anim::color(fromColor, toColor, stopRatio);
|
||||
}
|
||||
}
|
||||
return QColor();
|
||||
}
|
||||
|
||||
void ColorPicker::paintCircle(QPainter &p) {
|
||||
PainterHighQualityEnabler hq(p);
|
||||
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(_circleColor);
|
||||
|
||||
const auto progress = _circleAnimation.value(_down.pressed ? 1. : 0.);
|
||||
const auto h = circleHeight(progress);
|
||||
const auto bottom = _canvasForCircle->height() - h;
|
||||
|
||||
const auto circleX = _down.pos.x() + (circleHeight(kMax) - h) / 2;
|
||||
const auto circleY = _circleAnimation.animating()
|
||||
? anim::interpolate(bottom, _down.pos.y(), progress)
|
||||
: _down.pressed
|
||||
? _down.pos.y()
|
||||
: bottom;
|
||||
|
||||
const auto r = QRect(circleX, circleY, h, h);
|
||||
p.drawEllipse(r);
|
||||
|
||||
const auto innerH = InterpolateF(
|
||||
h * kMinInnerHeight,
|
||||
h * kMaxInnerHeight,
|
||||
_brush.sizeRatio);
|
||||
|
||||
p.setBrush(_brush.color);
|
||||
|
||||
const auto innerRect = QRectF(
|
||||
r.x() + (r.width() - innerH) / 2.,
|
||||
r.y() + (r.height() - innerH) / 2.,
|
||||
innerH,
|
||||
innerH);
|
||||
|
||||
paintOutline(p, innerRect);
|
||||
p.drawEllipse(innerRect);
|
||||
}
|
||||
|
||||
void ColorPicker::paintOutline(QPainter &p, const QRectF &rect) {
|
||||
const auto &s = _outlinedStop;
|
||||
if (!s.stopPos) {
|
||||
return;
|
||||
}
|
||||
const auto draw = [&](float opacity) {
|
||||
p.save();
|
||||
p.setOpacity(opacity);
|
||||
p.setPen(Qt::lightGray);
|
||||
p.setPen(Qt::NoBrush);
|
||||
p.drawEllipse(rect);
|
||||
p.restore();
|
||||
};
|
||||
const auto x = _down.pos.x();
|
||||
if (s.prevStopPos && (x >= s.prevStopPos && x <= s.stopPos)) {
|
||||
const auto from = *s.prevStopPos;
|
||||
const auto to = *s.stopPos;
|
||||
const auto ratio = InterpolationRatio(from, to, x);
|
||||
if (ratio >= 0. && ratio <= 1.) {
|
||||
draw(ratio);
|
||||
}
|
||||
} else if (s.nextStopPos && (x >= s.stopPos && x <= s.nextStopPos)) {
|
||||
const auto from = *s.stopPos;
|
||||
const auto to = *s.nextStopPos;
|
||||
const auto ratio = InterpolationRatio(from, to, x);
|
||||
if (ratio >= 0. && ratio <= 1.) {
|
||||
draw(1. - ratio);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int ColorPicker::circleHeight(float64 progress) const {
|
||||
return anim::interpolate(
|
||||
st::photoEditorColorPickerCircleSize,
|
||||
st::photoEditorColorPickerCircleBigSize,
|
||||
progress);
|
||||
}
|
||||
|
||||
void ColorPicker::setVisible(bool visible) {
|
||||
_colorLine->setVisible(visible);
|
||||
_canvasForCircle->setVisible(visible);
|
||||
}
|
||||
|
||||
rpl::producer<Brush> ColorPicker::saveBrushRequests() const {
|
||||
return _saveBrushRequests.events_starting_with_copy(_brush);
|
||||
}
|
||||
|
||||
int ColorPicker::colorToPosition(const QColor &color) const {
|
||||
const auto step = 1. / kPrecision;
|
||||
for (auto i = 0.; i <= 1.; i += step) {
|
||||
if (positionToColor(i * _width) == color) {
|
||||
return i * _width;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool ColorPicker::preventHandleKeyPress() const {
|
||||
return _canvasForCircle->isVisible()
|
||||
&& (_circleAnimation.animating() || _down.pressed);
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
67
Telegram/SourceFiles/editor/color_picker.h
Normal file
67
Telegram/SourceFiles/editor/color_picker.h
Normal file
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/unique_qptr.h"
|
||||
#include "editor/photo_editor_inner_common.h"
|
||||
#include "ui/effects/animations.h"
|
||||
|
||||
namespace Ui {
|
||||
class RpWidget;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Editor {
|
||||
|
||||
class ColorPicker final {
|
||||
public:
|
||||
struct OutlinedStop {
|
||||
std::optional<int> stopPos = std::nullopt;
|
||||
std::optional<int> prevStopPos = std::nullopt;
|
||||
std::optional<int> nextStopPos = std::nullopt;
|
||||
};
|
||||
|
||||
ColorPicker(not_null<Ui::RpWidget*> parent, const Brush &savedBrush);
|
||||
|
||||
void moveLine(const QPoint &position);
|
||||
void setVisible(bool visible);
|
||||
bool preventHandleKeyPress() const;
|
||||
|
||||
rpl::producer<Brush> saveBrushRequests() const;
|
||||
|
||||
private:
|
||||
void paintCircle(QPainter &p);
|
||||
void paintOutline(QPainter &p, const QRectF &rect);
|
||||
QColor positionToColor(int x) const;
|
||||
int colorToPosition(const QColor &color) const;
|
||||
int circleHeight(float64 progress = 0.) const;
|
||||
void updateMousePosition(const QPoint &pos, float64 progress);
|
||||
|
||||
const QColor _circleColor;
|
||||
const int _width;
|
||||
const int _lineHeight;
|
||||
|
||||
const base::unique_qptr<Ui::RpWidget> _colorLine;
|
||||
const base::unique_qptr<Ui::RpWidget> _canvasForCircle;
|
||||
|
||||
const QGradientStops _gradientStops;
|
||||
const OutlinedStop _outlinedStop;
|
||||
const QBrush _gradientBrush;
|
||||
|
||||
struct {
|
||||
QPoint pos;
|
||||
bool pressed = false;
|
||||
} _down;
|
||||
Brush _brush;
|
||||
|
||||
Ui::Animations::Simple _circleAnimation;
|
||||
|
||||
rpl::event_stream<Brush> _saveBrushRequests;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
33
Telegram/SourceFiles/editor/controllers/controllers.h
Normal file
33
Telegram/SourceFiles/editor/controllers/controllers.h
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "editor/controllers/stickers_panel_controller.h"
|
||||
#include "editor/controllers/undo_controller.h"
|
||||
#include "ui/layers/show.h"
|
||||
|
||||
namespace Editor {
|
||||
|
||||
struct Controllers final {
|
||||
Controllers(
|
||||
std::unique_ptr<StickersPanelController> stickersPanelController,
|
||||
std::unique_ptr<UndoController> undoController,
|
||||
std::shared_ptr<Ui::Show> show)
|
||||
: stickersPanelController(std::move(stickersPanelController))
|
||||
, undoController(std::move(undoController))
|
||||
, show(std::move(show)) {
|
||||
}
|
||||
~Controllers() {
|
||||
};
|
||||
|
||||
const std::unique_ptr<StickersPanelController> stickersPanelController;
|
||||
const std::unique_ptr<UndoController> undoController;
|
||||
const std::shared_ptr<Ui::Show> show;
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "editor/controllers/stickers_panel_controller.h"
|
||||
|
||||
#include "chat_helpers/tabbed_panel.h"
|
||||
#include "chat_helpers/tabbed_selector.h"
|
||||
#include "window/window_session_controller.h" // Window::GifPauseReason
|
||||
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_media_view.h"
|
||||
|
||||
namespace Editor {
|
||||
|
||||
StickersPanelController::StickersPanelController(
|
||||
not_null<Ui::RpWidget*> panelContainer,
|
||||
std::shared_ptr<ChatHelpers::Show> show)
|
||||
: _stickersPanel(
|
||||
base::make_unique_q<ChatHelpers::TabbedPanel>(
|
||||
panelContainer,
|
||||
ChatHelpers::TabbedPanelDescriptor{
|
||||
.ownedSelector = object_ptr<ChatHelpers::TabbedSelector>(
|
||||
nullptr,
|
||||
ChatHelpers::TabbedSelectorDescriptor{
|
||||
.show = show,
|
||||
.st = st::storiesComposeControls.tabbed,
|
||||
.level = Window::GifPauseReason::Layer,
|
||||
.mode = ChatHelpers::TabbedSelector::Mode::MediaEditor,
|
||||
.features = {
|
||||
.megagroupSet = false,
|
||||
.stickersSettings = false,
|
||||
.openStickerSets = false,
|
||||
},
|
||||
}),
|
||||
})) {
|
||||
_stickersPanel->setDesiredHeightValues(
|
||||
1.,
|
||||
st::emojiPanMinHeight / 2,
|
||||
st::emojiPanMinHeight);
|
||||
_stickersPanel->hide();
|
||||
}
|
||||
|
||||
auto StickersPanelController::stickerChosen() const
|
||||
-> rpl::producer<not_null<DocumentData*>> {
|
||||
return _stickersPanel->selector()->fileChosen(
|
||||
) | rpl::map([](const ChatHelpers::FileChosen &data) {
|
||||
return data.document;
|
||||
});
|
||||
}
|
||||
|
||||
rpl::producer<bool> StickersPanelController::panelShown() const {
|
||||
return _stickersPanel->shownValue();
|
||||
}
|
||||
|
||||
void StickersPanelController::setShowRequestChanges(
|
||||
rpl::producer<ShowRequest> &&showRequest) {
|
||||
std::move(
|
||||
showRequest
|
||||
) | rpl::on_next([=](ShowRequest show) {
|
||||
if (show == ShowRequest::ToggleAnimated) {
|
||||
_stickersPanel->toggleAnimated();
|
||||
_stickersPanel->raise();
|
||||
} else if (show == ShowRequest::ShowAnimated) {
|
||||
_stickersPanel->showAnimated();
|
||||
_stickersPanel->raise();
|
||||
} else if (show == ShowRequest::HideAnimated) {
|
||||
_stickersPanel->hideAnimated();
|
||||
} else if (show == ShowRequest::HideFast) {
|
||||
_stickersPanel->hideFast();
|
||||
}
|
||||
}, _stickersPanel->lifetime());
|
||||
}
|
||||
|
||||
void StickersPanelController::setMoveRequestChanges(
|
||||
rpl::producer<QPoint> &&moveRequest) {
|
||||
std::move(
|
||||
moveRequest
|
||||
) | rpl::on_next([=](const QPoint &point) {
|
||||
_stickersPanel->moveBottomRight(
|
||||
point.y(),
|
||||
point.x() + _stickersPanel->width() / 2);
|
||||
}, _stickersPanel->lifetime());
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/unique_qptr.h"
|
||||
|
||||
namespace ChatHelpers {
|
||||
class TabbedPanel;
|
||||
class Show;
|
||||
} // namespace ChatHelpers
|
||||
|
||||
namespace Ui {
|
||||
class RpWidget;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Editor {
|
||||
|
||||
class StickersPanelController final {
|
||||
public:
|
||||
enum class ShowRequest {
|
||||
ToggleAnimated,
|
||||
ShowAnimated,
|
||||
HideAnimated,
|
||||
HideFast,
|
||||
};
|
||||
|
||||
StickersPanelController(
|
||||
not_null<Ui::RpWidget*> panelContainer,
|
||||
std::shared_ptr<ChatHelpers::Show> show);
|
||||
|
||||
[[nodiscard]] auto stickerChosen() const
|
||||
-> rpl::producer<not_null<DocumentData*>>;
|
||||
[[nodiscard]] rpl::producer<bool> panelShown() const;
|
||||
|
||||
void setShowRequestChanges(rpl::producer<ShowRequest> &&showRequest);
|
||||
// Middle x and plain y position.
|
||||
void setMoveRequestChanges(rpl::producer<QPoint> &&moveRequest);
|
||||
|
||||
private:
|
||||
const base::unique_qptr<ChatHelpers::TabbedPanel> _stickersPanel;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
39
Telegram/SourceFiles/editor/controllers/undo_controller.cpp
Normal file
39
Telegram/SourceFiles/editor/controllers/undo_controller.cpp
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "editor/controllers/undo_controller.h"
|
||||
|
||||
namespace Editor {
|
||||
namespace {
|
||||
using EnableRequest = UndoController::EnableRequest;
|
||||
} // namespace
|
||||
|
||||
UndoController::UndoController() {
|
||||
}
|
||||
|
||||
void UndoController::setCanPerformChanges(
|
||||
rpl::producer<EnableRequest> &&command) {
|
||||
std::move(
|
||||
command
|
||||
) | rpl::start_to_stream(_enable, _lifetime);
|
||||
}
|
||||
|
||||
void UndoController::setPerformRequestChanges(rpl::producer<Undo> &&command) {
|
||||
std::move(
|
||||
command
|
||||
) | rpl::start_to_stream(_perform, _lifetime);
|
||||
}
|
||||
|
||||
rpl::producer<EnableRequest> UndoController::canPerformChanges() const {
|
||||
return _enable.events();
|
||||
}
|
||||
|
||||
rpl::producer<Undo> UndoController::performRequestChanges() const {
|
||||
return _perform.events();
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
41
Telegram/SourceFiles/editor/controllers/undo_controller.h
Normal file
41
Telegram/SourceFiles/editor/controllers/undo_controller.h
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Editor {
|
||||
|
||||
enum class Undo {
|
||||
Undo,
|
||||
Redo,
|
||||
};
|
||||
|
||||
class UndoController final {
|
||||
public:
|
||||
struct EnableRequest {
|
||||
Undo command = Undo::Undo;
|
||||
bool enable = true;
|
||||
};
|
||||
|
||||
UndoController();
|
||||
|
||||
void setCanPerformChanges(rpl::producer<EnableRequest> &&command);
|
||||
void setPerformRequestChanges(rpl::producer<Undo> &&command);
|
||||
|
||||
[[nodiscard]] rpl::producer<EnableRequest> canPerformChanges() const;
|
||||
[[nodiscard]] rpl::producer<Undo> performRequestChanges() const;
|
||||
|
||||
private:
|
||||
|
||||
rpl::event_stream<Undo> _perform;
|
||||
rpl::event_stream<EnableRequest> _enable;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
104
Telegram/SourceFiles/editor/editor.style
Normal file
104
Telegram/SourceFiles/editor/editor.style
Normal file
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
using "ui/basic.style";
|
||||
|
||||
using "window/window.style";
|
||||
using "ui/widgets/widgets.style";
|
||||
using "ui/chat/chat.style";
|
||||
|
||||
// photoEditorControlsBottomSkip * 2 + photoEditorControlsCenterSkip + photoEditorButtonBarHeight * 2
|
||||
photoEditorControlsHeight: 146px;
|
||||
|
||||
photoEditorControlsBottomSkip: 20px;
|
||||
photoEditorControlsCenterSkip: 6px;
|
||||
|
||||
photoEditorContentMargins: margins(
|
||||
photoEditorControlsBottomSkip,
|
||||
photoEditorControlsBottomSkip,
|
||||
photoEditorControlsBottomSkip,
|
||||
photoEditorControlsHeight);
|
||||
|
||||
photoEditorBarAnimationDuration: 200;
|
||||
|
||||
photoEditorButtonIconFg: mediaviewPipControlsFg;
|
||||
photoEditorButtonIconFgOver: mediaviewPipControlsFgOver;
|
||||
|
||||
photoEditorButtonIconFgActive: lightButtonFg;
|
||||
photoEditorButtonIconFgInactive: mediaviewPipPlaybackInactive;
|
||||
|
||||
photoEditorButtonBarHeight: 48px;
|
||||
photoEditorButtonBarWidth: 422px;
|
||||
photoEditorButtonBarPadding: margins(2px, 0px, 2px, 0px);
|
||||
photoEditorTextButtonPadding: margins(22px, 0px, 22px, 0px);
|
||||
|
||||
photoEditorButtonStyle: TextStyle(semiboldTextStyle) {
|
||||
font: font(14px semibold);
|
||||
}
|
||||
photoEditorButtonTextTop: 15px;
|
||||
|
||||
photoEditorAbout: FlatLabel(defaultFlatLabel) {
|
||||
textFg: mediaviewCaptionFg;
|
||||
minWidth: 240px;
|
||||
align: align(top);
|
||||
}
|
||||
photoEditorAboutMargin: margins(10px, 22px, 10px, 0px);
|
||||
|
||||
photoEditorRotateButton: IconButton(defaultIconButton) {
|
||||
width: photoEditorButtonBarHeight;
|
||||
height: photoEditorButtonBarHeight;
|
||||
|
||||
icon: icon {{ "photo_editor/rotate-flip_horizontal", photoEditorButtonIconFg }};
|
||||
iconOver: icon {{ "photo_editor/rotate-flip_horizontal", photoEditorButtonIconFgOver }};
|
||||
|
||||
rippleAreaPosition: point(4px, 4px);
|
||||
rippleAreaSize: 40px;
|
||||
ripple: universalRippleAnimation;
|
||||
}
|
||||
photoEditorFlipButton: IconButton(photoEditorRotateButton) {
|
||||
icon: icon {{ "photo_editor/flip", photoEditorButtonIconFg }};
|
||||
iconOver: icon {{ "photo_editor/flip", photoEditorButtonIconFgOver }};
|
||||
}
|
||||
photoEditorFlipIconActive: icon {{ "photo_editor/flip", photoEditorButtonIconFgActive }};
|
||||
|
||||
photoEditorPaintModeButton: IconButton(photoEditorRotateButton) {
|
||||
icon: icon {{ "photo_editor/paint", photoEditorButtonIconFg }};
|
||||
iconOver: icon {{ "photo_editor/paint", photoEditorButtonIconFgOver }};
|
||||
}
|
||||
photoEditorPaintIconActive: icon {{ "photo_editor/paint", photoEditorButtonIconFgActive }};
|
||||
|
||||
photoEditorUndoButton: IconButton(photoEditorRotateButton) {
|
||||
icon: icon {{ "photo_editor/undo", photoEditorButtonIconFg }};
|
||||
iconOver: icon {{ "photo_editor/undo", photoEditorButtonIconFgOver }};
|
||||
}
|
||||
|
||||
photoEditorRedoButton: IconButton(photoEditorRotateButton) {
|
||||
icon: icon {{ "photo_editor/undo-flip_horizontal", photoEditorButtonIconFg }};
|
||||
iconOver: icon {{ "photo_editor/undo-flip_horizontal", photoEditorButtonIconFgOver }};
|
||||
}
|
||||
|
||||
photoEditorStickersButton: IconButton(photoEditorRotateButton) {
|
||||
icon: icon {{ "settings/settings_stickers", photoEditorButtonIconFg }};
|
||||
iconOver: icon {{ "settings/settings_stickers", photoEditorButtonIconFgOver }};
|
||||
}
|
||||
photoEditorStickersIconActive: icon {{ "settings/settings_stickers", photoEditorButtonIconFgActive }};
|
||||
|
||||
photoEditorUndoButtonInactive: icon {{ "photo_editor/undo", photoEditorButtonIconFgInactive }};
|
||||
photoEditorRedoButtonInactive: icon {{ "photo_editor/undo-flip_horizontal", photoEditorButtonIconFgInactive }};
|
||||
|
||||
photoEditorColorPickerWidth: 250px;
|
||||
photoEditorColorPickerLineHeight: 10px;
|
||||
photoEditorColorPickerCanvasHeight: 300px;
|
||||
photoEditorColorPickerCircleSize: 24px;
|
||||
photoEditorColorPickerCircleBigSize: 50px;
|
||||
|
||||
photoEditorColorPickerCircleSkip: 50px;
|
||||
|
||||
photoEditorCropPointSize: 10px;
|
||||
photoEditorCropMinSize: 20px;
|
||||
|
||||
photoEditorItemHandleSize: 10px;
|
||||
358
Telegram/SourceFiles/editor/editor_crop.cpp
Normal file
358
Telegram/SourceFiles/editor/editor_crop.cpp
Normal file
@@ -0,0 +1,358 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "editor/editor_crop.h"
|
||||
|
||||
#include "ui/userpic_view.h"
|
||||
#include "styles/style_editor.h"
|
||||
#include "styles/style_basic.h"
|
||||
#include "styles/style_dialogs.h"
|
||||
|
||||
namespace Editor {
|
||||
namespace {
|
||||
|
||||
constexpr auto kETL = Qt::TopEdge | Qt::LeftEdge;
|
||||
constexpr auto kETR = Qt::TopEdge | Qt::RightEdge;
|
||||
constexpr auto kEBL = Qt::BottomEdge | Qt::LeftEdge;
|
||||
constexpr auto kEBR = Qt::BottomEdge | Qt::RightEdge;
|
||||
constexpr auto kEAll = Qt::TopEdge
|
||||
| Qt::LeftEdge
|
||||
| Qt::BottomEdge
|
||||
| Qt::RightEdge;
|
||||
|
||||
std::tuple<int, int, int, int> RectEdges(const QRectF &r) {
|
||||
return { r.left(), r.top(), r.left() + r.width(), r.top() + r.height() };
|
||||
}
|
||||
|
||||
QPoint PointOfEdge(Qt::Edges e, const QRectF &r) {
|
||||
switch(e) {
|
||||
case kETL: return QPoint(r.x(), r.y());
|
||||
case kETR: return QPoint(r.x() + r.width(), r.y());
|
||||
case kEBL: return QPoint(r.x(), r.y() + r.height());
|
||||
case kEBR: return QPoint(r.x() + r.width(), r.y() + r.height());
|
||||
default: return QPoint();
|
||||
}
|
||||
}
|
||||
|
||||
QSizeF FlipSizeByRotation(const QSizeF &size, int angle) {
|
||||
return (((angle / 90) % 2) == 1) ? size.transposed() : size;
|
||||
}
|
||||
|
||||
[[nodiscard]] QRectF OriginalCrop(QSize outer, QSize inner) {
|
||||
const auto size = inner.scaled(outer, Qt::KeepAspectRatio);
|
||||
return QRectF(
|
||||
(outer.width() - size.width()) / 2,
|
||||
(outer.height() - size.height()) / 2,
|
||||
size.width(),
|
||||
size.height());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Crop::Crop(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const PhotoModifications &modifications,
|
||||
const QSize &imageSize,
|
||||
EditorData data)
|
||||
: RpWidget(parent)
|
||||
, _pointSize(st::photoEditorCropPointSize)
|
||||
, _pointSizeH(_pointSize / 2.)
|
||||
, _innerMargins(QMarginsF(_pointSizeH, _pointSizeH, _pointSizeH, _pointSizeH)
|
||||
.toMargins())
|
||||
, _offset(_innerMargins.left(), _innerMargins.top())
|
||||
, _edgePointMargins(_pointSizeH, _pointSizeH, -_pointSizeH, -_pointSizeH)
|
||||
, _imageSize(imageSize)
|
||||
, _data(std::move(data))
|
||||
, _cropOriginal(modifications.crop.isValid()
|
||||
? modifications.crop
|
||||
: !_data.exactSize.isEmpty()
|
||||
? OriginalCrop(_imageSize, _data.exactSize)
|
||||
: QRectF(QPoint(), _imageSize))
|
||||
, _angle(modifications.angle)
|
||||
, _flipped(modifications.flipped)
|
||||
, _keepAspectRatio(_data.keepAspectRatio) {
|
||||
|
||||
setMouseTracking(true);
|
||||
|
||||
paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
auto p = QPainter(this);
|
||||
|
||||
p.fillPath(_painterPath, st::photoCropFadeBg);
|
||||
paintPoints(p);
|
||||
}, lifetime());
|
||||
|
||||
}
|
||||
|
||||
void Crop::applyTransform(
|
||||
const QRect &geometry,
|
||||
int angle,
|
||||
bool flipped,
|
||||
const QSizeF &scaledImageSize) {
|
||||
if (geometry.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
setGeometry(geometry);
|
||||
_innerRect = QRectF(_offset, FlipSizeByRotation(scaledImageSize, angle));
|
||||
_ratio.w = scaledImageSize.width() / float64(_imageSize.width());
|
||||
_ratio.h = scaledImageSize.height() / float64(_imageSize.height());
|
||||
_flipped = flipped;
|
||||
_angle = angle;
|
||||
|
||||
const auto cropHolder = QRectF(QPointF(), scaledImageSize);
|
||||
const auto cropHolderCenter = cropHolder.center();
|
||||
|
||||
auto matrix = QTransform()
|
||||
.translate(cropHolderCenter.x(), cropHolderCenter.y())
|
||||
.scale(flipped ? -1 : 1, 1)
|
||||
.rotate(angle)
|
||||
.translate(-cropHolderCenter.x(), -cropHolderCenter.y());
|
||||
|
||||
const auto cropHolderRotated = matrix.mapRect(cropHolder);
|
||||
|
||||
auto cropPaint = matrix
|
||||
.scale(_ratio.w, _ratio.h)
|
||||
.mapRect(_cropOriginal)
|
||||
.translated(
|
||||
-cropHolderRotated.x() + _offset.x(),
|
||||
-cropHolderRotated.y() + _offset.y());
|
||||
|
||||
// Check boundaries.
|
||||
const auto min = float64(st::photoEditorCropMinSize);
|
||||
if ((cropPaint.width() < min) || (cropPaint.height() < min)) {
|
||||
cropPaint.setWidth(std::max(min, cropPaint.width()));
|
||||
cropPaint.setHeight(std::max(min, cropPaint.height()));
|
||||
|
||||
const auto p = cropPaint.center().toPoint();
|
||||
setCropPaint(std::move(cropPaint));
|
||||
|
||||
computeDownState(p);
|
||||
performMove(p);
|
||||
clearDownState();
|
||||
|
||||
convertCropPaintToOriginal();
|
||||
} else {
|
||||
setCropPaint(std::move(cropPaint));
|
||||
}
|
||||
}
|
||||
|
||||
void Crop::paintPoints(QPainter &p) {
|
||||
p.save();
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(st::photoCropPointFg);
|
||||
for (const auto &r : ranges::views::values(_edges)) {
|
||||
p.drawRect(r);
|
||||
}
|
||||
p.restore();
|
||||
}
|
||||
|
||||
void Crop::setCropPaint(QRectF &&rect) {
|
||||
_cropPaint = std::move(rect);
|
||||
|
||||
updateEdges();
|
||||
|
||||
_painterPath.clear();
|
||||
_painterPath.addRect(_innerRect);
|
||||
if (_data.cropType == EditorData::CropType::Ellipse) {
|
||||
_painterPath.addEllipse(_cropPaint);
|
||||
} else if (_data.cropType == EditorData::CropType::RoundedRect) {
|
||||
const auto radius = std::min(_cropPaint.width(), _cropPaint.height())
|
||||
* Ui::ForumUserpicRadiusMultiplier();
|
||||
_painterPath.addRoundedRect(_cropPaint, radius, radius);
|
||||
} else {
|
||||
_painterPath.addRect(_cropPaint);
|
||||
}
|
||||
}
|
||||
|
||||
void Crop::convertCropPaintToOriginal() {
|
||||
const auto cropHolder = QTransform()
|
||||
.scale(_ratio.w, _ratio.h)
|
||||
.mapRect(QRectF(QPointF(), FlipSizeByRotation(_imageSize, _angle)));
|
||||
const auto cropHolderCenter = cropHolder.center();
|
||||
|
||||
const auto matrix = QTransform()
|
||||
.translate(cropHolderCenter.x(), cropHolderCenter.y())
|
||||
.rotate(-_angle)
|
||||
.scale((_flipped ? -1 : 1) * 1. / _ratio.w, 1. / _ratio.h)
|
||||
.translate(-cropHolderCenter.x(), -cropHolderCenter.y());
|
||||
|
||||
const auto cropHolderRotated = matrix.mapRect(cropHolder);
|
||||
|
||||
_cropOriginal = matrix
|
||||
.mapRect(QRectF(_cropPaint).translated(-_offset))
|
||||
.translated(
|
||||
-cropHolderRotated.x(),
|
||||
-cropHolderRotated.y());
|
||||
}
|
||||
|
||||
void Crop::updateEdges() {
|
||||
const auto &s = _pointSize;
|
||||
const auto &m = _edgePointMargins;
|
||||
const auto &r = _cropPaint;
|
||||
for (const auto &e : { kETL, kETR, kEBL, kEBR }) {
|
||||
_edges[e] = QRectF(PointOfEdge(e, r), QSize(s, s)) + m;
|
||||
}
|
||||
}
|
||||
|
||||
Qt::Edges Crop::mouseState(const QPoint &p) {
|
||||
for (const auto &[e, r] : _edges) {
|
||||
if (r.contains(p)) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
if (_cropPaint.contains(p)) {
|
||||
return kEAll;
|
||||
}
|
||||
return Qt::Edges();
|
||||
}
|
||||
|
||||
void Crop::mousePressEvent(QMouseEvent *e) {
|
||||
computeDownState(e->pos());
|
||||
}
|
||||
|
||||
void Crop::mouseReleaseEvent(QMouseEvent *e) {
|
||||
clearDownState();
|
||||
convertCropPaintToOriginal();
|
||||
}
|
||||
|
||||
void Crop::computeDownState(const QPoint &p) {
|
||||
const auto edge = mouseState(p);
|
||||
const auto &inner = _innerRect;
|
||||
const auto &crop = _cropPaint;
|
||||
const auto &[iLeft, iTop, iRight, iBottom] = RectEdges(inner);
|
||||
const auto &[cLeft, cTop, cRight, cBottom] = RectEdges(crop);
|
||||
_down = InfoAtDown{
|
||||
.rect = crop,
|
||||
.edge = edge,
|
||||
.point = (p - PointOfEdge(edge, crop)),
|
||||
.cropRatio = (_cropOriginal.width() / _cropOriginal.height()),
|
||||
.borders = InfoAtDown::Borders{
|
||||
.left = iLeft - cLeft,
|
||||
.right = iRight - cRight,
|
||||
.top = iTop - cTop,
|
||||
.bottom = iBottom - cBottom,
|
||||
}
|
||||
};
|
||||
if (_keepAspectRatio && (edge != kEAll)) {
|
||||
const auto hasLeft = (edge & Qt::LeftEdge);
|
||||
const auto hasTop = (edge & Qt::TopEdge);
|
||||
|
||||
const auto xSign = hasLeft ? -1 : 1;
|
||||
const auto ySign = hasTop ? -1 : 1;
|
||||
|
||||
auto &xSide = (hasLeft ? _down.borders.left : _down.borders.right);
|
||||
auto &ySide = (hasTop ? _down.borders.top : _down.borders.bottom);
|
||||
|
||||
const auto min = std::abs(std::min(xSign * xSide, ySign * ySide));
|
||||
const auto xIsMin = ((xSign * xSide) < (ySign * ySide));
|
||||
xSide = xSign * min;
|
||||
ySide = ySign * min;
|
||||
if (!xIsMin) {
|
||||
xSide *= _down.cropRatio;
|
||||
} else {
|
||||
ySide /= _down.cropRatio;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Crop::clearDownState() {
|
||||
_down = InfoAtDown();
|
||||
}
|
||||
|
||||
void Crop::performCrop(const QPoint &pos) {
|
||||
const auto &crop = _down.rect;
|
||||
const auto &pressedEdge = _down.edge;
|
||||
const auto hasLeft = (pressedEdge & Qt::LeftEdge);
|
||||
const auto hasTop = (pressedEdge & Qt::TopEdge);
|
||||
const auto hasRight = (pressedEdge & Qt::RightEdge);
|
||||
const auto hasBottom = (pressedEdge & Qt::BottomEdge);
|
||||
const auto diff = [&] {
|
||||
auto diff = pos - PointOfEdge(pressedEdge, crop) - _down.point;
|
||||
const auto xFactor = hasLeft ? 1 : -1;
|
||||
const auto yFactor = hasTop ? 1 : -1;
|
||||
const auto &borders = _down.borders;
|
||||
const auto &cropRatio = _down.cropRatio;
|
||||
if (_keepAspectRatio) {
|
||||
const auto diffSign = xFactor * yFactor;
|
||||
diff = (cropRatio != 1.)
|
||||
? QPoint(diff.x(), (1. / cropRatio) * diff.x() * diffSign)
|
||||
// For square/circle.
|
||||
: ((diff.x() * xFactor) < (diff.y() * yFactor))
|
||||
? QPoint(diff.x(), diff.x() * diffSign)
|
||||
: QPoint(diff.y() * diffSign, diff.y());
|
||||
}
|
||||
|
||||
const auto &minSize = st::photoEditorCropMinSize;
|
||||
const auto xMin = xFactor * int(crop.width() - minSize);
|
||||
// const auto xMin = int(xFactor * crop.width()
|
||||
// - xFactor * minSize * ((cropRatio > 1.) ? cropRatio : 1.));
|
||||
const auto yMin = yFactor * int(crop.height() - minSize);
|
||||
// const auto yMin = int(yFactor * crop.height()
|
||||
// - yFactor * minSize * ((cropRatio < 1.) ? (1. / cropRatio) : 1.));
|
||||
|
||||
const auto x = std::clamp(
|
||||
diff.x(),
|
||||
hasLeft ? borders.left : xMin,
|
||||
hasLeft ? xMin : borders.right);
|
||||
const auto y = std::clamp(
|
||||
diff.y(),
|
||||
hasTop ? borders.top : yMin,
|
||||
hasTop ? yMin : borders.bottom);
|
||||
return QPoint(x, y);
|
||||
}();
|
||||
setCropPaint(crop - QMargins(
|
||||
hasLeft ? diff.x() : 0,
|
||||
hasTop ? diff.y() : 0,
|
||||
hasRight ? -diff.x() : 0,
|
||||
hasBottom ? -diff.y() : 0));
|
||||
}
|
||||
|
||||
void Crop::performMove(const QPoint &pos) {
|
||||
const auto &inner = _down.rect;
|
||||
const auto &b = _down.borders;
|
||||
const auto diffX = std::clamp(pos.x() - _down.point.x(), b.left, b.right);
|
||||
const auto diffY = std::clamp(pos.y() - _down.point.y(), b.top, b.bottom);
|
||||
setCropPaint(inner.translated(diffX, diffY));
|
||||
}
|
||||
|
||||
void Crop::mouseMoveEvent(QMouseEvent *e) {
|
||||
const auto pos = e->pos();
|
||||
const auto pressedEdge = _down.edge;
|
||||
|
||||
if (pressedEdge) {
|
||||
if (pressedEdge == kEAll) {
|
||||
performMove(pos);
|
||||
} else if (pressedEdge) {
|
||||
performCrop(pos);
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
const auto edge = pressedEdge ? pressedEdge : mouseState(pos);
|
||||
|
||||
const auto cursor = ((edge == kETL) || (edge == kEBR))
|
||||
? style::cur_sizefdiag
|
||||
: ((edge == kETR) || (edge == kEBL))
|
||||
? style::cur_sizebdiag
|
||||
: (edge == kEAll)
|
||||
? style::cur_sizeall
|
||||
: style::cur_default;
|
||||
setCursor(cursor);
|
||||
}
|
||||
|
||||
style::margins Crop::cropMargins() const {
|
||||
return _innerMargins;
|
||||
}
|
||||
|
||||
QRect Crop::saveCropRect() {
|
||||
const auto savedCrop = _cropOriginal.toRect();
|
||||
return (!savedCrop.topLeft().isNull() || (savedCrop.size() != _imageSize))
|
||||
? savedCrop
|
||||
: QRect();
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
97
Telegram/SourceFiles/editor/editor_crop.h
Normal file
97
Telegram/SourceFiles/editor/editor_crop.h
Normal file
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
#include "base/flat_map.h"
|
||||
#include "editor/photo_editor_common.h"
|
||||
|
||||
namespace Editor {
|
||||
|
||||
// Crop control.
|
||||
class Crop final : public Ui::RpWidget {
|
||||
public:
|
||||
Crop(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const PhotoModifications &modifications,
|
||||
const QSize &imageSize,
|
||||
EditorData type);
|
||||
|
||||
void applyTransform(
|
||||
const QRect &geometry,
|
||||
int angle,
|
||||
bool flipped,
|
||||
const QSizeF &scaledImageSize);
|
||||
[[nodiscard]] QRect saveCropRect();
|
||||
[[nodiscard]] style::margins cropMargins() const;
|
||||
|
||||
protected:
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
|
||||
private:
|
||||
struct InfoAtDown {
|
||||
QRectF rect;
|
||||
Qt::Edges edge;
|
||||
QPoint point;
|
||||
float64 cropRatio = 0.;
|
||||
|
||||
struct Borders {
|
||||
int left = 0;
|
||||
int right = 0;
|
||||
int top = 0;
|
||||
int bottom = 0;
|
||||
} borders;
|
||||
};
|
||||
|
||||
void paintPoints(QPainter &p);
|
||||
|
||||
void updateEdges();
|
||||
[[nodiscard]] QPoint pointOfEdge(Qt::Edges e) const;
|
||||
void setCropPaint(QRectF &&rect);
|
||||
void convertCropPaintToOriginal();
|
||||
|
||||
void computeDownState(const QPoint &p);
|
||||
void clearDownState();
|
||||
[[nodiscard]] Qt::Edges mouseState(const QPoint &p);
|
||||
void performCrop(const QPoint &pos);
|
||||
void performMove(const QPoint &pos);
|
||||
|
||||
const int _pointSize;
|
||||
const float _pointSizeH;
|
||||
const style::margins _innerMargins;
|
||||
const QPoint _offset;
|
||||
const QMarginsF _edgePointMargins;
|
||||
const QSize _imageSize;
|
||||
const EditorData _data;
|
||||
|
||||
base::flat_map<Qt::Edges, QRectF> _edges;
|
||||
|
||||
struct {
|
||||
float64 w = 0.;
|
||||
float64 h = 0.;
|
||||
} _ratio;
|
||||
|
||||
QRectF _cropPaint;
|
||||
QRectF _cropOriginal;
|
||||
QRectF _innerRect;
|
||||
|
||||
QPainterPath _painterPath;
|
||||
|
||||
InfoAtDown _down;
|
||||
|
||||
int _angle = 0;
|
||||
bool _flipped = false;
|
||||
|
||||
bool _keepAspectRatio = false;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
222
Telegram/SourceFiles/editor/editor_layer_widget.cpp
Normal file
222
Telegram/SourceFiles/editor/editor_layer_widget.cpp
Normal file
@@ -0,0 +1,222 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "editor/editor_layer_widget.h"
|
||||
|
||||
#include "ui/painter.h"
|
||||
#include "ui/ui_utility.h"
|
||||
|
||||
#include <QtGui/QGuiApplication>
|
||||
|
||||
namespace Editor {
|
||||
namespace {
|
||||
|
||||
constexpr auto kCacheBackgroundFastTimeout = crl::time(200);
|
||||
constexpr auto kCacheBackgroundFullTimeout = crl::time(1000);
|
||||
constexpr auto kFadeBackgroundDuration = crl::time(200);
|
||||
|
||||
// Thread: Main.
|
||||
[[nodiscard]] bool IsNightMode() {
|
||||
return (st::windowBg->c.lightnessF() < 0.5);
|
||||
}
|
||||
|
||||
[[nodiscard]] QColor BlurOverlayColor(bool night) {
|
||||
return QColor(16, 16, 16, night ? 128 : 192);
|
||||
}
|
||||
|
||||
[[nodiscard]] QImage ProcessBackground(QImage image, bool night) {
|
||||
const auto size = image.size();
|
||||
auto p = QPainter(&image);
|
||||
p.fillRect(
|
||||
QRect(QPoint(), image.size() / image.devicePixelRatio()),
|
||||
BlurOverlayColor(night));
|
||||
p.end();
|
||||
return Images::DitherImage(
|
||||
Images::BlurLargeImage(
|
||||
std::move(image).scaled(
|
||||
size / style::ConvertScale(4),
|
||||
Qt::IgnoreAspectRatio,
|
||||
Qt::SmoothTransformation),
|
||||
24).scaled(
|
||||
size,
|
||||
Qt::IgnoreAspectRatio,
|
||||
Qt::SmoothTransformation));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
LayerWidget::LayerWidget(
|
||||
not_null<QWidget*> parent,
|
||||
base::unique_qptr<Ui::RpWidget> content)
|
||||
: Ui::LayerWidget(parent)
|
||||
, _content(std::move(content))
|
||||
, _backgroundTimer([=] { checkCacheBackground(); }) {
|
||||
_content->setParent(this);
|
||||
_content->show();
|
||||
|
||||
paintRequest(
|
||||
) | rpl::on_next([=](const QRect &clip) {
|
||||
auto p = QPainter(this);
|
||||
const auto faded = _backgroundFade.value(1.);
|
||||
if (faded < 1.) {
|
||||
p.drawImage(rect(), _backgroundBack);
|
||||
if (faded > 0.) {
|
||||
p.setOpacity(faded);
|
||||
p.drawImage(rect(), _background);
|
||||
}
|
||||
} else {
|
||||
p.drawImage(rect(), _background);
|
||||
}
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
bool LayerWidget::eventHook(QEvent *e) {
|
||||
return RpWidget::eventHook(e);
|
||||
}
|
||||
|
||||
void LayerWidget::start() {
|
||||
_backgroundNight = IsNightMode();
|
||||
_background = ProcessBackground(renderBackground(), _backgroundNight);
|
||||
|
||||
sizeValue(
|
||||
) | rpl::on_next([=](const QSize &size) {
|
||||
checkBackgroundStale();
|
||||
_content->resize(size);
|
||||
}, lifetime());
|
||||
|
||||
style::PaletteChanged() | rpl::on_next([=] {
|
||||
checkBackgroundStale();
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void LayerWidget::checkBackgroundStale() {
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
const auto &ready = _backgroundNext.isNull()
|
||||
? _background
|
||||
: _backgroundNext;
|
||||
if (ready.size() == size() * ratio
|
||||
&& _backgroundNight == IsNightMode()) {
|
||||
_backgroundTimer.cancel();
|
||||
} else if (!_backgroundCaching && !_backgroundTimer.isActive()) {
|
||||
_lastAreaChangeTime = crl::now();
|
||||
_backgroundTimer.callOnce(kCacheBackgroundFastTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
QImage LayerWidget::renderBackground() {
|
||||
const auto parent = parentWidget();
|
||||
const auto target = parent->parentWidget();
|
||||
Ui::SendPendingMoveResizeEvents(target);
|
||||
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
auto image = QImage(size() * ratio, QImage::Format_ARGB32_Premultiplied);
|
||||
image.setDevicePixelRatio(ratio);
|
||||
|
||||
const auto shown = !parent->isHidden();
|
||||
const auto focused = shown && Ui::InFocusChain(parent);
|
||||
if (shown) {
|
||||
if (focused) {
|
||||
target->setFocus();
|
||||
}
|
||||
parent->hide();
|
||||
}
|
||||
auto p = QPainter(&image);
|
||||
Ui::RenderWidget(p, target, QPoint(), geometry());
|
||||
p.end();
|
||||
|
||||
if (shown) {
|
||||
parent->show();
|
||||
if (focused) {
|
||||
if (isHidden()) {
|
||||
parent->setFocus();
|
||||
} else {
|
||||
setInnerFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
void LayerWidget::checkCacheBackground() {
|
||||
if (_backgroundCaching || _backgroundTimer.isActive()) {
|
||||
return;
|
||||
}
|
||||
const auto now = crl::now();
|
||||
if (now - _lastAreaChangeTime < kCacheBackgroundFullTimeout
|
||||
&& QGuiApplication::mouseButtons() != 0) {
|
||||
_backgroundTimer.callOnce(kCacheBackgroundFastTimeout);
|
||||
return;
|
||||
}
|
||||
cacheBackground();
|
||||
}
|
||||
|
||||
void LayerWidget::cacheBackground() {
|
||||
_backgroundCaching = true;
|
||||
const auto weak = base::make_weak(this);
|
||||
const auto night = IsNightMode();
|
||||
crl::async([weak, night, image = renderBackground()]() mutable {
|
||||
auto result = ProcessBackground(image, night);
|
||||
crl::on_main([weak, night, result = std::move(result)]() mutable {
|
||||
if (const auto strong = weak.get()) {
|
||||
strong->backgroundReady(std::move(result), night);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void LayerWidget::backgroundReady(QImage background, bool night) {
|
||||
_backgroundCaching = false;
|
||||
|
||||
const auto required = size() * style::DevicePixelRatio();
|
||||
if (background.size() == required && night == IsNightMode()) {
|
||||
_backgroundNext = std::move(background);
|
||||
_backgroundNight = night;
|
||||
if (!_backgroundFade.animating()) {
|
||||
startBackgroundFade();
|
||||
}
|
||||
update();
|
||||
} else if (_background.size() != required) {
|
||||
_backgroundTimer.callOnce(kCacheBackgroundFastTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
void LayerWidget::startBackgroundFade() {
|
||||
if (_backgroundNext.isNull()) {
|
||||
return;
|
||||
}
|
||||
_backgroundBack = std::move(_background);
|
||||
_background = base::take(_backgroundNext);
|
||||
_backgroundFade.start([=] {
|
||||
update();
|
||||
if (!_backgroundFade.animating()) {
|
||||
_backgroundBack = QImage();
|
||||
startBackgroundFade();
|
||||
}
|
||||
}, 0., 1., kFadeBackgroundDuration);
|
||||
}
|
||||
|
||||
void LayerWidget::parentResized() {
|
||||
resizeToWidth(parentWidget()->width());
|
||||
if (_background.isNull()) {
|
||||
start();
|
||||
}
|
||||
}
|
||||
|
||||
void LayerWidget::keyPressEvent(QKeyEvent *e) {
|
||||
QGuiApplication::sendEvent(_content.get(), e);
|
||||
}
|
||||
|
||||
int LayerWidget::resizeGetHeight(int newWidth) {
|
||||
return parentWidget()->height();
|
||||
}
|
||||
|
||||
bool LayerWidget::closeByOutsideClick() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
59
Telegram/SourceFiles/editor/editor_layer_widget.h
Normal file
59
Telegram/SourceFiles/editor/editor_layer_widget.h
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/layers/layer_widget.h"
|
||||
#include "ui/image/image.h"
|
||||
#include "editor/photo_editor_common.h"
|
||||
#include "base/unique_qptr.h"
|
||||
#include "base/timer.h"
|
||||
|
||||
enum class ImageRoundRadius;
|
||||
|
||||
namespace Window {
|
||||
class Controller;
|
||||
class SessionController;
|
||||
} // namespace Window
|
||||
|
||||
namespace Editor {
|
||||
|
||||
class LayerWidget final : public Ui::LayerWidget {
|
||||
public:
|
||||
LayerWidget(
|
||||
not_null<QWidget*> parent,
|
||||
base::unique_qptr<Ui::RpWidget> content);
|
||||
|
||||
void parentResized() override;
|
||||
bool closeByOutsideClick() const override;
|
||||
|
||||
private:
|
||||
bool eventHook(QEvent *e) override;
|
||||
void keyPressEvent(QKeyEvent *e) override;
|
||||
int resizeGetHeight(int newWidth) override;
|
||||
|
||||
void start();
|
||||
void cacheBackground();
|
||||
void checkBackgroundStale();
|
||||
void checkCacheBackground();
|
||||
[[nodiscard]] QImage renderBackground();
|
||||
void backgroundReady(QImage background, bool night);
|
||||
void startBackgroundFade();
|
||||
|
||||
const base::unique_qptr<Ui::RpWidget> _content;
|
||||
QImage _backgroundBack;
|
||||
QImage _background;
|
||||
QImage _backgroundNext;
|
||||
Ui::Animations::Simple _backgroundFade;
|
||||
base::Timer _backgroundTimer;
|
||||
crl::time _lastAreaChangeTime = 0;
|
||||
bool _backgroundCaching = false;
|
||||
bool _backgroundNight = false;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
240
Telegram/SourceFiles/editor/editor_paint.cpp
Normal file
240
Telegram/SourceFiles/editor/editor_paint.cpp
Normal file
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "editor/editor_paint.h"
|
||||
|
||||
#include "core/mime_type.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "editor/controllers/controllers.h"
|
||||
#include "editor/scene/scene.h"
|
||||
#include "editor/scene/scene_item_canvas.h"
|
||||
#include "editor/scene/scene_item_image.h"
|
||||
#include "editor/scene/scene_item_sticker.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "lottie/lottie_single_player.h"
|
||||
#include "storage/storage_media_prepare.h"
|
||||
#include "ui/chat/attach/attach_prepare.h"
|
||||
#include "ui/ui_utility.h"
|
||||
|
||||
#include <QGraphicsView>
|
||||
#include <QtCore/QMimeData>
|
||||
|
||||
namespace Editor {
|
||||
namespace {
|
||||
|
||||
constexpr auto kMaxBrush = 25.;
|
||||
constexpr auto kMinBrush = 1.;
|
||||
|
||||
std::shared_ptr<Scene> EnsureScene(
|
||||
PhotoModifications &mods,
|
||||
const QSize &size) {
|
||||
if (!mods.paint) {
|
||||
mods.paint = std::make_shared<Scene>(QRectF(QPointF(), size));
|
||||
}
|
||||
return mods.paint;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
using ItemPtr = std::shared_ptr<QGraphicsItem>;
|
||||
|
||||
Paint::Paint(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
PhotoModifications &modifications,
|
||||
const QSize &imageSize,
|
||||
std::shared_ptr<Controllers> controllers)
|
||||
: RpWidget(parent)
|
||||
, _controllers(controllers)
|
||||
, _scene(EnsureScene(modifications, imageSize))
|
||||
, _view(base::make_unique_q<QGraphicsView>(_scene.get(), this))
|
||||
, _imageSize(imageSize) {
|
||||
Expects(modifications.paint != nullptr);
|
||||
|
||||
keepResult();
|
||||
|
||||
_view->show();
|
||||
_view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
_view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
_view->setFrameStyle(int(QFrame::NoFrame) | QFrame::Plain);
|
||||
_view->viewport()->setAutoFillBackground(false);
|
||||
|
||||
// Undo / Redo.
|
||||
controllers->undoController->performRequestChanges(
|
||||
) | rpl::on_next([=](const Undo &command) {
|
||||
if (command == Undo::Undo) {
|
||||
_scene->performUndo();
|
||||
} else {
|
||||
_scene->performRedo();
|
||||
}
|
||||
|
||||
_hasUndo = _scene->hasUndo();
|
||||
_hasRedo = _scene->hasRedo();
|
||||
}, lifetime());
|
||||
|
||||
controllers->undoController->setCanPerformChanges(rpl::merge(
|
||||
_hasUndo.value() | rpl::map([](bool enable) {
|
||||
return UndoController::EnableRequest{
|
||||
.command = Undo::Undo,
|
||||
.enable = enable,
|
||||
};
|
||||
}),
|
||||
_hasRedo.value() | rpl::map([](bool enable) {
|
||||
return UndoController::EnableRequest{
|
||||
.command = Undo::Redo,
|
||||
.enable = enable,
|
||||
};
|
||||
})));
|
||||
|
||||
if (controllers->stickersPanelController) {
|
||||
using ShowRequest = StickersPanelController::ShowRequest;
|
||||
|
||||
controllers->stickersPanelController->setShowRequestChanges(
|
||||
controllers->stickersPanelController->stickerChosen(
|
||||
) | rpl::map_to(ShowRequest::HideAnimated));
|
||||
|
||||
controllers->stickersPanelController->stickerChosen(
|
||||
) | rpl::on_next([=](not_null<DocumentData*> document) {
|
||||
const auto item = std::make_shared<ItemSticker>(
|
||||
document,
|
||||
itemBaseData());
|
||||
_scene->addItem(item);
|
||||
_scene->clearSelection();
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
rpl::merge(
|
||||
controllers->stickersPanelController
|
||||
? controllers->stickersPanelController->stickerChosen(
|
||||
) | rpl::to_empty
|
||||
: rpl::never<>() | rpl::type_erased,
|
||||
_scene->addsItem()
|
||||
) | rpl::on_next([=] {
|
||||
clearRedoList();
|
||||
updateUndoState();
|
||||
}, lifetime());
|
||||
|
||||
_scene->removesItem(
|
||||
) | rpl::on_next([=] {
|
||||
updateUndoState();
|
||||
}, lifetime());
|
||||
|
||||
}
|
||||
|
||||
void Paint::applyTransform(QRect geometry, int angle, bool flipped) {
|
||||
if (geometry.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
setGeometry(geometry);
|
||||
const auto size = geometry.size();
|
||||
|
||||
const auto rotatedImageSize = QTransform()
|
||||
.rotate(angle)
|
||||
.mapRect(QRect(QPoint(), _imageSize));
|
||||
|
||||
const auto ratioW = size.width() / float64(rotatedImageSize.width())
|
||||
* (flipped ? -1 : 1);
|
||||
const auto ratioH = size.height() / float64(rotatedImageSize.height());
|
||||
|
||||
_view->setTransform(QTransform().scale(ratioW, ratioH).rotate(angle));
|
||||
_view->setGeometry(QRect(QPoint(), size));
|
||||
|
||||
_transform = {
|
||||
.angle = angle,
|
||||
.flipped = flipped,
|
||||
.zoom = size.width() / float64(_scene->sceneRect().width()),
|
||||
};
|
||||
_scene->updateZoom(_transform.zoom);
|
||||
}
|
||||
|
||||
std::shared_ptr<Scene> Paint::saveScene() const {
|
||||
_scene->save(SaveState::Save);
|
||||
return _scene->items().empty()
|
||||
? nullptr
|
||||
: _scene;
|
||||
}
|
||||
|
||||
void Paint::restoreScene() {
|
||||
_scene->restore(SaveState::Save);
|
||||
}
|
||||
|
||||
void Paint::cancel() {
|
||||
_scene->restore(SaveState::Keep);
|
||||
}
|
||||
|
||||
void Paint::keepResult() {
|
||||
_scene->save(SaveState::Keep);
|
||||
}
|
||||
|
||||
void Paint::clearRedoList() {
|
||||
_scene->clearRedoList();
|
||||
|
||||
_hasRedo = false;
|
||||
}
|
||||
|
||||
void Paint::updateUndoState() {
|
||||
_hasUndo = _scene->hasUndo();
|
||||
_hasRedo = _scene->hasRedo();
|
||||
}
|
||||
|
||||
void Paint::applyBrush(const Brush &brush) {
|
||||
_scene->applyBrush(
|
||||
brush.color,
|
||||
(kMinBrush + float64(kMaxBrush - kMinBrush) * brush.sizeRatio));
|
||||
}
|
||||
|
||||
void Paint::handleMimeData(const QMimeData *data) {
|
||||
const auto add = [&](QImage image) {
|
||||
if (image.isNull()) {
|
||||
return;
|
||||
}
|
||||
if (!Ui::ValidateThumbDimensions(image.width(), image.height())) {
|
||||
_controllers->show->showBox(
|
||||
Ui::MakeInformBox(tr::lng_edit_media_invalid_file()));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto item = std::make_shared<ItemImage>(
|
||||
Ui::PixmapFromImage(std::move(image)),
|
||||
itemBaseData());
|
||||
_scene->addItem(item);
|
||||
_scene->clearSelection();
|
||||
};
|
||||
|
||||
using Error = Ui::PreparedList::Error;
|
||||
const auto premium = false; // Don't support > 2GB files here.
|
||||
const auto list = Core::ReadMimeUrls(data);
|
||||
auto result = !list.isEmpty()
|
||||
? Storage::PrepareMediaList(
|
||||
list.mid(0, 1),
|
||||
_imageSize.width() / 2,
|
||||
premium)
|
||||
: Ui::PreparedList(Error::EmptyFile, QString());
|
||||
if (result.error == Error::None) {
|
||||
add(base::take(result.files.front().preview));
|
||||
} else if (auto read = Core::ReadMimeImage(data)) {
|
||||
add(std::move(read.image));
|
||||
}
|
||||
}
|
||||
|
||||
ItemBase::Data Paint::itemBaseData() const {
|
||||
const auto s = _scene->sceneRect().toRect().size();
|
||||
const auto size = std::min(s.width(), s.height()) / 2;
|
||||
const auto x = s.width() / 2;
|
||||
const auto y = s.height() / 2;
|
||||
return ItemBase::Data{
|
||||
.initialZoom = _transform.zoom,
|
||||
.zPtr = _scene->lastZ(),
|
||||
.size = size,
|
||||
.x = x,
|
||||
.y = y,
|
||||
.flipped = _transform.flipped,
|
||||
.rotation = -_transform.angle,
|
||||
.imageSize = _imageSize,
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
71
Telegram/SourceFiles/editor/editor_paint.h
Normal file
71
Telegram/SourceFiles/editor/editor_paint.h
Normal file
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
#include "editor/photo_editor_common.h"
|
||||
#include "editor/photo_editor_inner_common.h"
|
||||
#include "editor/scene/scene_item_base.h"
|
||||
|
||||
class QGraphicsItem;
|
||||
class QGraphicsView;
|
||||
|
||||
namespace Editor {
|
||||
|
||||
struct Controllers;
|
||||
class Scene;
|
||||
|
||||
// Paint control.
|
||||
class Paint final : public Ui::RpWidget {
|
||||
public:
|
||||
Paint(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
PhotoModifications &modifications,
|
||||
const QSize &imageSize,
|
||||
std::shared_ptr<Controllers> controllers);
|
||||
|
||||
[[nodiscard]] std::shared_ptr<Scene> saveScene() const;
|
||||
void restoreScene();
|
||||
|
||||
void applyTransform(QRect geometry, int angle, bool flipped);
|
||||
void applyBrush(const Brush &brush);
|
||||
void cancel();
|
||||
void keepResult();
|
||||
void updateUndoState();
|
||||
|
||||
void handleMimeData(const QMimeData *data);
|
||||
|
||||
private:
|
||||
struct SavedItem {
|
||||
std::shared_ptr<QGraphicsItem> item;
|
||||
bool undid = false;
|
||||
};
|
||||
|
||||
ItemBase::Data itemBaseData() const;
|
||||
|
||||
void clearRedoList();
|
||||
|
||||
const std::shared_ptr<Controllers> _controllers;
|
||||
const std::shared_ptr<Scene> _scene;
|
||||
const base::unique_qptr<QGraphicsView> _view;
|
||||
const QSize _imageSize;
|
||||
|
||||
struct {
|
||||
int angle = 0;
|
||||
bool flipped = false;
|
||||
float64 zoom = 0.;
|
||||
} _transform;
|
||||
|
||||
rpl::variable<bool> _hasUndo = true;
|
||||
rpl::variable<bool> _hasRedo = true;
|
||||
|
||||
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
242
Telegram/SourceFiles/editor/photo_editor.cpp
Normal file
242
Telegram/SourceFiles/editor/photo_editor.cpp
Normal file
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "editor/photo_editor.h"
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/core_settings.h"
|
||||
#include "editor/color_picker.h"
|
||||
#include "editor/controllers/controllers.h"
|
||||
#include "editor/photo_editor_content.h"
|
||||
#include "editor/photo_editor_controls.h"
|
||||
#include "window/window_controller.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "ui/layers/layer_widget.h"
|
||||
#include "styles/style_editor.h"
|
||||
|
||||
namespace Editor {
|
||||
namespace {
|
||||
|
||||
constexpr auto kPrecision = 100000;
|
||||
|
||||
[[nodiscard]] QByteArray Serialize(const Brush &brush) {
|
||||
auto result = QByteArray();
|
||||
auto stream = QDataStream(&result, QIODevice::WriteOnly);
|
||||
stream.setVersion(QDataStream::Qt_5_3);
|
||||
stream << qint32(brush.sizeRatio * kPrecision) << brush.color;
|
||||
stream.device()->close();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] Brush Deserialize(const QByteArray &data) {
|
||||
auto stream = QDataStream(data);
|
||||
auto result = Brush();
|
||||
auto size = qint32(0);
|
||||
stream >> size >> result.color;
|
||||
result.sizeRatio = size / float(kPrecision);
|
||||
return (stream.status() != QDataStream::Ok)
|
||||
? Brush()
|
||||
: result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PhotoEditor::PhotoEditor(
|
||||
not_null<QWidget*> parent,
|
||||
not_null<Window::Controller*> controller,
|
||||
std::shared_ptr<Image> photo,
|
||||
PhotoModifications modifications,
|
||||
EditorData data)
|
||||
: PhotoEditor(
|
||||
parent,
|
||||
controller->uiShow(),
|
||||
(controller->sessionController()
|
||||
? controller->sessionController()->uiShow()
|
||||
: nullptr),
|
||||
std::move(photo),
|
||||
std::move(modifications),
|
||||
std::move(data)) {
|
||||
}
|
||||
|
||||
PhotoEditor::PhotoEditor(
|
||||
not_null<QWidget*> parent,
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
std::shared_ptr<ChatHelpers::Show> sessionShow,
|
||||
std::shared_ptr<Image> photo,
|
||||
PhotoModifications modifications,
|
||||
EditorData data)
|
||||
: RpWidget(parent)
|
||||
, _modifications(std::move(modifications))
|
||||
, _controllers(std::make_shared<Controllers>(
|
||||
sessionShow
|
||||
? std::make_unique<StickersPanelController>(
|
||||
this,
|
||||
std::move(sessionShow))
|
||||
: nullptr,
|
||||
std::make_unique<UndoController>(),
|
||||
std::move(show)))
|
||||
, _content(base::make_unique_q<PhotoEditorContent>(
|
||||
this,
|
||||
photo,
|
||||
_modifications,
|
||||
_controllers,
|
||||
data))
|
||||
, _controls(base::make_unique_q<PhotoEditorControls>(
|
||||
this,
|
||||
_controllers,
|
||||
_modifications,
|
||||
data))
|
||||
, _colorPicker(std::make_unique<ColorPicker>(
|
||||
this,
|
||||
Deserialize(Core::App().settings().photoEditorBrush()))) {
|
||||
|
||||
sizeValue(
|
||||
) | rpl::on_next([=](const QSize &size) {
|
||||
if (size.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
_content->setGeometry(rect() - st::photoEditorContentMargins);
|
||||
}, lifetime());
|
||||
|
||||
_content->innerRect(
|
||||
) | rpl::on_next([=](QRect inner) {
|
||||
if (inner.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const auto innerTop = _content->y() + inner.top();
|
||||
const auto skip = st::photoEditorCropPointSize;
|
||||
const auto controlsRect = rect()
|
||||
- style::margins(0, innerTop + inner.height() + skip, 0, 0);
|
||||
_controls->setGeometry(controlsRect);
|
||||
}, lifetime());
|
||||
|
||||
_controls->colorLinePositionValue(
|
||||
) | rpl::on_next([=](const QPoint &p) {
|
||||
_colorPicker->moveLine(p);
|
||||
}, _controls->lifetime());
|
||||
|
||||
_controls->colorLineShownValue(
|
||||
) | rpl::on_next([=](bool shown) {
|
||||
_colorPicker->setVisible(shown);
|
||||
}, _controls->lifetime());
|
||||
|
||||
_mode.value(
|
||||
) | rpl::on_next([=](const PhotoEditorMode &mode) {
|
||||
_content->applyMode(mode);
|
||||
_controls->applyMode(mode);
|
||||
}, lifetime());
|
||||
|
||||
_controls->rotateRequests(
|
||||
) | rpl::on_next([=](int angle) {
|
||||
_modifications.angle += 90;
|
||||
if (_modifications.angle >= 360) {
|
||||
_modifications.angle -= 360;
|
||||
}
|
||||
_content->applyModifications(_modifications);
|
||||
}, lifetime());
|
||||
|
||||
_controls->flipRequests(
|
||||
) | rpl::on_next([=] {
|
||||
_modifications.flipped = !_modifications.flipped;
|
||||
_content->applyModifications(_modifications);
|
||||
}, lifetime());
|
||||
|
||||
_controls->paintModeRequests(
|
||||
) | rpl::on_next([=] {
|
||||
_mode = PhotoEditorMode{
|
||||
.mode = PhotoEditorMode::Mode::Paint,
|
||||
.action = PhotoEditorMode::Action::None,
|
||||
};
|
||||
}, lifetime());
|
||||
|
||||
_controls->doneRequests(
|
||||
) | rpl::on_next([=] {
|
||||
const auto mode = _mode.current().mode;
|
||||
if (mode == PhotoEditorMode::Mode::Paint) {
|
||||
_mode = PhotoEditorMode{
|
||||
.mode = PhotoEditorMode::Mode::Transform,
|
||||
.action = PhotoEditorMode::Action::Save,
|
||||
};
|
||||
} else if (mode == PhotoEditorMode::Mode::Transform) {
|
||||
_mode = PhotoEditorMode{
|
||||
.mode = PhotoEditorMode::Mode::Out,
|
||||
.action = PhotoEditorMode::Action::Save,
|
||||
};
|
||||
save();
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
_controls->cancelRequests(
|
||||
) | rpl::on_next([=] {
|
||||
const auto mode = _mode.current().mode;
|
||||
if (mode == PhotoEditorMode::Mode::Paint) {
|
||||
_mode = PhotoEditorMode{
|
||||
.mode = PhotoEditorMode::Mode::Transform,
|
||||
.action = PhotoEditorMode::Action::Discard,
|
||||
};
|
||||
} else if (mode == PhotoEditorMode::Mode::Transform) {
|
||||
_mode = PhotoEditorMode{
|
||||
.mode = PhotoEditorMode::Mode::Out,
|
||||
.action = PhotoEditorMode::Action::Discard,
|
||||
};
|
||||
_cancel.fire({});
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
_colorPicker->saveBrushRequests(
|
||||
) | rpl::on_next([=](const Brush &brush) {
|
||||
_content->applyBrush(brush);
|
||||
|
||||
const auto serialized = Serialize(brush);
|
||||
if (Core::App().settings().photoEditorBrush() != serialized) {
|
||||
Core::App().settings().setPhotoEditorBrush(serialized);
|
||||
Core::App().saveSettingsDelayed();
|
||||
}
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void PhotoEditor::keyPressEvent(QKeyEvent *e) {
|
||||
if (!_colorPicker->preventHandleKeyPress()) {
|
||||
_content->handleKeyPress(e) || _controls->handleKeyPress(e);
|
||||
}
|
||||
}
|
||||
|
||||
void PhotoEditor::save() {
|
||||
_content->save(_modifications);
|
||||
_done.fire_copy(_modifications);
|
||||
}
|
||||
|
||||
rpl::producer<PhotoModifications> PhotoEditor::doneRequests() const {
|
||||
return _done.events();
|
||||
}
|
||||
|
||||
rpl::producer<> PhotoEditor::cancelRequests() const {
|
||||
return _cancel.events();
|
||||
}
|
||||
|
||||
void InitEditorLayer(
|
||||
not_null<Ui::LayerWidget*> layer,
|
||||
not_null<PhotoEditor*> editor,
|
||||
Fn<void(PhotoModifications)> doneCallback) {
|
||||
editor->cancelRequests(
|
||||
) | rpl::on_next([=] {
|
||||
layer->closeLayer();
|
||||
}, editor->lifetime());
|
||||
|
||||
const auto weak = base::make_weak(layer.get());
|
||||
editor->doneRequests(
|
||||
) | rpl::on_next([=, done = std::move(doneCallback)](
|
||||
const PhotoModifications &mods) {
|
||||
done(mods);
|
||||
if (const auto strong = weak.get()) {
|
||||
strong->closeLayer();
|
||||
}
|
||||
}, editor->lifetime());
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
81
Telegram/SourceFiles/editor/photo_editor.h
Normal file
81
Telegram/SourceFiles/editor/photo_editor.h
Normal file
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/image/image.h"
|
||||
#include "base/unique_qptr.h"
|
||||
#include "editor/photo_editor_common.h"
|
||||
#include "editor/photo_editor_inner_common.h"
|
||||
|
||||
namespace Ui {
|
||||
class LayerWidget;
|
||||
class Show;
|
||||
} // namespace Ui
|
||||
|
||||
namespace ChatHelpers {
|
||||
class Show;
|
||||
} // namespace ChatHelpers
|
||||
|
||||
namespace Window {
|
||||
class Controller;
|
||||
} // namespace Window
|
||||
|
||||
namespace Editor {
|
||||
|
||||
class ColorPicker;
|
||||
class PhotoEditorContent;
|
||||
class PhotoEditorControls;
|
||||
struct Controllers;
|
||||
|
||||
class PhotoEditor final : public Ui::RpWidget {
|
||||
public:
|
||||
PhotoEditor(
|
||||
not_null<QWidget*> parent,
|
||||
not_null<Window::Controller*> controller,
|
||||
std::shared_ptr<Image> photo,
|
||||
PhotoModifications modifications,
|
||||
EditorData data = EditorData());
|
||||
PhotoEditor(
|
||||
not_null<QWidget*> parent,
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
std::shared_ptr<ChatHelpers::Show> sessionShow,
|
||||
std::shared_ptr<Image> photo,
|
||||
PhotoModifications modifications,
|
||||
EditorData data = EditorData());
|
||||
|
||||
void save();
|
||||
[[nodiscard]] rpl::producer<PhotoModifications> doneRequests() const;
|
||||
[[nodiscard]] rpl::producer<> cancelRequests() const;
|
||||
|
||||
private:
|
||||
void keyPressEvent(QKeyEvent *e) override;
|
||||
|
||||
PhotoModifications _modifications;
|
||||
|
||||
const std::shared_ptr<Controllers> _controllers;
|
||||
|
||||
base::unique_qptr<PhotoEditorContent> _content;
|
||||
base::unique_qptr<PhotoEditorControls> _controls;
|
||||
const std::unique_ptr<ColorPicker> _colorPicker;
|
||||
|
||||
rpl::variable<PhotoEditorMode> _mode = PhotoEditorMode{
|
||||
.mode = PhotoEditorMode::Mode::Transform,
|
||||
.action = PhotoEditorMode::Action::None,
|
||||
};
|
||||
rpl::event_stream<PhotoModifications> _done;
|
||||
rpl::event_stream<> _cancel;
|
||||
|
||||
};
|
||||
|
||||
void InitEditorLayer(
|
||||
not_null<Ui::LayerWidget*> layer,
|
||||
not_null<PhotoEditor*> editor,
|
||||
Fn<void(PhotoModifications)> doneCallback);
|
||||
|
||||
} // namespace Editor
|
||||
59
Telegram/SourceFiles/editor/photo_editor_common.cpp
Normal file
59
Telegram/SourceFiles/editor/photo_editor_common.cpp
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "editor/photo_editor_common.h"
|
||||
|
||||
#include "editor/scene/scene.h"
|
||||
#include "ui/painter.h"
|
||||
|
||||
namespace Editor {
|
||||
|
||||
QImage ImageModified(QImage image, const PhotoModifications &mods) {
|
||||
Expects(!image.isNull());
|
||||
|
||||
if (!mods) {
|
||||
return image;
|
||||
}
|
||||
if (mods.paint) {
|
||||
if (image.format() != QImage::Format_ARGB32_Premultiplied) {
|
||||
image = image.convertToFormat(
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
}
|
||||
|
||||
Painter p(&image);
|
||||
PainterHighQualityEnabler hq(p);
|
||||
|
||||
mods.paint->render(&p, image.rect());
|
||||
}
|
||||
auto cropped = mods.crop.isValid()
|
||||
? image.copy(mods.crop)
|
||||
: image;
|
||||
QTransform transform;
|
||||
if (mods.flipped) {
|
||||
transform.scale(-1, 1);
|
||||
}
|
||||
if (mods.angle) {
|
||||
transform.rotate(mods.angle);
|
||||
}
|
||||
return cropped.transformed(transform);
|
||||
}
|
||||
|
||||
bool PhotoModifications::empty() const {
|
||||
return !angle && !flipped && !crop.isValid() && !paint;
|
||||
}
|
||||
|
||||
PhotoModifications::operator bool() const {
|
||||
return !empty();
|
||||
}
|
||||
|
||||
PhotoModifications::~PhotoModifications() {
|
||||
if (paint && (paint.use_count() == 1)) {
|
||||
paint->deleteLater();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
44
Telegram/SourceFiles/editor/photo_editor_common.h
Normal file
44
Telegram/SourceFiles/editor/photo_editor_common.h
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Editor {
|
||||
|
||||
class Scene;
|
||||
|
||||
struct PhotoModifications {
|
||||
int angle = 0;
|
||||
bool flipped = false;
|
||||
QRect crop;
|
||||
std::shared_ptr<Scene> paint = nullptr;
|
||||
|
||||
[[nodiscard]] bool empty() const;
|
||||
[[nodiscard]] explicit operator bool() const;
|
||||
~PhotoModifications();
|
||||
|
||||
};
|
||||
|
||||
struct EditorData {
|
||||
enum class CropType {
|
||||
Rect,
|
||||
Ellipse,
|
||||
RoundedRect,
|
||||
};
|
||||
|
||||
TextWithEntities about;
|
||||
QString confirm;
|
||||
QSize exactSize;
|
||||
CropType cropType = CropType::Rect;
|
||||
bool keepAspectRatio = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] QImage ImageModified(
|
||||
QImage image,
|
||||
const PhotoModifications &mods);
|
||||
|
||||
} // namespace Editor
|
||||
166
Telegram/SourceFiles/editor/photo_editor_content.cpp
Normal file
166
Telegram/SourceFiles/editor/photo_editor_content.cpp
Normal file
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "editor/photo_editor_content.h"
|
||||
|
||||
#include "editor/editor_crop.h"
|
||||
#include "editor/editor_paint.h"
|
||||
#include "history/history_drag_area.h"
|
||||
#include "media/view/media_view_pip.h"
|
||||
#include "storage/storage_media_prepare.h"
|
||||
|
||||
namespace Editor {
|
||||
|
||||
using Media::View::FlipSizeByRotation;
|
||||
using Media::View::RotatedRect;
|
||||
|
||||
PhotoEditorContent::PhotoEditorContent(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
std::shared_ptr<Image> photo,
|
||||
PhotoModifications modifications,
|
||||
std::shared_ptr<Controllers> controllers,
|
||||
EditorData data)
|
||||
: RpWidget(parent)
|
||||
, _photoSize(photo->size())
|
||||
, _paint(base::make_unique_q<Paint>(
|
||||
this,
|
||||
modifications,
|
||||
_photoSize,
|
||||
std::move(controllers)))
|
||||
, _crop(base::make_unique_q<Crop>(
|
||||
this,
|
||||
modifications,
|
||||
_photoSize,
|
||||
std::move(data)))
|
||||
, _photo(std::move(photo))
|
||||
, _modifications(modifications) {
|
||||
|
||||
rpl::combine(
|
||||
_modifications.value(),
|
||||
sizeValue()
|
||||
) | rpl::on_next([=](
|
||||
const PhotoModifications &mods, const QSize &size) {
|
||||
if (size.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const auto imageSizeF = [&] {
|
||||
const auto rotatedSize
|
||||
= FlipSizeByRotation(size, mods.angle);
|
||||
const auto m = _crop->cropMargins();
|
||||
const auto sizeForCrop = rotatedSize
|
||||
- QSize(m.left() + m.right(), m.top() + m.bottom());
|
||||
const auto originalSize = QSizeF(_photoSize);
|
||||
if ((originalSize.width() > sizeForCrop.width())
|
||||
|| (originalSize.height() > sizeForCrop.height())) {
|
||||
return originalSize.scaled(
|
||||
sizeForCrop,
|
||||
Qt::KeepAspectRatio);
|
||||
}
|
||||
return originalSize;
|
||||
}();
|
||||
const auto imageSize = QSize(imageSizeF.width(), imageSizeF.height());
|
||||
_imageRect = QRect(
|
||||
QPoint(-imageSize.width() / 2, -imageSize.height() / 2),
|
||||
imageSize);
|
||||
|
||||
_imageMatrix.reset();
|
||||
_imageMatrix.translate(size.width() / 2, size.height() / 2);
|
||||
if (mods.flipped) {
|
||||
_imageMatrix.scale(-1, 1);
|
||||
}
|
||||
_imageMatrix.rotate(mods.angle);
|
||||
|
||||
const auto geometry = _imageMatrix.mapRect(_imageRect);
|
||||
_crop->applyTransform(
|
||||
geometry + _crop->cropMargins(),
|
||||
mods.angle,
|
||||
mods.flipped, imageSizeF);
|
||||
_paint->applyTransform(geometry, mods.angle, mods.flipped);
|
||||
|
||||
_innerRect = geometry;
|
||||
}, lifetime());
|
||||
|
||||
paintRequest(
|
||||
) | rpl::on_next([=](const QRect &clip) {
|
||||
auto p = QPainter(this);
|
||||
|
||||
p.fillRect(clip, Qt::transparent);
|
||||
p.setTransform(_imageMatrix);
|
||||
p.drawPixmap(_imageRect, _photo->pix(_imageRect.size()));
|
||||
}, lifetime());
|
||||
|
||||
setupDragArea();
|
||||
}
|
||||
|
||||
void PhotoEditorContent::applyModifications(
|
||||
PhotoModifications modifications) {
|
||||
_modifications = std::move(modifications);
|
||||
update();
|
||||
}
|
||||
|
||||
void PhotoEditorContent::save(PhotoModifications &modifications) {
|
||||
modifications.crop = _crop->saveCropRect();
|
||||
_paint->keepResult();
|
||||
|
||||
const auto savedScene = _paint->saveScene();
|
||||
if (!modifications.paint) {
|
||||
modifications.paint = savedScene;
|
||||
}
|
||||
}
|
||||
|
||||
void PhotoEditorContent::applyMode(const PhotoEditorMode &mode) {
|
||||
if (mode.mode == PhotoEditorMode::Mode::Out) {
|
||||
if (mode.action == PhotoEditorMode::Action::Discard) {
|
||||
_paint->restoreScene();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const auto isTransform = (mode.mode == PhotoEditorMode::Mode::Transform);
|
||||
_crop->setVisible(isTransform);
|
||||
|
||||
_paint->setAttribute(Qt::WA_TransparentForMouseEvents, isTransform);
|
||||
if (!isTransform) {
|
||||
_paint->updateUndoState();
|
||||
}
|
||||
|
||||
if (mode.action == PhotoEditorMode::Action::Discard) {
|
||||
_paint->cancel();
|
||||
} else if (mode.action == PhotoEditorMode::Action::Save) {
|
||||
_paint->keepResult();
|
||||
}
|
||||
_mode = mode;
|
||||
}
|
||||
|
||||
void PhotoEditorContent::applyBrush(const Brush &brush) {
|
||||
_paint->applyBrush(brush);
|
||||
}
|
||||
|
||||
bool PhotoEditorContent::handleKeyPress(not_null<QKeyEvent*> e) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
void PhotoEditorContent::setupDragArea() {
|
||||
auto dragEnterFilter = [=](const QMimeData *data) {
|
||||
return (_mode.mode == PhotoEditorMode::Mode::Paint)
|
||||
? Storage::ValidatePhotoEditorMediaDragData(data)
|
||||
: false;
|
||||
};
|
||||
|
||||
const auto areas = DragArea::SetupDragAreaToContainer(
|
||||
this,
|
||||
std::move(dragEnterFilter),
|
||||
nullptr,
|
||||
nullptr,
|
||||
[](const QMimeData *d) { return Storage::MimeDataState::Image; },
|
||||
true);
|
||||
|
||||
areas.photo->setDroppedCallback([=](const QMimeData *data) {
|
||||
_paint->handleMimeData(data);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
61
Telegram/SourceFiles/editor/photo_editor_content.h
Normal file
61
Telegram/SourceFiles/editor/photo_editor_content.h
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
#include "editor/photo_editor_common.h"
|
||||
#include "editor/photo_editor_inner_common.h"
|
||||
#include "ui/image/image.h"
|
||||
|
||||
namespace Editor {
|
||||
|
||||
class Crop;
|
||||
class Paint;
|
||||
struct Controllers;
|
||||
|
||||
class PhotoEditorContent final : public Ui::RpWidget {
|
||||
public:
|
||||
PhotoEditorContent(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
std::shared_ptr<Image> photo,
|
||||
PhotoModifications modifications,
|
||||
std::shared_ptr<Controllers> controllers,
|
||||
EditorData data);
|
||||
|
||||
void applyModifications(PhotoModifications modifications);
|
||||
void applyMode(const PhotoEditorMode &mode);
|
||||
void applyBrush(const Brush &brush);
|
||||
void save(PhotoModifications &modifications);
|
||||
|
||||
bool handleKeyPress(not_null<QKeyEvent*> e) const;
|
||||
|
||||
void setupDragArea();
|
||||
|
||||
[[nodiscard]] rpl::producer<QRect> innerRect() const {
|
||||
return _innerRect.value();
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
const QSize _photoSize;
|
||||
const base::unique_qptr<Paint> _paint;
|
||||
const base::unique_qptr<Crop> _crop;
|
||||
const std::shared_ptr<Image> _photo;
|
||||
|
||||
rpl::variable<QRect> _innerRect;
|
||||
rpl::variable<PhotoModifications> _modifications;
|
||||
rpl::event_stream<int> _keyPresses;
|
||||
|
||||
QRect _imageRect;
|
||||
QTransform _imageMatrix;
|
||||
PhotoEditorMode _mode;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
548
Telegram/SourceFiles/editor/photo_editor_controls.cpp
Normal file
548
Telegram/SourceFiles/editor/photo_editor_controls.cpp
Normal file
@@ -0,0 +1,548 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "editor/photo_editor_controls.h"
|
||||
|
||||
#include "editor/controllers/controllers.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/image/image_prepare.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/wrap/fade_wrap.h"
|
||||
#include "ui/painter.h"
|
||||
#include "styles/style_editor.h"
|
||||
|
||||
namespace Editor {
|
||||
|
||||
class EdgeButton final : public Ui::RippleButton {
|
||||
public:
|
||||
EdgeButton(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const QString &text,
|
||||
int height,
|
||||
bool left,
|
||||
const style::color &bg,
|
||||
const style::color &fg,
|
||||
const style::RippleAnimation &st);
|
||||
|
||||
protected:
|
||||
QImage prepareRippleMask() const override;
|
||||
QPoint prepareRippleStartPosition() const override;
|
||||
|
||||
private:
|
||||
void init();
|
||||
|
||||
const style::color &_fg;
|
||||
Ui::Text::String _text;
|
||||
const int _width;
|
||||
const QRect _rippleRect;
|
||||
const QColor _bg;
|
||||
const bool _left;
|
||||
|
||||
QImage rounded(std::optional<QColor> color) const;
|
||||
|
||||
};
|
||||
|
||||
EdgeButton::EdgeButton(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const QString &text,
|
||||
int height,
|
||||
bool left,
|
||||
const style::color &bg,
|
||||
const style::color &fg,
|
||||
const style::RippleAnimation &st)
|
||||
: Ui::RippleButton(parent, st)
|
||||
, _fg(fg)
|
||||
, _text(st::photoEditorButtonStyle, text)
|
||||
, _width(_text.maxWidth()
|
||||
+ st::photoEditorTextButtonPadding.left()
|
||||
+ st::photoEditorTextButtonPadding.right())
|
||||
, _rippleRect(QRect(0, 0, _width, height))
|
||||
, _bg(bg->c)
|
||||
, _left(left) {
|
||||
resize(_width, height);
|
||||
init();
|
||||
}
|
||||
|
||||
void EdgeButton::init() {
|
||||
// const auto bg = rounded(_bg);
|
||||
|
||||
paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
Painter p(this);
|
||||
|
||||
// p.drawImage(QPoint(), bg);
|
||||
|
||||
paintRipple(p, _rippleRect.x(), _rippleRect.y());
|
||||
|
||||
p.setPen(_fg);
|
||||
const auto textTop = st::photoEditorButtonTextTop;
|
||||
_text.draw(p, 0, textTop, width(), style::al_center);
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
QImage EdgeButton::rounded(std::optional<QColor> color) const {
|
||||
auto result = QImage(
|
||||
_rippleRect.size() * style::DevicePixelRatio(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
result.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
result.fill(color.value_or(Qt::white));
|
||||
|
||||
const auto parts = RectPart::None
|
||||
| (_left ? RectPart::TopLeft : RectPart::TopRight)
|
||||
| (_left ? RectPart::BottomLeft : RectPart::BottomRight);
|
||||
return Images::Round(std::move(result), ImageRoundRadius::Large, parts);
|
||||
}
|
||||
|
||||
QImage EdgeButton::prepareRippleMask() const {
|
||||
return rounded(std::nullopt);
|
||||
}
|
||||
|
||||
QPoint EdgeButton::prepareRippleStartPosition() const {
|
||||
return mapFromGlobal(QCursor::pos()) - _rippleRect.topLeft();
|
||||
}
|
||||
|
||||
class ButtonBar final : public Ui::RpWidget {
|
||||
public:
|
||||
ButtonBar(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const style::color &bg);
|
||||
|
||||
private:
|
||||
QImage _roundedBg;
|
||||
|
||||
};
|
||||
|
||||
ButtonBar::ButtonBar(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const style::color &bg)
|
||||
: RpWidget(parent) {
|
||||
sizeValue(
|
||||
) | rpl::on_next([=](const QSize &size) {
|
||||
const auto children = RpWidget::children();
|
||||
const auto widgets = ranges::views::all(
|
||||
children
|
||||
) | ranges::views::filter([](not_null<const QObject*> object) {
|
||||
return object->isWidgetType();
|
||||
}) | ranges::views::transform([](not_null<QObject*> object) {
|
||||
return static_cast<QWidget*>(object.get());
|
||||
}) | ranges::to_vector;
|
||||
if (widgets.size() < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto layout = [&](bool symmetrical) {
|
||||
auto widths = widgets | ranges::views::transform(
|
||||
&QWidget::width
|
||||
) | ranges::to_vector;
|
||||
const auto count = int(widths.size());
|
||||
const auto middle = count / 2;
|
||||
if (symmetrical) {
|
||||
for (auto i = 0; i != middle; ++i) {
|
||||
const auto j = count - i - 1;
|
||||
widths[i] = widths[j] = std::max(widths[i], widths[j]);
|
||||
}
|
||||
}
|
||||
const auto residualWidth = size.width()
|
||||
- ranges::accumulate(widths, 0);
|
||||
if (symmetrical && residualWidth < 0) {
|
||||
return false;
|
||||
}
|
||||
const auto step = residualWidth / float(count - 1);
|
||||
|
||||
auto left = 0.;
|
||||
auto &&ints = ranges::views::ints(0, ranges::unreachable);
|
||||
auto &&list = ranges::views::zip(widgets, widths, ints);
|
||||
for (const auto &[widget, width, index] : list) {
|
||||
widget->move(int((index >= middle)
|
||||
? (left + width - widget->width())
|
||||
: left), 0);
|
||||
left += width + step;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
if (!layout(true)) {
|
||||
layout(false);
|
||||
}
|
||||
|
||||
auto result = QImage(
|
||||
size * style::DevicePixelRatio(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
result.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
result.fill(bg->c);
|
||||
|
||||
_roundedBg = Images::Round(
|
||||
std::move(result),
|
||||
ImageRoundRadius::Large);
|
||||
}, lifetime());
|
||||
|
||||
paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
auto p = QPainter(this);
|
||||
p.drawImage(QPoint(), _roundedBg);
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
PhotoEditorControls::PhotoEditorControls(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
std::shared_ptr<Controllers> controllers,
|
||||
const PhotoModifications modifications,
|
||||
const EditorData &data)
|
||||
: RpWidget(parent)
|
||||
, _bg(st::roundedBg)
|
||||
, _buttonHeight(st::photoEditorButtonBarHeight)
|
||||
, _transformButtons(base::make_unique_q<ButtonBar>(this, _bg))
|
||||
, _paintTopButtons(base::make_unique_q<ButtonBar>(this, _bg))
|
||||
, _paintBottomButtons(base::make_unique_q<ButtonBar>(this, _bg))
|
||||
, _about(data.about.empty()
|
||||
? nullptr
|
||||
: base::make_unique_q<Ui::FadeWrap<Ui::FlatLabel>>(
|
||||
this,
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
this,
|
||||
rpl::single(data.about),
|
||||
st::photoEditorAbout)))
|
||||
, _transformCancel(base::make_unique_q<EdgeButton>(
|
||||
_transformButtons,
|
||||
tr::lng_cancel(tr::now),
|
||||
_buttonHeight,
|
||||
true,
|
||||
_bg,
|
||||
st::mediaviewCaptionFg,
|
||||
st::photoEditorRotateButton.ripple))
|
||||
, _flipButton(base::make_unique_q<Ui::IconButton>(
|
||||
_transformButtons,
|
||||
st::photoEditorFlipButton))
|
||||
, _rotateButton(base::make_unique_q<Ui::IconButton>(
|
||||
_transformButtons,
|
||||
st::photoEditorRotateButton))
|
||||
, _paintModeButton(base::make_unique_q<Ui::IconButton>(
|
||||
_transformButtons,
|
||||
st::photoEditorPaintModeButton))
|
||||
, _transformDone(base::make_unique_q<EdgeButton>(
|
||||
_transformButtons,
|
||||
(data.confirm.isEmpty() ? tr::lng_box_done(tr::now) : data.confirm),
|
||||
_buttonHeight,
|
||||
false,
|
||||
_bg,
|
||||
st::mediaviewTextLinkFg,
|
||||
st::photoEditorRotateButton.ripple))
|
||||
, _paintCancel(base::make_unique_q<EdgeButton>(
|
||||
_paintBottomButtons,
|
||||
tr::lng_cancel(tr::now),
|
||||
_buttonHeight,
|
||||
true,
|
||||
_bg,
|
||||
st::mediaviewCaptionFg,
|
||||
st::photoEditorRotateButton.ripple))
|
||||
, _undoButton(base::make_unique_q<Ui::IconButton>(
|
||||
_paintTopButtons,
|
||||
st::photoEditorUndoButton))
|
||||
, _redoButton(base::make_unique_q<Ui::IconButton>(
|
||||
_paintTopButtons,
|
||||
st::photoEditorRedoButton))
|
||||
, _paintModeButtonActive(base::make_unique_q<Ui::IconButton>(
|
||||
_paintBottomButtons,
|
||||
st::photoEditorPaintModeButton))
|
||||
, _stickersButton(controllers->stickersPanelController
|
||||
? base::make_unique_q<Ui::IconButton>(
|
||||
_paintBottomButtons,
|
||||
st::photoEditorStickersButton)
|
||||
: nullptr)
|
||||
, _paintDone(base::make_unique_q<EdgeButton>(
|
||||
_paintBottomButtons,
|
||||
tr::lng_box_done(tr::now),
|
||||
_buttonHeight,
|
||||
false,
|
||||
_bg,
|
||||
st::mediaviewTextLinkFg,
|
||||
st::photoEditorRotateButton.ripple)) {
|
||||
|
||||
{
|
||||
const auto icon = &st::photoEditorPaintIconActive;
|
||||
_paintModeButtonActive->setIconOverride(icon, icon);
|
||||
}
|
||||
_paintModeButtonActive->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
|
||||
sizeValue(
|
||||
) | rpl::on_next([=](const QSize &size) {
|
||||
if (size.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &padding = st::photoEditorButtonBarPadding;
|
||||
const auto w = std::min(st::photoEditorButtonBarWidth, size.width())
|
||||
- padding.left()
|
||||
- padding.right();
|
||||
_transformButtons->resize(w, _buttonHeight);
|
||||
_paintBottomButtons->resize(w, _buttonHeight);
|
||||
_paintTopButtons->resize(w, _buttonHeight);
|
||||
|
||||
const auto buttonsTop = bottomButtonsTop();
|
||||
|
||||
const auto ¤t = _transformButtons->isHidden()
|
||||
? _paintBottomButtons
|
||||
: _transformButtons;
|
||||
|
||||
current->moveToLeft(
|
||||
(size.width() - current->width()) / 2,
|
||||
buttonsTop);
|
||||
|
||||
if (_about) {
|
||||
const auto &margin = st::photoEditorAboutMargin;
|
||||
const auto skip = st::photoEditorCropPointSize;
|
||||
_about->resizeToWidth(
|
||||
size.width() - margin.left() - margin.right());
|
||||
_about->moveToLeft(
|
||||
(size.width() - _about->width()) / 2,
|
||||
margin.top() - skip);
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
_mode.changes(
|
||||
) | rpl::on_next([=](const PhotoEditorMode &mode) {
|
||||
if (mode.mode == PhotoEditorMode::Mode::Out) {
|
||||
return;
|
||||
}
|
||||
const auto animated = (_paintBottomButtons->isVisible()
|
||||
== _transformButtons->isVisible())
|
||||
? anim::type::instant
|
||||
: anim::type::normal;
|
||||
showAnimated(mode.mode, animated);
|
||||
}, lifetime());
|
||||
|
||||
_paintBottomButtons->positionValue(
|
||||
) | rpl::on_next([=](const QPoint &containerPos) {
|
||||
_paintTopButtons->moveToLeft(
|
||||
containerPos.x(),
|
||||
containerPos.y()
|
||||
- st::photoEditorControlsCenterSkip
|
||||
- _paintTopButtons->height());
|
||||
}, _paintBottomButtons->lifetime());
|
||||
|
||||
_paintBottomButtons->shownValue(
|
||||
) | rpl::on_next([=](bool shown) {
|
||||
_paintTopButtons->setVisible(shown);
|
||||
}, _paintBottomButtons->lifetime());
|
||||
|
||||
controllers->undoController->setPerformRequestChanges(rpl::merge(
|
||||
_undoButton->clicks() | rpl::map_to(Undo::Undo),
|
||||
_redoButton->clicks() | rpl::map_to(Undo::Redo),
|
||||
_keyPresses.events(
|
||||
) | rpl::filter([=](not_null<QKeyEvent*> e) {
|
||||
using Mode = PhotoEditorMode::Mode;
|
||||
return (e->matches(QKeySequence::Undo)
|
||||
&& !_undoButton->isHidden()
|
||||
&& !_undoButton->testAttribute(
|
||||
Qt::WA_TransparentForMouseEvents)
|
||||
&& (_mode.current().mode == Mode::Paint))
|
||||
|| (e->matches(QKeySequence::Redo)
|
||||
&& !_redoButton->isHidden()
|
||||
&& !_redoButton->testAttribute(
|
||||
Qt::WA_TransparentForMouseEvents)
|
||||
&& (_mode.current().mode == Mode::Paint));
|
||||
}) | rpl::map([=](not_null<QKeyEvent*> e) {
|
||||
return e->matches(QKeySequence::Undo) ? Undo::Undo : Undo::Redo;
|
||||
})));
|
||||
|
||||
controllers->undoController->canPerformChanges(
|
||||
) | rpl::on_next([=](const UndoController::EnableRequest &r) {
|
||||
const auto isUndo = (r.command == Undo::Undo);
|
||||
const auto &button = isUndo ? _undoButton : _redoButton;
|
||||
button->setAttribute(Qt::WA_TransparentForMouseEvents, !r.enable);
|
||||
if (!r.enable) {
|
||||
button->clearState();
|
||||
}
|
||||
|
||||
button->setIconOverride(r.enable
|
||||
? nullptr
|
||||
: isUndo
|
||||
? &st::photoEditorUndoButtonInactive
|
||||
: &st::photoEditorRedoButtonInactive);
|
||||
}, lifetime());
|
||||
|
||||
if (_stickersButton) {
|
||||
using ShowRequest = StickersPanelController::ShowRequest;
|
||||
|
||||
controllers->stickersPanelController->setShowRequestChanges(
|
||||
rpl::merge(
|
||||
_mode.value(
|
||||
) | rpl::map_to(ShowRequest::HideFast),
|
||||
_stickersButton->clicks(
|
||||
) | rpl::map_to(ShowRequest::ToggleAnimated)
|
||||
));
|
||||
|
||||
controllers->stickersPanelController->setMoveRequestChanges(
|
||||
_paintBottomButtons->positionValue(
|
||||
) | rpl::map([=](const QPoint &containerPos) {
|
||||
return QPoint(
|
||||
(x() + width()) / 2,
|
||||
y() + containerPos.y() + _stickersButton->y());
|
||||
}));
|
||||
|
||||
controllers->stickersPanelController->panelShown(
|
||||
) | rpl::on_next([=](bool shown) {
|
||||
const auto icon = shown
|
||||
? &st::photoEditorStickersIconActive
|
||||
: nullptr;
|
||||
_stickersButton->setIconOverride(icon, icon);
|
||||
}, _stickersButton->lifetime());
|
||||
}
|
||||
|
||||
rpl::single(rpl::empty) | rpl::skip(
|
||||
modifications.flipped ? 0 : 1
|
||||
) | rpl::then(
|
||||
_flipButton->clicks() | rpl::to_empty
|
||||
) | rpl::on_next([=] {
|
||||
_flipped = !_flipped;
|
||||
const auto icon = _flipped ? &st::photoEditorFlipIconActive : nullptr;
|
||||
_flipButton->setIconOverride(icon, icon);
|
||||
}, _flipButton->lifetime());
|
||||
|
||||
}
|
||||
|
||||
rpl::producer<int> PhotoEditorControls::rotateRequests() const {
|
||||
return _rotateButton->clicks() | rpl::map_to(90);
|
||||
}
|
||||
|
||||
rpl::producer<> PhotoEditorControls::flipRequests() const {
|
||||
return _flipButton->clicks() | rpl::to_empty;
|
||||
}
|
||||
|
||||
rpl::producer<> PhotoEditorControls::paintModeRequests() const {
|
||||
return _paintModeButton->clicks() | rpl::to_empty;
|
||||
}
|
||||
|
||||
rpl::producer<> PhotoEditorControls::doneRequests() const {
|
||||
return rpl::merge(
|
||||
_transformDone->clicks() | rpl::to_empty,
|
||||
_paintDone->clicks() | rpl::to_empty,
|
||||
_keyPresses.events(
|
||||
) | rpl::filter([=](not_null<QKeyEvent*> e) {
|
||||
const auto key = e->key();
|
||||
return ((key == Qt::Key_Enter) || (key == Qt::Key_Return))
|
||||
&& !_toggledBarAnimation.animating();
|
||||
}) | rpl::to_empty);
|
||||
}
|
||||
|
||||
rpl::producer<> PhotoEditorControls::cancelRequests() const {
|
||||
return rpl::merge(
|
||||
_transformCancel->clicks() | rpl::to_empty,
|
||||
_paintCancel->clicks() | rpl::to_empty,
|
||||
_keyPresses.events(
|
||||
) | rpl::filter([=](not_null<QKeyEvent*> e) {
|
||||
const auto key = e->key();
|
||||
return (key == Qt::Key_Escape)
|
||||
&& !_toggledBarAnimation.animating();
|
||||
}) | rpl::to_empty);
|
||||
}
|
||||
|
||||
int PhotoEditorControls::bottomButtonsTop() const {
|
||||
return height()
|
||||
- st::photoEditorControlsBottomSkip
|
||||
- _transformButtons->height();
|
||||
}
|
||||
|
||||
void PhotoEditorControls::showAnimated(
|
||||
PhotoEditorMode::Mode mode,
|
||||
anim::type animated) {
|
||||
using Mode = PhotoEditorMode::Mode;
|
||||
|
||||
const auto duration = st::photoEditorBarAnimationDuration;
|
||||
|
||||
const auto isTransform = (mode == Mode::Transform);
|
||||
if (_about) {
|
||||
_about->toggle(isTransform, animated);
|
||||
}
|
||||
|
||||
const auto buttonsLeft = (width() - _transformButtons->width()) / 2;
|
||||
const auto buttonsTop = bottomButtonsTop();
|
||||
|
||||
const auto visibleBar = _transformButtons->isVisible()
|
||||
? _transformButtons.get()
|
||||
: _paintBottomButtons.get();
|
||||
|
||||
const auto shouldVisibleBar = isTransform
|
||||
? _transformButtons.get()
|
||||
: _paintBottomButtons.get(); // Mode::Paint
|
||||
|
||||
const auto computeTop = [=](float64 progress) {
|
||||
return anim::interpolate(buttonsTop, height() * 2, progress);
|
||||
};
|
||||
|
||||
const auto showShouldVisibleBar = [=] {
|
||||
_toggledBarAnimation.stop();
|
||||
auto callback = [=](float64 value) {
|
||||
shouldVisibleBar->moveToLeft(buttonsLeft, computeTop(value));
|
||||
};
|
||||
if (animated == anim::type::instant) {
|
||||
callback(1.);
|
||||
} else {
|
||||
_toggledBarAnimation.start(
|
||||
std::move(callback),
|
||||
1.,
|
||||
0.,
|
||||
duration);
|
||||
}
|
||||
};
|
||||
|
||||
auto animationCallback = [=](float64 value) {
|
||||
if (shouldVisibleBar == visibleBar) {
|
||||
showShouldVisibleBar();
|
||||
return;
|
||||
}
|
||||
visibleBar->moveToLeft(buttonsLeft, computeTop(value));
|
||||
|
||||
if (value == 1.) {
|
||||
shouldVisibleBar->show();
|
||||
shouldVisibleBar->moveToLeft(buttonsLeft, computeTop(1.));
|
||||
visibleBar->hide();
|
||||
|
||||
showShouldVisibleBar();
|
||||
}
|
||||
};
|
||||
|
||||
if (animated == anim::type::instant) {
|
||||
animationCallback(1.);
|
||||
} else {
|
||||
_toggledBarAnimation.start(
|
||||
std::move(animationCallback),
|
||||
0.,
|
||||
1.,
|
||||
duration);
|
||||
}
|
||||
}
|
||||
|
||||
void PhotoEditorControls::applyMode(const PhotoEditorMode &mode) {
|
||||
_mode = mode;
|
||||
}
|
||||
|
||||
rpl::producer<QPoint> PhotoEditorControls::colorLinePositionValue() const {
|
||||
return rpl::merge(
|
||||
geometryValue() | rpl::to_empty,
|
||||
_paintTopButtons->geometryValue() | rpl::to_empty
|
||||
) | rpl::map([=] {
|
||||
const auto r = _paintTopButtons->geometry();
|
||||
return mapToParent(r.topLeft())
|
||||
+ QPoint(r.width() / 2, r.height() / 2);
|
||||
});
|
||||
}
|
||||
|
||||
rpl::producer<bool> PhotoEditorControls::colorLineShownValue() const {
|
||||
return _paintTopButtons->shownValue();
|
||||
}
|
||||
|
||||
bool PhotoEditorControls::handleKeyPress(not_null<QKeyEvent*> e) const {
|
||||
_keyPresses.fire(std::move(e));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PhotoEditorControls::animating() const {
|
||||
return _toggledBarAnimation.animating();
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
89
Telegram/SourceFiles/editor/photo_editor_controls.h
Normal file
89
Telegram/SourceFiles/editor/photo_editor_controls.h
Normal file
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
#include "ui/effects/animations.h"
|
||||
#include "editor/photo_editor_common.h"
|
||||
#include "editor/photo_editor_inner_common.h"
|
||||
|
||||
namespace Ui {
|
||||
class IconButton;
|
||||
class FlatLabel;
|
||||
template <typename Widget>
|
||||
class FadeWrap;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Editor {
|
||||
|
||||
class EdgeButton;
|
||||
class ButtonBar;
|
||||
struct Controllers;
|
||||
struct EditorData;
|
||||
|
||||
class PhotoEditorControls final : public Ui::RpWidget {
|
||||
public:
|
||||
PhotoEditorControls(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
std::shared_ptr<Controllers> controllers,
|
||||
const PhotoModifications modifications,
|
||||
const EditorData &data);
|
||||
|
||||
[[nodiscard]] rpl::producer<int> rotateRequests() const;
|
||||
[[nodiscard]] rpl::producer<> flipRequests() const;
|
||||
[[nodiscard]] rpl::producer<> paintModeRequests() const;
|
||||
[[nodiscard]] rpl::producer<> doneRequests() const;
|
||||
[[nodiscard]] rpl::producer<> cancelRequests() const;
|
||||
[[nodiscard]] rpl::producer<QPoint> colorLinePositionValue() const;
|
||||
[[nodiscard]] rpl::producer<bool> colorLineShownValue() const;
|
||||
|
||||
[[nodiscard]] bool animating() const;
|
||||
|
||||
bool handleKeyPress(not_null<QKeyEvent*> e) const;
|
||||
|
||||
void applyMode(const PhotoEditorMode &mode);
|
||||
|
||||
private:
|
||||
void showAnimated(
|
||||
PhotoEditorMode::Mode mode,
|
||||
anim::type animated = anim::type::normal);
|
||||
|
||||
int bottomButtonsTop() const;
|
||||
|
||||
const style::color &_bg;
|
||||
const int _buttonHeight;
|
||||
const base::unique_qptr<ButtonBar> _transformButtons;
|
||||
const base::unique_qptr<ButtonBar> _paintTopButtons;
|
||||
const base::unique_qptr<ButtonBar> _paintBottomButtons;
|
||||
|
||||
const base::unique_qptr<Ui::FadeWrap<Ui::FlatLabel>> _about;
|
||||
|
||||
const base::unique_qptr<EdgeButton> _transformCancel;
|
||||
const base::unique_qptr<Ui::IconButton> _flipButton;
|
||||
const base::unique_qptr<Ui::IconButton> _rotateButton;
|
||||
const base::unique_qptr<Ui::IconButton> _paintModeButton;
|
||||
const base::unique_qptr<EdgeButton> _transformDone;
|
||||
|
||||
const base::unique_qptr<EdgeButton> _paintCancel;
|
||||
const base::unique_qptr<Ui::IconButton> _undoButton;
|
||||
const base::unique_qptr<Ui::IconButton> _redoButton;
|
||||
const base::unique_qptr<Ui::IconButton> _paintModeButtonActive;
|
||||
const base::unique_qptr<Ui::IconButton> _stickersButton;
|
||||
const base::unique_qptr<EdgeButton> _paintDone;
|
||||
|
||||
bool _flipped = false;
|
||||
|
||||
Ui::Animations::Simple _toggledBarAnimation;
|
||||
|
||||
rpl::variable<PhotoEditorMode> _mode;
|
||||
rpl::event_stream<not_null<QKeyEvent*>> _keyPresses;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
38
Telegram/SourceFiles/editor/photo_editor_inner_common.h
Normal file
38
Telegram/SourceFiles/editor/photo_editor_inner_common.h
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Editor {
|
||||
|
||||
class Scene;
|
||||
|
||||
struct PhotoEditorMode {
|
||||
enum class Mode {
|
||||
Transform,
|
||||
Paint,
|
||||
Out,
|
||||
} mode = Mode::Transform;
|
||||
|
||||
enum class Action {
|
||||
None,
|
||||
Save,
|
||||
Discard,
|
||||
} action = Action::None;
|
||||
};
|
||||
|
||||
struct Brush {
|
||||
float sizeRatio = 0.;
|
||||
QColor color;
|
||||
};
|
||||
|
||||
enum class SaveState {
|
||||
Save,
|
||||
Keep,
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
171
Telegram/SourceFiles/editor/photo_editor_layer_widget.cpp
Normal file
171
Telegram/SourceFiles/editor/photo_editor_layer_widget.cpp
Normal file
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "editor/photo_editor_layer_widget.h"
|
||||
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/boxes/confirm_box.h" // InformBox
|
||||
#include "editor/editor_layer_widget.h"
|
||||
#include "editor/photo_editor.h"
|
||||
#include "storage/localimageloader.h"
|
||||
#include "storage/storage_media_prepare.h"
|
||||
#include "ui/chat/attach/attach_prepare.h"
|
||||
#include "window/window_controller.h"
|
||||
#include "window/window_session_controller.h"
|
||||
|
||||
#include <QtGui/QGuiApplication>
|
||||
|
||||
namespace Editor {
|
||||
|
||||
void OpenWithPreparedFile(
|
||||
not_null<QWidget*> parent,
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
not_null<Ui::PreparedFile*> file,
|
||||
int previewWidth,
|
||||
Fn<void(bool ok)> &&doneCallback,
|
||||
QSize exactSize) {
|
||||
using ImageInfo = Ui::PreparedFileInformation::Image;
|
||||
const auto image = std::get_if<ImageInfo>(&file->information->media);
|
||||
if (!image) {
|
||||
doneCallback(false);
|
||||
return;
|
||||
}
|
||||
const auto photoType = (file->type == Ui::PreparedFile::Type::Photo);
|
||||
const auto modifiedFileType = (file->type == Ui::PreparedFile::Type::File)
|
||||
&& !image->modifications.empty();
|
||||
if (!photoType && !modifiedFileType) {
|
||||
doneCallback(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto sideLimit = PhotoSideLimit();
|
||||
const auto accepted = std::make_shared<bool>();
|
||||
auto callback = [=](const PhotoModifications &mods) {
|
||||
*accepted = true;
|
||||
image->modifications = mods;
|
||||
Storage::UpdateImageDetails(*file, previewWidth, sideLimit);
|
||||
{
|
||||
using namespace Ui;
|
||||
const auto size = file->preview.size();
|
||||
file->type = ValidateThumbDimensions(size.width(), size.height())
|
||||
? PreparedFile::Type::Photo
|
||||
: PreparedFile::Type::File;
|
||||
}
|
||||
doneCallback(true);
|
||||
};
|
||||
auto copy = image->data;
|
||||
const auto fileImage = std::make_shared<Image>(std::move(copy));
|
||||
const auto keepRatio = !exactSize.isEmpty();
|
||||
auto editor = base::make_unique_q<PhotoEditor>(
|
||||
parent,
|
||||
show,
|
||||
show,
|
||||
fileImage,
|
||||
image->modifications,
|
||||
EditorData{ .exactSize = exactSize, .keepAspectRatio = keepRatio });
|
||||
const auto raw = editor.get();
|
||||
auto layer = std::make_unique<LayerWidget>(parent, std::move(editor));
|
||||
InitEditorLayer(layer.get(), raw, std::move(callback));
|
||||
QObject::connect(layer.get(), &QObject::destroyed, [=] {
|
||||
if (!*accepted) {
|
||||
doneCallback(false);
|
||||
}
|
||||
});
|
||||
show->showLayer(std::move(layer), Ui::LayerOption::KeepOther);
|
||||
}
|
||||
|
||||
void PrepareProfilePhoto(
|
||||
not_null<QWidget*> parent,
|
||||
not_null<Window::Controller*> controller,
|
||||
EditorData data,
|
||||
Fn<void(QImage &&image)> &&doneCallback,
|
||||
QImage &&image) {
|
||||
const auto resizeToMinSize = [=](
|
||||
QImage &&image,
|
||||
Qt::AspectRatioMode mode) {
|
||||
const auto &minSize = kProfilePhotoSize;
|
||||
if ((image.width() < minSize) || (image.height() < minSize)) {
|
||||
return image.scaled(
|
||||
minSize,
|
||||
minSize,
|
||||
mode,
|
||||
Qt::SmoothTransformation);
|
||||
}
|
||||
return std::move(image);
|
||||
};
|
||||
|
||||
if (image.isNull()
|
||||
|| (image.width() > (10 * image.height()))
|
||||
|| (image.height() > (10 * image.width()))) {
|
||||
controller->show(Ui::MakeInformBox(tr::lng_bad_photo()));
|
||||
return;
|
||||
}
|
||||
image = resizeToMinSize(
|
||||
std::move(image),
|
||||
Qt::KeepAspectRatioByExpanding);
|
||||
const auto fileImage = std::make_shared<Image>(std::move(image));
|
||||
|
||||
auto applyModifications = [=, done = std::move(doneCallback)](
|
||||
const PhotoModifications &mods) {
|
||||
done(resizeToMinSize(
|
||||
ImageModified(fileImage->original(), mods),
|
||||
Qt::KeepAspectRatio));
|
||||
};
|
||||
|
||||
auto crop = [&] {
|
||||
const auto &i = fileImage;
|
||||
const auto minSide = std::min(i->width(), i->height());
|
||||
return QRect(
|
||||
(i->width() - minSide) / 2,
|
||||
(i->height() - minSide) / 2,
|
||||
minSide,
|
||||
minSide);
|
||||
}();
|
||||
|
||||
auto editor = base::make_unique_q<PhotoEditor>(
|
||||
parent,
|
||||
controller,
|
||||
fileImage,
|
||||
PhotoModifications{ .crop = std::move(crop) },
|
||||
data);
|
||||
const auto raw = editor.get();
|
||||
auto layer = std::make_unique<LayerWidget>(parent, std::move(editor));
|
||||
InitEditorLayer(layer.get(), raw, std::move(applyModifications));
|
||||
controller->showLayer(std::move(layer), Ui::LayerOption::KeepOther);
|
||||
}
|
||||
|
||||
void PrepareProfilePhotoFromFile(
|
||||
not_null<QWidget*> parent,
|
||||
not_null<Window::Controller*> controller,
|
||||
EditorData data,
|
||||
Fn<void(QImage &&image)> &&doneCallback) {
|
||||
const auto callback = [=, done = std::move(doneCallback)](
|
||||
const FileDialog::OpenResult &result) mutable {
|
||||
if (result.paths.isEmpty() && result.remoteContent.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto image = Images::Read({
|
||||
.path = result.paths.isEmpty() ? QString() : result.paths.front(),
|
||||
.content = result.remoteContent,
|
||||
.forceOpaque = true,
|
||||
}).image;
|
||||
PrepareProfilePhoto(
|
||||
parent,
|
||||
controller,
|
||||
data,
|
||||
std::move(done),
|
||||
std::move(image));
|
||||
};
|
||||
FileDialog::GetOpenPath(
|
||||
parent.get(),
|
||||
tr::lng_choose_image(tr::now),
|
||||
FileDialog::ImagesOrAllFilter(),
|
||||
crl::guard(parent, callback));
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
53
Telegram/SourceFiles/editor/photo_editor_layer_widget.h
Normal file
53
Telegram/SourceFiles/editor/photo_editor_layer_widget.h
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
enum class ImageRoundRadius;
|
||||
|
||||
namespace ChatHelpers {
|
||||
class Show;
|
||||
} // namespace ChatHelpers
|
||||
|
||||
namespace Ui {
|
||||
class RpWidget;
|
||||
struct PreparedFile;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Window {
|
||||
class Controller;
|
||||
class SessionController;
|
||||
} // namespace Window
|
||||
|
||||
namespace Editor {
|
||||
|
||||
constexpr auto kProfilePhotoSize = int(640);
|
||||
|
||||
struct EditorData;
|
||||
|
||||
void OpenWithPreparedFile(
|
||||
not_null<QWidget*> parent,
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
not_null<Ui::PreparedFile*> file,
|
||||
int previewWidth,
|
||||
Fn<void(bool ok)> &&doneCallback,
|
||||
QSize exactSize = {});
|
||||
|
||||
void PrepareProfilePhoto(
|
||||
not_null<QWidget*> parent,
|
||||
not_null<Window::Controller*> controller,
|
||||
EditorData data,
|
||||
Fn<void(QImage &&image)> &&doneCallback,
|
||||
QImage &&image);
|
||||
|
||||
void PrepareProfilePhotoFromFile(
|
||||
not_null<QWidget*> parent,
|
||||
not_null<Window::Controller*> controller,
|
||||
EditorData data,
|
||||
Fn<void(QImage &&image)> &&doneCallback);
|
||||
|
||||
} // namespace Editor
|
||||
220
Telegram/SourceFiles/editor/scene/scene.cpp
Normal file
220
Telegram/SourceFiles/editor/scene/scene.cpp
Normal file
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "editor/scene/scene.h"
|
||||
|
||||
#include "editor/scene/scene_item_canvas.h"
|
||||
#include "editor/scene/scene_item_line.h"
|
||||
#include "editor/scene/scene_item_sticker.h"
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
|
||||
namespace Editor {
|
||||
namespace {
|
||||
|
||||
using ItemPtr = std::shared_ptr<NumberedItem>;
|
||||
|
||||
bool SkipMouseEvent(not_null<QGraphicsSceneMouseEvent*> event) {
|
||||
return event->isAccepted() || (event->button() == Qt::RightButton);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Scene::Scene(const QRectF &rect)
|
||||
: QGraphicsScene(rect)
|
||||
, _canvas(std::make_shared<ItemCanvas>())
|
||||
, _lastZ(std::make_shared<float64>(9000.)) {
|
||||
QGraphicsScene::addItem(_canvas.get());
|
||||
_canvas->clearPixmap();
|
||||
|
||||
_canvas->grabContentRequests(
|
||||
) | rpl::on_next([=](ItemCanvas::Content &&content) {
|
||||
const auto item = std::make_shared<ItemLine>(
|
||||
std::move(content.pixmap));
|
||||
item->setPos(content.position);
|
||||
addItem(item);
|
||||
_canvas->setZValue(++_lastLineZ);
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void Scene::cancelDrawing() {
|
||||
_canvas->cancelDrawing();
|
||||
}
|
||||
|
||||
void Scene::addItem(ItemPtr item) {
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
item->setNumber(_itemNumber++);
|
||||
QGraphicsScene::addItem(item.get());
|
||||
_items.push_back(std::move(item));
|
||||
_addsItem.fire({});
|
||||
}
|
||||
|
||||
void Scene::removeItem(not_null<QGraphicsItem*> item) {
|
||||
const auto it = ranges::find_if(_items, [&](const ItemPtr &i) {
|
||||
return i.get() == item;
|
||||
});
|
||||
if (it == end(_items)) {
|
||||
return;
|
||||
}
|
||||
removeItem(*it);
|
||||
}
|
||||
|
||||
void Scene::removeItem(const ItemPtr &item) {
|
||||
item->setStatus(NumberedItem::Status::Removed);
|
||||
_removesItem.fire({});
|
||||
}
|
||||
|
||||
void Scene::mousePressEvent(QGraphicsSceneMouseEvent *event) {
|
||||
QGraphicsScene::mousePressEvent(event);
|
||||
if (SkipMouseEvent(event)) {
|
||||
return;
|
||||
}
|
||||
_canvas->handleMousePressEvent(event);
|
||||
}
|
||||
|
||||
void Scene::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) {
|
||||
QGraphicsScene::mouseReleaseEvent(event);
|
||||
if (SkipMouseEvent(event)) {
|
||||
return;
|
||||
}
|
||||
_canvas->handleMouseReleaseEvent(event);
|
||||
}
|
||||
|
||||
void Scene::mouseMoveEvent(QGraphicsSceneMouseEvent *event) {
|
||||
QGraphicsScene::mouseMoveEvent(event);
|
||||
if (SkipMouseEvent(event)) {
|
||||
return;
|
||||
}
|
||||
_canvas->handleMouseMoveEvent(event);
|
||||
}
|
||||
|
||||
void Scene::applyBrush(const QColor &color, float size) {
|
||||
_canvas->applyBrush(color, size);
|
||||
}
|
||||
|
||||
rpl::producer<> Scene::addsItem() const {
|
||||
return _addsItem.events();
|
||||
}
|
||||
|
||||
rpl::producer<> Scene::removesItem() const {
|
||||
return _removesItem.events();
|
||||
}
|
||||
|
||||
std::vector<ItemPtr> Scene::items(
|
||||
Qt::SortOrder order) const {
|
||||
auto copyItems = _items;
|
||||
|
||||
ranges::sort(copyItems, [&](ItemPtr a, ItemPtr b) {
|
||||
const auto numA = a->number();
|
||||
const auto numB = b->number();
|
||||
return (order == Qt::AscendingOrder) ? (numA < numB) : (numA > numB);
|
||||
});
|
||||
|
||||
return copyItems;
|
||||
}
|
||||
|
||||
std::shared_ptr<float64> Scene::lastZ() const {
|
||||
return _lastZ;
|
||||
}
|
||||
|
||||
void Scene::updateZoom(float64 zoom) {
|
||||
_canvas->updateZoom(zoom);
|
||||
for (const auto &item : items()) {
|
||||
if (item->type() >= ItemBase::Type) {
|
||||
static_cast<ItemBase*>(item.get())->updateZoom(zoom);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Scene::hasUndo() const {
|
||||
return ranges::any_of(_items, &NumberedItem::isNormalStatus);
|
||||
}
|
||||
|
||||
bool Scene::hasRedo() const {
|
||||
return ranges::any_of(_items, &NumberedItem::isUndidStatus);
|
||||
}
|
||||
|
||||
void Scene::performUndo() {
|
||||
const auto filtered = items(Qt::DescendingOrder);
|
||||
|
||||
const auto it = ranges::find_if(filtered, &NumberedItem::isNormalStatus);
|
||||
if (it != filtered.end()) {
|
||||
(*it)->setStatus(NumberedItem::Status::Undid);
|
||||
}
|
||||
}
|
||||
|
||||
void Scene::performRedo() {
|
||||
const auto filtered = items(Qt::AscendingOrder);
|
||||
|
||||
const auto it = ranges::find_if(filtered, &NumberedItem::isUndidStatus);
|
||||
if (it != filtered.end()) {
|
||||
(*it)->setStatus(NumberedItem::Status::Normal);
|
||||
}
|
||||
}
|
||||
|
||||
void Scene::removeIf(Fn<bool(const ItemPtr &)> proj) {
|
||||
auto copy = std::vector<ItemPtr>();
|
||||
for (const auto &item : _items) {
|
||||
const auto toRemove = proj(item);
|
||||
if (toRemove) {
|
||||
// Scene loses ownership of an item.
|
||||
// It seems for some reason this line causes a crash. =(
|
||||
// QGraphicsScene::removeItem(item.get());
|
||||
} else {
|
||||
copy.push_back(item);
|
||||
}
|
||||
}
|
||||
_items = std::move(copy);
|
||||
}
|
||||
|
||||
void Scene::clearRedoList() {
|
||||
for (const auto &item : _items) {
|
||||
if (item->isUndidStatus()) {
|
||||
item->setStatus(NumberedItem::Status::Removed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Scene::save(SaveState state) {
|
||||
removeIf([](const ItemPtr &item) {
|
||||
return item->isRemovedStatus()
|
||||
&& !item->hasState(SaveState::Keep)
|
||||
&& !item->hasState(SaveState::Save);
|
||||
});
|
||||
|
||||
for (const auto &item : _items) {
|
||||
item->save(state);
|
||||
}
|
||||
clearSelection();
|
||||
cancelDrawing();
|
||||
}
|
||||
|
||||
void Scene::restore(SaveState state) {
|
||||
removeIf([=](const ItemPtr &item) {
|
||||
return !item->hasState(state);
|
||||
});
|
||||
|
||||
for (const auto &item : _items) {
|
||||
item->restore(state);
|
||||
}
|
||||
clearSelection();
|
||||
cancelDrawing();
|
||||
}
|
||||
|
||||
Scene::~Scene() {
|
||||
// Prevent destroying by scene of all items.
|
||||
QGraphicsScene::removeItem(_canvas.get());
|
||||
for (const auto &item : items()) {
|
||||
// Scene loses ownership of an item.
|
||||
QGraphicsScene::removeItem(item.get());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
77
Telegram/SourceFiles/editor/scene/scene.h
Normal file
77
Telegram/SourceFiles/editor/scene/scene.h
Normal file
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <base/unique_qptr.h>
|
||||
#include <editor/photo_editor_inner_common.h>
|
||||
|
||||
#include <QGraphicsScene>
|
||||
|
||||
class QGraphicsSceneMouseEvent;
|
||||
|
||||
namespace Ui {
|
||||
class RpWidget;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Editor {
|
||||
|
||||
class ItemCanvas;
|
||||
class NumberedItem;
|
||||
|
||||
class Scene final : public QGraphicsScene {
|
||||
public:
|
||||
using ItemPtr = std::shared_ptr<NumberedItem>;
|
||||
|
||||
Scene(const QRectF &rect);
|
||||
~Scene();
|
||||
void applyBrush(const QColor &color, float size);
|
||||
|
||||
[[nodiscard]] std::vector<ItemPtr> items(
|
||||
Qt::SortOrder order = Qt::DescendingOrder) const;
|
||||
void addItem(ItemPtr item);
|
||||
void removeItem(not_null<QGraphicsItem*> item);
|
||||
void removeItem(const ItemPtr &item);
|
||||
[[nodiscard]] rpl::producer<> addsItem() const;
|
||||
[[nodiscard]] rpl::producer<> removesItem() const;
|
||||
|
||||
[[nodiscard]] std::shared_ptr<float64> lastZ() const;
|
||||
|
||||
void updateZoom(float64 zoom);
|
||||
|
||||
void cancelDrawing();
|
||||
|
||||
[[nodiscard]] bool hasUndo() const;
|
||||
[[nodiscard]] bool hasRedo() const;
|
||||
|
||||
void performUndo();
|
||||
void performRedo();
|
||||
|
||||
void save(SaveState state);
|
||||
void restore(SaveState state);
|
||||
|
||||
void clearRedoList();
|
||||
protected:
|
||||
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
|
||||
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
|
||||
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
|
||||
private:
|
||||
void removeIf(Fn<bool(const ItemPtr &)> proj);
|
||||
const std::shared_ptr<ItemCanvas> _canvas;
|
||||
const std::shared_ptr<float64> _lastZ;
|
||||
|
||||
std::vector<ItemPtr> _items;
|
||||
|
||||
float64 _lastLineZ = 0.;
|
||||
int _itemNumber = 0;
|
||||
|
||||
rpl::event_stream<> _addsItem, _removesItem;
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
456
Telegram/SourceFiles/editor/scene/scene_item_base.cpp
Normal file
456
Telegram/SourceFiles/editor/scene/scene_item_base.cpp
Normal file
@@ -0,0 +1,456 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "editor/scene/scene_item_base.h"
|
||||
|
||||
#include "editor/scene/scene.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/painter.h"
|
||||
#include "styles/style_editor.h"
|
||||
#include "styles/style_menu_icons.h"
|
||||
|
||||
#include <QGraphicsScene>
|
||||
#include <QGraphicsSceneHoverEvent>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QStyleOptionGraphicsItem>
|
||||
#include <QtMath>
|
||||
|
||||
namespace Editor {
|
||||
namespace {
|
||||
|
||||
constexpr auto kSnapAngle = 45.;
|
||||
|
||||
const auto kDuplicateSequence = QKeySequence("ctrl+d");
|
||||
const auto kFlipSequence = QKeySequence("ctrl+s");
|
||||
const auto kDeleteSequence = QKeySequence("delete");
|
||||
|
||||
constexpr auto kMinSizeRatio = 0.05;
|
||||
constexpr auto kMaxSizeRatio = 1.00;
|
||||
|
||||
auto Normalized(float64 angle) {
|
||||
return angle
|
||||
+ ((std::abs(angle) < 360) ? 0 : (-360 * (angle < 0 ? -1 : 1)));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int NumberedItem::type() const {
|
||||
return NumberedItem::Type;
|
||||
}
|
||||
|
||||
int NumberedItem::number() const {
|
||||
return _number;
|
||||
}
|
||||
|
||||
void NumberedItem::setNumber(int number) {
|
||||
_number = number;
|
||||
}
|
||||
|
||||
NumberedItem::Status NumberedItem::status() const {
|
||||
return _status;
|
||||
}
|
||||
|
||||
bool NumberedItem::isNormalStatus() const {
|
||||
return _status == Status::Normal;
|
||||
}
|
||||
|
||||
bool NumberedItem::isUndidStatus() const {
|
||||
return _status == Status::Undid;
|
||||
}
|
||||
|
||||
bool NumberedItem::isRemovedStatus() const {
|
||||
return _status == Status::Removed;
|
||||
}
|
||||
|
||||
void NumberedItem::save(SaveState state) {
|
||||
}
|
||||
|
||||
void NumberedItem::restore(SaveState state) {
|
||||
}
|
||||
|
||||
bool NumberedItem::hasState(SaveState state) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
void NumberedItem::setStatus(Status status) {
|
||||
if (status != _status) {
|
||||
_status = status;
|
||||
setVisible(status == Status::Normal);
|
||||
}
|
||||
}
|
||||
|
||||
ItemBase::ItemBase(Data data)
|
||||
: _lastZ(data.zPtr)
|
||||
, _imageSize(data.imageSize)
|
||||
, _horizontalSize(data.size) {
|
||||
setFlags(QGraphicsItem::ItemIsMovable
|
||||
| QGraphicsItem::ItemIsSelectable
|
||||
| QGraphicsItem::ItemIsFocusable);
|
||||
setAcceptHoverEvents(true);
|
||||
applyData(data);
|
||||
}
|
||||
|
||||
QRectF ItemBase::boundingRect() const {
|
||||
return innerRect() + _scaledInnerMargins;
|
||||
}
|
||||
|
||||
QRectF ItemBase::contentRect() const {
|
||||
return innerRect() - _scaledInnerMargins;
|
||||
}
|
||||
|
||||
QRectF ItemBase::innerRect() const {
|
||||
const auto &hSize = _horizontalSize;
|
||||
const auto &vSize = _verticalSize;
|
||||
return QRectF(-hSize / 2, -vSize / 2, hSize, vSize);
|
||||
}
|
||||
|
||||
void ItemBase::paint(
|
||||
QPainter *p,
|
||||
const QStyleOptionGraphicsItem *option,
|
||||
QWidget *) {
|
||||
if (!(option->state & QStyle::State_Selected)) {
|
||||
return;
|
||||
}
|
||||
PainterHighQualityEnabler hq(*p);
|
||||
const auto hasFocus = (option->state & QStyle::State_HasFocus);
|
||||
p->setPen(hasFocus ? _pens.select : _pens.selectInactive);
|
||||
p->drawRect(innerRect());
|
||||
|
||||
p->setPen(hasFocus ? _pens.handle : _pens.handleInactive);
|
||||
p->setBrush(st::photoEditorItemBaseHandleFg);
|
||||
p->drawEllipse(rightHandleRect());
|
||||
p->drawEllipse(leftHandleRect());
|
||||
}
|
||||
|
||||
void ItemBase::mouseMoveEvent(QGraphicsSceneMouseEvent *event) {
|
||||
if (isHandling()) {
|
||||
const auto mousePos = event->pos();
|
||||
const auto shift = event->modifiers().testFlag(Qt::ShiftModifier);
|
||||
const auto isLeft = (_handle == HandleType::Left);
|
||||
if (!shift) {
|
||||
// Resize.
|
||||
const auto p = isLeft ? (mousePos * -1) : mousePos;
|
||||
const auto dx = int(2.0 * p.x());
|
||||
const auto dy = int(2.0 * p.y());
|
||||
prepareGeometryChange();
|
||||
_horizontalSize = std::clamp(
|
||||
(dx > dy ? dx : dy),
|
||||
_sizeLimits.min,
|
||||
_sizeLimits.max);
|
||||
updateVerticalSize();
|
||||
}
|
||||
|
||||
// Rotate.
|
||||
const auto origin = mapToScene(boundingRect().center());
|
||||
const auto pos = mapToScene(mousePos);
|
||||
|
||||
const auto diff = pos - origin;
|
||||
const auto angle = Normalized((isLeft ? 180 : 0)
|
||||
+ (std::atan2(diff.y(), diff.x()) * 180 / M_PI));
|
||||
setRotation(shift
|
||||
? (base::SafeRound(angle / kSnapAngle) * kSnapAngle)
|
||||
: angle);
|
||||
} else {
|
||||
QGraphicsItem::mouseMoveEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
void ItemBase::hoverMoveEvent(QGraphicsSceneHoverEvent *event) {
|
||||
setCursor(isHandling()
|
||||
? Qt::ClosedHandCursor
|
||||
: (handleType(event->pos()) != HandleType::None) && isSelected()
|
||||
? Qt::OpenHandCursor
|
||||
: Qt::ArrowCursor);
|
||||
QGraphicsItem::hoverMoveEvent(event);
|
||||
}
|
||||
|
||||
void ItemBase::mousePressEvent(QGraphicsSceneMouseEvent *event) {
|
||||
setZValue((*_lastZ)++);
|
||||
if (event->button() == Qt::LeftButton) {
|
||||
_handle = handleType(event->pos());
|
||||
}
|
||||
if (isHandling()) {
|
||||
setCursor(Qt::ClosedHandCursor);
|
||||
} else {
|
||||
QGraphicsItem::mousePressEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
void ItemBase::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) {
|
||||
if ((event->button() == Qt::LeftButton) && isHandling()) {
|
||||
_handle = HandleType::None;
|
||||
} else {
|
||||
QGraphicsItem::mouseReleaseEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
void ItemBase::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) {
|
||||
if (scene()) {
|
||||
scene()->clearSelection();
|
||||
setSelected(true);
|
||||
}
|
||||
|
||||
const auto add = [&](
|
||||
auto base,
|
||||
const QKeySequence &sequence,
|
||||
Fn<void()> callback,
|
||||
const style::icon *icon) {
|
||||
// TODO: refactor.
|
||||
const auto sequenceText = QChar('\t')
|
||||
+ sequence.toString(QKeySequence::NativeText);
|
||||
_menu->addAction(
|
||||
base(tr::now) + sequenceText,
|
||||
std::move(callback),
|
||||
icon);
|
||||
};
|
||||
|
||||
_menu = base::make_unique_q<Ui::PopupMenu>(
|
||||
nullptr,
|
||||
st::popupMenuWithIcons);
|
||||
add(
|
||||
tr::lng_photo_editor_menu_delete,
|
||||
kDeleteSequence,
|
||||
[=] { actionDelete(); },
|
||||
&st::menuIconDelete);
|
||||
add(
|
||||
tr::lng_photo_editor_menu_flip,
|
||||
kFlipSequence,
|
||||
[=] { actionFlip(); },
|
||||
&st::menuIconFlip);
|
||||
add(
|
||||
tr::lng_photo_editor_menu_duplicate,
|
||||
kDuplicateSequence,
|
||||
[=] { actionDuplicate(); },
|
||||
&st::menuIconCopy);
|
||||
|
||||
_menu->popup(event->screenPos());
|
||||
}
|
||||
|
||||
void ItemBase::performForSelectedItems(Action action) {
|
||||
if (const auto s = scene()) {
|
||||
for (const auto item : s->selectedItems()) {
|
||||
if (const auto base = static_cast<ItemBase*>(item)) {
|
||||
(base->*action)();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ItemBase::actionFlip() {
|
||||
setFlip(!flipped());
|
||||
}
|
||||
|
||||
void ItemBase::actionDelete() {
|
||||
if (const auto s = static_cast<Scene*>(scene())) {
|
||||
s->removeItem(this);
|
||||
}
|
||||
}
|
||||
|
||||
void ItemBase::actionDuplicate() {
|
||||
if (const auto s = static_cast<Scene*>(scene())) {
|
||||
auto data = generateData();
|
||||
data.x += int(_horizontalSize / 3);
|
||||
data.y += int(_verticalSize / 3);
|
||||
const auto newItem = duplicate(std::move(data));
|
||||
if (hasFocus()) {
|
||||
newItem->setFocus();
|
||||
}
|
||||
const auto selected = isSelected();
|
||||
newItem->setSelected(selected);
|
||||
setSelected(false);
|
||||
s->addItem(newItem);
|
||||
}
|
||||
}
|
||||
|
||||
void ItemBase::keyPressEvent(QKeyEvent *e) {
|
||||
if (e->key() == Qt::Key_Escape) {
|
||||
if (const auto s = scene()) {
|
||||
s->clearSelection();
|
||||
s->clearFocus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
handleActionKey(e);
|
||||
}
|
||||
|
||||
void ItemBase::handleActionKey(not_null<QKeyEvent*> e) {
|
||||
const auto matches = [&](const QKeySequence &sequence) {
|
||||
const auto searchKey = (e->modifiers() | e->key())
|
||||
& ~(Qt::KeypadModifier | Qt::GroupSwitchModifier);
|
||||
const auto events = QKeySequence(searchKey);
|
||||
return sequence.matches(events) == QKeySequence::ExactMatch;
|
||||
};
|
||||
if (matches(kDuplicateSequence)) {
|
||||
performForSelectedItems(&ItemBase::actionDuplicate);
|
||||
} else if (matches(kDeleteSequence)) {
|
||||
performForSelectedItems(&ItemBase::actionDelete);
|
||||
} else if (matches(kFlipSequence)) {
|
||||
performForSelectedItems(&ItemBase::actionFlip);
|
||||
}
|
||||
}
|
||||
|
||||
QRectF ItemBase::rightHandleRect() const {
|
||||
return QRectF(
|
||||
(_horizontalSize / 2) - (_scaledHandleSize / 2),
|
||||
0 - (_scaledHandleSize / 2),
|
||||
_scaledHandleSize,
|
||||
_scaledHandleSize);
|
||||
}
|
||||
|
||||
QRectF ItemBase::leftHandleRect() const {
|
||||
return QRectF(
|
||||
(-_horizontalSize / 2) - (_scaledHandleSize / 2),
|
||||
0 - (_scaledHandleSize / 2),
|
||||
_scaledHandleSize,
|
||||
_scaledHandleSize);
|
||||
}
|
||||
|
||||
bool ItemBase::isHandling() const {
|
||||
return _handle != HandleType::None;
|
||||
}
|
||||
|
||||
float64 ItemBase::size() const {
|
||||
return _horizontalSize;
|
||||
}
|
||||
|
||||
void ItemBase::updateVerticalSize() {
|
||||
const auto verticalSize = _horizontalSize * _aspectRatio;
|
||||
_verticalSize = std::max(
|
||||
verticalSize,
|
||||
float64(_sizeLimits.min));
|
||||
if (verticalSize < _sizeLimits.min) {
|
||||
_horizontalSize = _verticalSize / _aspectRatio;
|
||||
}
|
||||
}
|
||||
|
||||
void ItemBase::setAspectRatio(float64 aspectRatio) {
|
||||
_aspectRatio = aspectRatio;
|
||||
updateVerticalSize();
|
||||
}
|
||||
|
||||
ItemBase::HandleType ItemBase::handleType(const QPointF &pos) const {
|
||||
return rightHandleRect().contains(pos)
|
||||
? HandleType::Right
|
||||
: leftHandleRect().contains(pos)
|
||||
? HandleType::Left
|
||||
: HandleType::None;
|
||||
}
|
||||
|
||||
bool ItemBase::flipped() const {
|
||||
return _flipped;
|
||||
}
|
||||
|
||||
void ItemBase::setFlip(bool value) {
|
||||
if (_flipped != value) {
|
||||
performFlip();
|
||||
_flipped = value;
|
||||
}
|
||||
}
|
||||
|
||||
int ItemBase::type() const {
|
||||
return ItemBase::Type;
|
||||
}
|
||||
|
||||
void ItemBase::updateZoom(float64 zoom) {
|
||||
_scaledHandleSize = st::photoEditorItemHandleSize / zoom;
|
||||
_scaledInnerMargins = QMarginsF(
|
||||
_scaledHandleSize,
|
||||
_scaledHandleSize,
|
||||
_scaledHandleSize,
|
||||
_scaledHandleSize) * 0.5;
|
||||
|
||||
const auto maxSide = std::max(
|
||||
_imageSize.width(),
|
||||
_imageSize.height());
|
||||
_sizeLimits = {
|
||||
.min = int(maxSide * kMinSizeRatio),
|
||||
.max = int(maxSide * kMaxSizeRatio),
|
||||
};
|
||||
_horizontalSize = std::clamp(
|
||||
_horizontalSize,
|
||||
float64(_sizeLimits.min),
|
||||
float64(_sizeLimits.max));
|
||||
updateVerticalSize();
|
||||
|
||||
updatePens(QPen(
|
||||
QBrush(),
|
||||
1 / zoom,
|
||||
Qt::DashLine,
|
||||
Qt::SquareCap,
|
||||
Qt::RoundJoin));
|
||||
}
|
||||
|
||||
void ItemBase::performFlip() {
|
||||
}
|
||||
|
||||
void ItemBase::updatePens(QPen pen) {
|
||||
_pens = {
|
||||
.select = pen,
|
||||
.selectInactive = pen,
|
||||
.handle = pen,
|
||||
.handleInactive = pen,
|
||||
};
|
||||
_pens.select.setColor(Qt::white);
|
||||
_pens.selectInactive.setColor(Qt::gray);
|
||||
_pens.handle.setColor(Qt::white);
|
||||
_pens.handleInactive.setColor(Qt::gray);
|
||||
_pens.handle.setStyle(Qt::SolidLine);
|
||||
_pens.handleInactive.setStyle(Qt::SolidLine);
|
||||
}
|
||||
|
||||
ItemBase::Data ItemBase::generateData() const {
|
||||
return {
|
||||
.initialZoom = (st::photoEditorItemHandleSize / _scaledHandleSize),
|
||||
.zPtr = _lastZ,
|
||||
.size = int(_horizontalSize),
|
||||
.x = int(scenePos().x()),
|
||||
.y = int(scenePos().y()),
|
||||
.flipped = flipped(),
|
||||
.rotation = int(rotation()),
|
||||
.imageSize = _imageSize,
|
||||
};
|
||||
}
|
||||
|
||||
void ItemBase::applyData(const Data &data) {
|
||||
// _lastZ is const.
|
||||
// _imageSize is const.
|
||||
_horizontalSize = data.size;
|
||||
setPos(data.x, data.y);
|
||||
setZValue((*_lastZ)++);
|
||||
setFlip(data.flipped);
|
||||
setRotation(data.rotation);
|
||||
updateZoom(data.initialZoom);
|
||||
update();
|
||||
}
|
||||
|
||||
void ItemBase::save(SaveState state) {
|
||||
const auto z = zValue();
|
||||
auto &saved = (state == SaveState::Keep) ? _keeped : _saved;
|
||||
saved = {
|
||||
.data = generateData(),
|
||||
.zValue = z,
|
||||
.status = status(),
|
||||
};
|
||||
}
|
||||
|
||||
void ItemBase::restore(SaveState state) {
|
||||
if (!hasState(state)) {
|
||||
return;
|
||||
}
|
||||
const auto &saved = (state == SaveState::Keep) ? _keeped : _saved;
|
||||
applyData(saved.data);
|
||||
setZValue(saved.zValue);
|
||||
setStatus(saved.status);
|
||||
}
|
||||
|
||||
bool ItemBase::hasState(SaveState state) const {
|
||||
const auto &saved = (state == SaveState::Keep) ? _keeped : _saved;
|
||||
return saved.zValue;
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
159
Telegram/SourceFiles/editor/scene/scene_item_base.h
Normal file
159
Telegram/SourceFiles/editor/scene/scene_item_base.h
Normal file
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/unique_qptr.h"
|
||||
#include "editor/photo_editor_inner_common.h"
|
||||
|
||||
#include <QGraphicsItem>
|
||||
|
||||
class QGraphicsSceneHoverEvent;
|
||||
class QGraphicsSceneMouseEvent;
|
||||
class QStyleOptionGraphicsItem;
|
||||
|
||||
namespace Ui {
|
||||
class PopupMenu;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Editor {
|
||||
|
||||
class NumberedItem : public QGraphicsItem {
|
||||
public:
|
||||
enum class Status {
|
||||
Normal,
|
||||
Undid,
|
||||
Removed,
|
||||
};
|
||||
|
||||
enum { Type = UserType + 1 };
|
||||
using QGraphicsItem::QGraphicsItem;
|
||||
|
||||
int type() const override;
|
||||
void setNumber(int number);
|
||||
[[nodiscard]] int number() const;
|
||||
|
||||
[[nodiscard]] Status status() const;
|
||||
void setStatus(Status status);
|
||||
[[nodiscard]] bool isNormalStatus() const;
|
||||
[[nodiscard]] bool isUndidStatus() const;
|
||||
[[nodiscard]] bool isRemovedStatus() const;
|
||||
|
||||
virtual void save(SaveState state);
|
||||
virtual void restore(SaveState state);
|
||||
virtual bool hasState(SaveState state) const;
|
||||
private:
|
||||
int _number = 0;
|
||||
Status _status = Status::Normal;
|
||||
};
|
||||
|
||||
class ItemBase : public NumberedItem {
|
||||
public:
|
||||
enum { Type = UserType + 2 };
|
||||
|
||||
struct Data {
|
||||
float64 initialZoom = 0.;
|
||||
std::shared_ptr<float64> zPtr;
|
||||
int size = 0;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
bool flipped = false;
|
||||
int rotation = 0;
|
||||
QSize imageSize;
|
||||
};
|
||||
|
||||
ItemBase(Data data);
|
||||
QRectF boundingRect() const override;
|
||||
void paint(
|
||||
QPainter *p,
|
||||
const QStyleOptionGraphicsItem *option,
|
||||
QWidget *widget) override;
|
||||
int type() const override;
|
||||
|
||||
bool flipped() const;
|
||||
void setFlip(bool value);
|
||||
|
||||
void updateZoom(float64 zoom);
|
||||
|
||||
bool hasState(SaveState state) const override;
|
||||
void save(SaveState state) override;
|
||||
void restore(SaveState state) override;
|
||||
protected:
|
||||
enum HandleType {
|
||||
None,
|
||||
Left,
|
||||
Right,
|
||||
};
|
||||
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
|
||||
void hoverMoveEvent(QGraphicsSceneHoverEvent *event) override;
|
||||
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
|
||||
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
|
||||
void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override;
|
||||
void keyPressEvent(QKeyEvent *e) override;
|
||||
|
||||
using Action = void(ItemBase::*)();
|
||||
void performForSelectedItems(Action action);
|
||||
void actionFlip();
|
||||
void actionDelete();
|
||||
void actionDuplicate();
|
||||
|
||||
QRectF contentRect() const;
|
||||
QRectF innerRect() const;
|
||||
float64 size() const;
|
||||
float64 horizontalSize() const;
|
||||
float64 verticalSize() const;
|
||||
void setAspectRatio(float64 aspectRatio);
|
||||
|
||||
virtual void performFlip();
|
||||
virtual std::shared_ptr<ItemBase> duplicate(Data data) const = 0;
|
||||
private:
|
||||
HandleType handleType(const QPointF &pos) const;
|
||||
QRectF rightHandleRect() const;
|
||||
QRectF leftHandleRect() const;
|
||||
bool isHandling() const;
|
||||
void updateVerticalSize();
|
||||
void updatePens(QPen pen);
|
||||
void handleActionKey(not_null<QKeyEvent*> e);
|
||||
|
||||
Data generateData() const;
|
||||
void applyData(const Data &data);
|
||||
|
||||
const std::shared_ptr<float64> _lastZ;
|
||||
const QSize _imageSize;
|
||||
|
||||
struct {
|
||||
QPen select;
|
||||
QPen selectInactive;
|
||||
QPen handle;
|
||||
QPen handleInactive;
|
||||
} _pens;
|
||||
|
||||
base::unique_qptr<Ui::PopupMenu> _menu;
|
||||
|
||||
struct {
|
||||
Data data;
|
||||
float64 zValue = 0.;
|
||||
NumberedItem::Status status;
|
||||
} _saved, _keeped;
|
||||
|
||||
struct {
|
||||
int min = 0;
|
||||
int max = 0;
|
||||
} _sizeLimits;
|
||||
float64 _scaledHandleSize = 1.0;
|
||||
QMarginsF _scaledInnerMargins;
|
||||
|
||||
float64 _horizontalSize = 0;
|
||||
float64 _verticalSize = 0;
|
||||
float64 _aspectRatio = 1.0;
|
||||
HandleType _handle = HandleType::None;
|
||||
|
||||
bool _flipped = false;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
324
Telegram/SourceFiles/editor/scene/scene_item_canvas.cpp
Normal file
324
Telegram/SourceFiles/editor/scene/scene_item_canvas.cpp
Normal file
@@ -0,0 +1,324 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "editor/scene/scene_item_canvas.h"
|
||||
|
||||
#include <QGraphicsScene>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
|
||||
namespace Editor {
|
||||
namespace {
|
||||
|
||||
constexpr auto kMinPointDistanceBase = 2.0;
|
||||
constexpr auto kMaxPointDistance = 15.0;
|
||||
constexpr auto kSmoothingStrength = 0.5;
|
||||
constexpr auto kPressureDecay = 0.95;
|
||||
constexpr auto kMinPressure = 0.3;
|
||||
constexpr auto kBatchUpdateInterval = 16;
|
||||
constexpr auto kSegmentOverlap = 3;
|
||||
constexpr auto kHalfStrength = kSmoothingStrength / 2.0;
|
||||
constexpr auto kInvStrength = 1.0 - kSmoothingStrength;
|
||||
|
||||
[[nodiscard]] float64 PointDistance(const QPointF &a, const QPointF &b) {
|
||||
const auto dx = a.x() - b.x();
|
||||
const auto dy = a.y() - b.y();
|
||||
return std::sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ItemCanvas::ItemCanvas() {
|
||||
setAcceptedMouseButtons({});
|
||||
}
|
||||
|
||||
void ItemCanvas::clearPixmap() {
|
||||
_hq = nullptr;
|
||||
_p = nullptr;
|
||||
|
||||
_pixmap = QPixmap(
|
||||
(scene()->sceneRect().size() * style::DevicePixelRatio()).toSize());
|
||||
_pixmap.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
_pixmap.fill(Qt::transparent);
|
||||
|
||||
_p = std::make_unique<Painter>(&_pixmap);
|
||||
_hq = std::make_unique<PainterHighQualityEnabler>(*_p);
|
||||
_p->setPen(Qt::NoPen);
|
||||
_p->setBrush(_brushData.color);
|
||||
}
|
||||
|
||||
void ItemCanvas::applyBrush(const QColor &color, float size) {
|
||||
_brushData.color = color;
|
||||
_brushData.size = size;
|
||||
_p->setBrush(color);
|
||||
_brushMargins = QMarginsF(size, size, size, size);// / 2.;
|
||||
}
|
||||
|
||||
QRectF ItemCanvas::boundingRect() const {
|
||||
return scene()->sceneRect();
|
||||
}
|
||||
|
||||
void ItemCanvas::computeContentRect(const QPointF &p) {
|
||||
if (!scene()) {
|
||||
return;
|
||||
}
|
||||
const auto sceneSize = scene()->sceneRect().size();
|
||||
_contentRect = QRectF(
|
||||
QPointF(
|
||||
std::clamp(p.x() - _brushMargins.left(), 0., _contentRect.x()),
|
||||
std::clamp(p.y() - _brushMargins.top(), 0., _contentRect.y())),
|
||||
QPointF(
|
||||
std::clamp(
|
||||
p.x() + _brushMargins.right(),
|
||||
_contentRect.x() + _contentRect.width(),
|
||||
sceneSize.width()),
|
||||
std::clamp(
|
||||
p.y() + _brushMargins.bottom(),
|
||||
_contentRect.y() + _contentRect.height(),
|
||||
sceneSize.height())));
|
||||
}
|
||||
|
||||
std::vector<ItemCanvas::StrokePoint> ItemCanvas::smoothStroke(
|
||||
const std::vector<StrokePoint> &points) const {
|
||||
if (points.size() < 4) {
|
||||
return points;
|
||||
}
|
||||
auto result = std::vector<StrokePoint>();
|
||||
result.reserve(points.size());
|
||||
result.push_back(points[0]);
|
||||
result.push_back(points[1]);
|
||||
for (auto i = 2; i < int(points.size()) - 1; ++i) {
|
||||
const auto &prev = points[i - 1].pos;
|
||||
const auto &curr = points[i].pos;
|
||||
const auto &next = points[i + 1].pos;
|
||||
const auto smoothed = curr * kInvStrength
|
||||
+ (prev + next) * kHalfStrength;
|
||||
result.push_back({
|
||||
.pos = smoothed,
|
||||
.pressure = points[i].pressure,
|
||||
.time = points[i].time,
|
||||
});
|
||||
}
|
||||
result.push_back(points.back());
|
||||
return result;
|
||||
}
|
||||
|
||||
void ItemCanvas::renderSegment(
|
||||
const std::vector<StrokePoint> &points,
|
||||
int startIdx) {
|
||||
if (points.size() < 2 || startIdx >= int(points.size()) - 1) {
|
||||
return;
|
||||
}
|
||||
auto path = QPainterPath();
|
||||
const auto effectiveStart = std::max(0, startIdx);
|
||||
path.moveTo(points[effectiveStart].pos);
|
||||
for (auto i = effectiveStart; i < int(points.size()) - 1; ++i) {
|
||||
const auto &p0 = points[i].pos;
|
||||
const auto &p1 = points[i + 1].pos;
|
||||
const auto ctrl = (p0 + p1) / 2.0;
|
||||
if (i == effectiveStart) {
|
||||
path.lineTo(ctrl);
|
||||
} else {
|
||||
path.quadTo(p0, ctrl);
|
||||
}
|
||||
}
|
||||
path.lineTo(points.back().pos);
|
||||
const auto count = points.size() - std::max(0, startIdx);
|
||||
const auto avgPressure = count > 0
|
||||
? std::accumulate(
|
||||
points.begin() + std::max(0, startIdx),
|
||||
points.end(),
|
||||
0.0,
|
||||
[](float64 sum, const StrokePoint &p) {
|
||||
return sum + p.pressure;
|
||||
}) / count
|
||||
: 1.0;
|
||||
const auto width = _brushData.size * avgPressure;
|
||||
auto stroker = QPainterPathStroker();
|
||||
stroker.setWidth(width);
|
||||
stroker.setCapStyle(Qt::RoundCap);
|
||||
stroker.setJoinStyle(Qt::RoundJoin);
|
||||
const auto outline = stroker.createStroke(path);
|
||||
_p->fillPath(outline, _brushData.color);
|
||||
_rectToUpdate |= outline.boundingRect() + _brushMargins;
|
||||
}
|
||||
|
||||
void ItemCanvas::drawIncrementalStroke() {
|
||||
if (_currentStroke.size() < 2) {
|
||||
return;
|
||||
}
|
||||
const auto startIdx = std::max(
|
||||
0,
|
||||
_lastRenderedIndex - kSegmentOverlap);
|
||||
auto segment = std::vector<StrokePoint>(
|
||||
_currentStroke.begin() + startIdx,
|
||||
_currentStroke.end());
|
||||
if (segment.size() < 2) {
|
||||
return;
|
||||
}
|
||||
if (segment.size() >= 4) {
|
||||
for (auto i = 0; i < 2; ++i) {
|
||||
segment = smoothStroke(segment);
|
||||
}
|
||||
}
|
||||
renderSegment(
|
||||
segment,
|
||||
std::min(kSegmentOverlap, int(segment.size()) - 1));
|
||||
_lastRenderedIndex = std::max(
|
||||
0,
|
||||
int(_currentStroke.size()) - kSegmentOverlap);
|
||||
}
|
||||
|
||||
void ItemCanvas::addStrokePoint(const QPointF &point, int64 time) {
|
||||
if (!_currentStroke.empty()) {
|
||||
const auto distance = PointDistance(
|
||||
point,
|
||||
_currentStroke.back().pos);
|
||||
const auto minDistance = kMinPointDistanceBase * std::min(1.0, _zoom);
|
||||
if (distance < minDistance) {
|
||||
return;
|
||||
}
|
||||
if (distance > kMaxPointDistance) {
|
||||
const auto steps = int(std::ceil(distance / kMaxPointDistance));
|
||||
const auto &lastPos = _currentStroke.back().pos;
|
||||
const auto &lastPressure = _currentStroke.back().pressure;
|
||||
for (auto i = 1; i < steps; ++i) {
|
||||
const auto t = float64(i) / steps;
|
||||
const auto interpolated = lastPos * (1.0 - t) + point * t;
|
||||
const auto interpTime = _lastPointTime
|
||||
+ int64((time - _lastPointTime) * t);
|
||||
_currentStroke.push_back({
|
||||
.pos = interpolated,
|
||||
.pressure = lastPressure,
|
||||
.time = interpTime,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const auto timeDelta = _lastPointTime
|
||||
? std::max(int64(1), time - _lastPointTime)
|
||||
: kBatchUpdateInterval;
|
||||
const auto speed = !_currentStroke.empty()
|
||||
? PointDistance(point, _currentStroke.back().pos) / timeDelta
|
||||
: 0.0;
|
||||
const auto pressureFromSpeed = std::clamp(
|
||||
1.0 - speed * 0.1,
|
||||
kMinPressure,
|
||||
1.0);
|
||||
const auto pressure = _currentStroke.empty()
|
||||
? 1.0
|
||||
: _currentStroke.back().pressure * kPressureDecay
|
||||
+ pressureFromSpeed * (1.0 - kPressureDecay);
|
||||
_currentStroke.push_back({
|
||||
.pos = point,
|
||||
.pressure = pressure,
|
||||
.time = time,
|
||||
});
|
||||
_lastPointTime = time;
|
||||
computeContentRect(point);
|
||||
}
|
||||
|
||||
void ItemCanvas::handleMousePressEvent(
|
||||
not_null<QGraphicsSceneMouseEvent*> e) {
|
||||
_lastPoint = e->scenePos();
|
||||
_rectToUpdate = QRectF();
|
||||
_currentStroke.clear();
|
||||
_lastRenderedIndex = 0;
|
||||
_lastPointTime = 0;
|
||||
const auto now = crl::now();
|
||||
addStrokePoint(_lastPoint, now);
|
||||
_contentRect = QRectF(_lastPoint, _lastPoint) + _brushMargins;
|
||||
_drawing = true;
|
||||
}
|
||||
|
||||
void ItemCanvas::handleMouseMoveEvent(
|
||||
not_null<QGraphicsSceneMouseEvent*> e) {
|
||||
if (!_drawing) {
|
||||
return;
|
||||
}
|
||||
const auto scenePos = e->scenePos();
|
||||
const auto now = crl::now();
|
||||
addStrokePoint(scenePos, now);
|
||||
_lastPoint = scenePos;
|
||||
if (_currentStroke.size() - _lastRenderedIndex >= 3) {
|
||||
drawIncrementalStroke();
|
||||
update(_rectToUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
void ItemCanvas::handleMouseReleaseEvent(
|
||||
not_null<QGraphicsSceneMouseEvent*> e) {
|
||||
if (!_drawing) {
|
||||
return;
|
||||
}
|
||||
_drawing = false;
|
||||
drawIncrementalStroke();
|
||||
update(_rectToUpdate);
|
||||
if (_contentRect.isValid()) {
|
||||
const auto scaledContentRect = QRectF(
|
||||
_contentRect.x() * style::DevicePixelRatio(),
|
||||
_contentRect.y() * style::DevicePixelRatio(),
|
||||
_contentRect.width() * style::DevicePixelRatio(),
|
||||
_contentRect.height() * style::DevicePixelRatio());
|
||||
_grabContentRequests.fire({
|
||||
.pixmap = _pixmap.copy(scaledContentRect.toRect()),
|
||||
.position = _contentRect.topLeft(),
|
||||
});
|
||||
}
|
||||
_currentStroke.clear();
|
||||
_lastRenderedIndex = 0;
|
||||
_lastPointTime = 0;
|
||||
_currentPath = QPainterPath();
|
||||
clearPixmap();
|
||||
update();
|
||||
}
|
||||
|
||||
void ItemCanvas::paint(
|
||||
QPainter *p,
|
||||
const QStyleOptionGraphicsItem *,
|
||||
QWidget *) {
|
||||
p->fillRect(_rectToUpdate, Qt::transparent);
|
||||
p->drawPixmap(0, 0, _pixmap);
|
||||
_rectToUpdate = QRectF();
|
||||
}
|
||||
|
||||
rpl::producer<ItemCanvas::Content> ItemCanvas::grabContentRequests() const {
|
||||
return _grabContentRequests.events();
|
||||
}
|
||||
|
||||
bool ItemCanvas::collidesWithItem(
|
||||
const QGraphicsItem *,
|
||||
Qt::ItemSelectionMode) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ItemCanvas::collidesWithPath(
|
||||
const QPainterPath &,
|
||||
Qt::ItemSelectionMode) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
void ItemCanvas::cancelDrawing() {
|
||||
_drawing = false;
|
||||
_currentStroke.clear();
|
||||
_lastRenderedIndex = 0;
|
||||
_lastPointTime = 0;
|
||||
_currentPath = QPainterPath();
|
||||
_contentRect = QRectF();
|
||||
clearPixmap();
|
||||
update();
|
||||
}
|
||||
|
||||
void ItemCanvas::updateZoom(float64 zoom) {
|
||||
_zoom = zoom;
|
||||
}
|
||||
|
||||
ItemCanvas::~ItemCanvas() {
|
||||
_hq = nullptr;
|
||||
_p = nullptr;
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
96
Telegram/SourceFiles/editor/scene/scene_item_canvas.h
Normal file
96
Telegram/SourceFiles/editor/scene/scene_item_canvas.h
Normal file
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/painter.h"
|
||||
|
||||
#include <QGraphicsItem>
|
||||
|
||||
class QGraphicsSceneMouseEvent;
|
||||
|
||||
namespace Editor {
|
||||
|
||||
class ItemCanvas : public QGraphicsItem {
|
||||
public:
|
||||
struct Content {
|
||||
QPixmap pixmap;
|
||||
QPointF position;
|
||||
};
|
||||
|
||||
ItemCanvas();
|
||||
~ItemCanvas();
|
||||
|
||||
void applyBrush(const QColor &color, float size);
|
||||
void clearPixmap();
|
||||
void cancelDrawing();
|
||||
void updateZoom(float64 zoom);
|
||||
|
||||
QRectF boundingRect() const override;
|
||||
void paint(
|
||||
QPainter *p,
|
||||
const QStyleOptionGraphicsItem *option,
|
||||
QWidget *widget) override;
|
||||
|
||||
void handleMousePressEvent(not_null<QGraphicsSceneMouseEvent*> event);
|
||||
void handleMouseReleaseEvent(not_null<QGraphicsSceneMouseEvent*> event);
|
||||
void handleMouseMoveEvent(not_null<QGraphicsSceneMouseEvent*> event);
|
||||
|
||||
[[nodiscard]] rpl::producer<Content> grabContentRequests() const;
|
||||
|
||||
protected:
|
||||
bool collidesWithItem(
|
||||
const QGraphicsItem *,
|
||||
Qt::ItemSelectionMode) const override;
|
||||
|
||||
bool collidesWithPath(
|
||||
const QPainterPath &,
|
||||
Qt::ItemSelectionMode) const override;
|
||||
private:
|
||||
struct StrokePoint {
|
||||
QPointF pos;
|
||||
float64 pressure = 1.0;
|
||||
int64 time = 0;
|
||||
};
|
||||
|
||||
void computeContentRect(const QPointF &p);
|
||||
void addStrokePoint(const QPointF &point, int64 time);
|
||||
void drawIncrementalStroke();
|
||||
std::vector<StrokePoint> smoothStroke(
|
||||
const std::vector<StrokePoint> &points) const;
|
||||
void renderSegment(
|
||||
const std::vector<StrokePoint> &points,
|
||||
int startIdx);
|
||||
|
||||
bool _drawing = false;
|
||||
std::vector<StrokePoint> _currentStroke;
|
||||
int _lastRenderedIndex = 0;
|
||||
float64 _zoom = 1.0;
|
||||
int64 _lastPointTime = 0;
|
||||
|
||||
std::unique_ptr<PainterHighQualityEnabler> _hq;
|
||||
std::unique_ptr<Painter> _p;
|
||||
|
||||
QRectF _rectToUpdate;
|
||||
QRectF _contentRect;
|
||||
QMarginsF _brushMargins;
|
||||
|
||||
QPointF _lastPoint;
|
||||
|
||||
QPixmap _pixmap;
|
||||
QPainterPath _currentPath;
|
||||
|
||||
struct {
|
||||
float size = 1.;
|
||||
QColor color;
|
||||
} _brushData;
|
||||
|
||||
rpl::event_stream<Content> _grabContentRequests;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
46
Telegram/SourceFiles/editor/scene/scene_item_image.cpp
Normal file
46
Telegram/SourceFiles/editor/scene/scene_item_image.cpp
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "editor/scene/scene_item_image.h"
|
||||
|
||||
namespace Editor {
|
||||
|
||||
ItemImage::ItemImage(
|
||||
QPixmap &&pixmap,
|
||||
ItemBase::Data data)
|
||||
: ItemBase(std::move(data))
|
||||
, _pixmap(std::move(pixmap)) {
|
||||
setAspectRatio(_pixmap.isNull()
|
||||
? 1.0
|
||||
: (_pixmap.height() / float64(_pixmap.width())));
|
||||
}
|
||||
|
||||
void ItemImage::paint(
|
||||
QPainter *p,
|
||||
const QStyleOptionGraphicsItem *option,
|
||||
QWidget *w) {
|
||||
const auto rect = contentRect();
|
||||
const auto pixmapSize = QSizeF(_pixmap.size() / style::DevicePixelRatio())
|
||||
.scaled(rect.size(), Qt::KeepAspectRatio);
|
||||
const auto resultRect = QRectF(rect.topLeft(), pixmapSize).translated(
|
||||
(rect.width() - pixmapSize.width()) / 2.,
|
||||
(rect.height() - pixmapSize.height()) / 2.);
|
||||
p->drawPixmap(resultRect.toRect(), _pixmap);
|
||||
ItemBase::paint(p, option, w);
|
||||
}
|
||||
|
||||
void ItemImage::performFlip() {
|
||||
_pixmap = _pixmap.transformed(QTransform().scale(-1, 1));
|
||||
update();
|
||||
}
|
||||
|
||||
std::shared_ptr<ItemBase> ItemImage::duplicate(ItemBase::Data data) const {
|
||||
auto pixmap = _pixmap;
|
||||
return std::make_shared<ItemImage>(std::move(pixmap), std::move(data));
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
31
Telegram/SourceFiles/editor/scene/scene_item_image.h
Normal file
31
Telegram/SourceFiles/editor/scene/scene_item_image.h
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "editor/scene/scene_item_base.h"
|
||||
|
||||
namespace Editor {
|
||||
|
||||
class ItemImage : public ItemBase {
|
||||
public:
|
||||
ItemImage(
|
||||
QPixmap &&pixmap,
|
||||
ItemBase::Data data);
|
||||
void paint(
|
||||
QPainter *p,
|
||||
const QStyleOptionGraphicsItem *option,
|
||||
QWidget *widget) override;
|
||||
protected:
|
||||
void performFlip() override;
|
||||
std::shared_ptr<ItemBase> duplicate(ItemBase::Data data) const override;
|
||||
private:
|
||||
QPixmap _pixmap;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
62
Telegram/SourceFiles/editor/scene/scene_item_line.cpp
Normal file
62
Telegram/SourceFiles/editor/scene/scene_item_line.cpp
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "editor/scene/scene_item_line.h"
|
||||
|
||||
#include <QGraphicsScene>
|
||||
|
||||
namespace Editor {
|
||||
|
||||
ItemLine::ItemLine(QPixmap &&pixmap)
|
||||
: _pixmap(std::move(pixmap))
|
||||
, _rect(QPointF(), _pixmap.size() / float64(style::DevicePixelRatio())) {
|
||||
}
|
||||
|
||||
QRectF ItemLine::boundingRect() const {
|
||||
return _rect;
|
||||
}
|
||||
|
||||
void ItemLine::paint(
|
||||
QPainter *p,
|
||||
const QStyleOptionGraphicsItem *,
|
||||
QWidget *) {
|
||||
p->drawPixmap(0, 0, _pixmap);
|
||||
}
|
||||
|
||||
bool ItemLine::collidesWithItem(
|
||||
const QGraphicsItem *,
|
||||
Qt::ItemSelectionMode) const {
|
||||
return false;
|
||||
}
|
||||
bool ItemLine::collidesWithPath(
|
||||
const QPainterPath &,
|
||||
Qt::ItemSelectionMode) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
void ItemLine::save(SaveState state) {
|
||||
auto &saved = (state == SaveState::Keep) ? _keeped : _saved;
|
||||
saved = {
|
||||
.saved = true,
|
||||
.status = status(),
|
||||
};
|
||||
}
|
||||
|
||||
void ItemLine::restore(SaveState state) {
|
||||
if (!hasState(state)) {
|
||||
return;
|
||||
}
|
||||
const auto &saved = (state == SaveState::Keep) ? _keeped : _saved;
|
||||
setStatus(saved.status);
|
||||
}
|
||||
|
||||
bool ItemLine::hasState(SaveState state) const {
|
||||
const auto &saved = (state == SaveState::Keep) ? _keeped : _saved;
|
||||
return saved.saved;
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
44
Telegram/SourceFiles/editor/scene/scene_item_line.h
Normal file
44
Telegram/SourceFiles/editor/scene/scene_item_line.h
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "editor/scene/scene_item_base.h"
|
||||
|
||||
namespace Editor {
|
||||
|
||||
class ItemLine : public NumberedItem {
|
||||
public:
|
||||
ItemLine(QPixmap &&pixmap);
|
||||
QRectF boundingRect() const override;
|
||||
void paint(
|
||||
QPainter *p,
|
||||
const QStyleOptionGraphicsItem *option,
|
||||
QWidget *widget) override;
|
||||
|
||||
bool hasState(SaveState state) const override;
|
||||
void save(SaveState state) override;
|
||||
void restore(SaveState state) override;
|
||||
protected:
|
||||
bool collidesWithItem(
|
||||
const QGraphicsItem *,
|
||||
Qt::ItemSelectionMode) const override;
|
||||
bool collidesWithPath(
|
||||
const QPainterPath &,
|
||||
Qt::ItemSelectionMode) const override;
|
||||
private:
|
||||
const QPixmap _pixmap;
|
||||
const QRectF _rect;
|
||||
|
||||
struct {
|
||||
bool saved = false;
|
||||
NumberedItem::Status status;
|
||||
} _saved, _keeped;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
140
Telegram/SourceFiles/editor/scene/scene_item_sticker.cpp
Normal file
140
Telegram/SourceFiles/editor/scene/scene_item_sticker.cpp
Normal file
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "editor/scene/scene_item_sticker.h"
|
||||
|
||||
#include "chat_helpers/stickers_lottie.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_document_media.h"
|
||||
#include "data/data_session.h"
|
||||
#include "lottie/lottie_common.h"
|
||||
#include "lottie/lottie_single_player.h"
|
||||
#include "main/main_session.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "styles/style_editor.h"
|
||||
|
||||
namespace Editor {
|
||||
namespace {
|
||||
|
||||
} // namespace
|
||||
|
||||
ItemSticker::ItemSticker(
|
||||
not_null<DocumentData*> document,
|
||||
ItemBase::Data data)
|
||||
: ItemBase(std::move(data))
|
||||
, _document(document)
|
||||
, _mediaView(_document->createMediaView()) {
|
||||
const auto stickerData = document->sticker();
|
||||
if (!stickerData) {
|
||||
return;
|
||||
}
|
||||
const auto updateThumbnail = [=] {
|
||||
const auto guard = gsl::finally([&] {
|
||||
if (_image.isNull()) {
|
||||
setAspectRatio(1.);
|
||||
}
|
||||
});
|
||||
if (stickerData->isLottie()) {
|
||||
_lottie.player = ChatHelpers::LottiePlayerFromDocument(
|
||||
_mediaView.get(),
|
||||
ChatHelpers::StickerLottieSize::MessageHistory,
|
||||
QSize(kStickerSideSize, kStickerSideSize)
|
||||
* style::DevicePixelRatio(),
|
||||
Lottie::Quality::High);
|
||||
_lottie.player->updates(
|
||||
) | rpl::on_next([=] {
|
||||
updatePixmap(_lottie.player->frame());
|
||||
_lottie.player = nullptr;
|
||||
_lottie.lifetime.destroy();
|
||||
update();
|
||||
}, _lottie.lifetime);
|
||||
return true;
|
||||
} else if (stickerData->isWebm()
|
||||
&& !_document->dimensions.isEmpty()) {
|
||||
const auto callback = [=](::Media::Clip::Notification) {
|
||||
const auto size = _document->dimensions;
|
||||
if (_webm && _webm->ready() && !_webm->started()) {
|
||||
_webm->start({ .frame = size, .keepAlpha = true });
|
||||
}
|
||||
if (_webm && _webm->started()) {
|
||||
updatePixmap(_webm->current(
|
||||
{ .frame = size, .keepAlpha = true },
|
||||
0));
|
||||
_webm = nullptr;
|
||||
}
|
||||
};
|
||||
_webm = ::Media::Clip::MakeReader(
|
||||
_mediaView->owner()->location(),
|
||||
_mediaView->bytes(),
|
||||
callback);
|
||||
return true;
|
||||
}
|
||||
const auto sticker = _mediaView->getStickerLarge();
|
||||
if (!sticker) {
|
||||
return false;
|
||||
}
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
auto pixmap = sticker->pixNoCache(sticker->size() * ratio);
|
||||
pixmap.setDevicePixelRatio(ratio);
|
||||
updatePixmap(pixmap.toImage());
|
||||
return true;
|
||||
};
|
||||
if (!updateThumbnail()) {
|
||||
_document->session().downloaderTaskFinished(
|
||||
) | rpl::on_next([=] {
|
||||
if (updateThumbnail()) {
|
||||
_loadingLifetime.destroy();
|
||||
update();
|
||||
}
|
||||
}, _loadingLifetime);
|
||||
}
|
||||
}
|
||||
|
||||
void ItemSticker::updatePixmap(QImage &&image) {
|
||||
_image = std::move(image);
|
||||
if (flipped()) {
|
||||
performFlip();
|
||||
} else {
|
||||
update();
|
||||
}
|
||||
if (!_image.isNull()) {
|
||||
setAspectRatio(_image.height() / float64(_image.width()));
|
||||
}
|
||||
}
|
||||
|
||||
void ItemSticker::paint(
|
||||
QPainter *p,
|
||||
const QStyleOptionGraphicsItem *option,
|
||||
QWidget *w) {
|
||||
const auto rect = contentRect();
|
||||
const auto imageSize = QSizeF(_image.size() / style::DevicePixelRatio())
|
||||
.scaled(rect.size(), Qt::KeepAspectRatio);
|
||||
const auto resultRect = QRectF(rect.topLeft(), imageSize).translated(
|
||||
(rect.width() - imageSize.width()) / 2.,
|
||||
(rect.height() - imageSize.height()) / 2.);
|
||||
p->drawImage(resultRect, _image);
|
||||
ItemBase::paint(p, option, w);
|
||||
}
|
||||
|
||||
not_null<DocumentData*> ItemSticker::sticker() const {
|
||||
return _document;
|
||||
}
|
||||
|
||||
int ItemSticker::type() const {
|
||||
return Type;
|
||||
}
|
||||
|
||||
void ItemSticker::performFlip() {
|
||||
_image = _image.transformed(QTransform().scale(-1, 1));
|
||||
update();
|
||||
}
|
||||
|
||||
std::shared_ptr<ItemBase> ItemSticker::duplicate(ItemBase::Data data) const {
|
||||
return std::make_shared<ItemSticker>(_document, std::move(data));
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
58
Telegram/SourceFiles/editor/scene/scene_item_sticker.h
Normal file
58
Telegram/SourceFiles/editor/scene/scene_item_sticker.h
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "editor/scene/scene_item_base.h"
|
||||
#include "media/clip/media_clip_reader.h"
|
||||
|
||||
namespace Data {
|
||||
class DocumentMedia;
|
||||
} // namespace Data
|
||||
namespace Lottie {
|
||||
class SinglePlayer;
|
||||
} // namespace Lottie
|
||||
class DocumentData;
|
||||
|
||||
namespace Editor {
|
||||
|
||||
class ItemSticker : public ItemBase {
|
||||
public:
|
||||
enum { Type = ItemBase::Type + 1 };
|
||||
|
||||
ItemSticker(
|
||||
not_null<DocumentData*> document,
|
||||
ItemBase::Data data);
|
||||
void paint(
|
||||
QPainter *p,
|
||||
const QStyleOptionGraphicsItem *option,
|
||||
QWidget *widget) override;
|
||||
[[nodiscard]] not_null<DocumentData*> sticker() const;
|
||||
int type() const override;
|
||||
|
||||
protected:
|
||||
void performFlip() override;
|
||||
std::shared_ptr<ItemBase> duplicate(ItemBase::Data data) const override;
|
||||
|
||||
private:
|
||||
const not_null<DocumentData*> _document;
|
||||
const std::shared_ptr<::Data::DocumentMedia> _mediaView;
|
||||
|
||||
void updatePixmap(QImage &&image);
|
||||
|
||||
struct {
|
||||
std::unique_ptr<Lottie::SinglePlayer> player;
|
||||
rpl::lifetime lifetime;
|
||||
} _lottie;
|
||||
::Media::Clip::ReaderPointer _webm;
|
||||
QImage _image;
|
||||
|
||||
rpl::lifetime _loadingLifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
Reference in New Issue
Block a user