504 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			504 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
/*
 | 
						|
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 "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::semiboldTextStyle, 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::start_with_next([=] {
 | 
						|
		Painter p(this);
 | 
						|
 | 
						|
		// p.drawImage(QPoint(), bg);
 | 
						|
 | 
						|
		paintRipple(p, _rippleRect.x(), _rippleRect.y());
 | 
						|
 | 
						|
		p.setPen(_fg);
 | 
						|
		const auto textTop = (height() - _text.minHeight()) / 2;
 | 
						|
		_text.draw(p, 0, textTop, width(), style::al_center);
 | 
						|
	}, lifetime());
 | 
						|
}
 | 
						|
 | 
						|
QImage EdgeButton::rounded(std::optional<QColor> color) const {
 | 
						|
	auto result = QImage(
 | 
						|
		_rippleRect.size() * cIntRetinaFactor(),
 | 
						|
		QImage::Format_ARGB32_Premultiplied);
 | 
						|
	result.setDevicePixelRatio(cIntRetinaFactor());
 | 
						|
	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::start_with_next([=](const QSize &size) {
 | 
						|
		const auto children = RpWidget::children();
 | 
						|
		if (children.empty()) {
 | 
						|
			return;
 | 
						|
		}
 | 
						|
		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<Ui::RpWidget*>(object.get());
 | 
						|
		}) | ranges::to_vector;
 | 
						|
 | 
						|
		const auto residualWidth = size.width()
 | 
						|
			- ranges::accumulate(widgets, 0, ranges::plus(), &QWidget::width);
 | 
						|
		const auto step = residualWidth / float(widgets.size() - 1);
 | 
						|
 | 
						|
		auto left = 0.;
 | 
						|
		for (const auto &widget : widgets) {
 | 
						|
			widget->moveToLeft(int(left), 0);
 | 
						|
			left += widget->width() + step;
 | 
						|
		}
 | 
						|
 | 
						|
		auto result = QImage(
 | 
						|
			size * cIntRetinaFactor(),
 | 
						|
			QImage::Format_ARGB32_Premultiplied);
 | 
						|
		result.setDevicePixelRatio(cIntRetinaFactor());
 | 
						|
		result.fill(bg->c);
 | 
						|
 | 
						|
		_roundedBg = Images::Round(
 | 
						|
			std::move(result),
 | 
						|
			ImageRoundRadius::Large);
 | 
						|
	}, lifetime());
 | 
						|
 | 
						|
	paintRequest(
 | 
						|
	) | rpl::start_with_next([=] {
 | 
						|
		Painter p(this);
 | 
						|
 | 
						|
		p.drawImage(QPoint(), _roundedBg);
 | 
						|
	}, lifetime());
 | 
						|
}
 | 
						|
 | 
						|
PhotoEditorControls::PhotoEditorControls(
 | 
						|
	not_null<Ui::RpWidget*> parent,
 | 
						|
	std::shared_ptr<Controllers> controllers,
 | 
						|
	const PhotoModifications modifications,
 | 
						|
	bool doneControls)
 | 
						|
: 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))
 | 
						|
, _transformCancel(base::make_unique_q<EdgeButton>(
 | 
						|
	_transformButtons,
 | 
						|
	tr::lng_cancel(tr::now),
 | 
						|
	_buttonHeight,
 | 
						|
	true,
 | 
						|
	_bg,
 | 
						|
	st::activeButtonFg,
 | 
						|
	st::photoEditorRotateButton.ripple))
 | 
						|
, _rotateButton(base::make_unique_q<Ui::IconButton>(
 | 
						|
	_transformButtons,
 | 
						|
	st::photoEditorRotateButton))
 | 
						|
, _flipButton(base::make_unique_q<Ui::IconButton>(
 | 
						|
	_transformButtons,
 | 
						|
	st::photoEditorFlipButton))
 | 
						|
, _paintModeButton(base::make_unique_q<Ui::IconButton>(
 | 
						|
	_transformButtons,
 | 
						|
	st::photoEditorPaintModeButton))
 | 
						|
, _transformDone(base::make_unique_q<EdgeButton>(
 | 
						|
	_transformButtons,
 | 
						|
	tr::lng_box_done(tr::now),
 | 
						|
	_buttonHeight,
 | 
						|
	false,
 | 
						|
	_bg,
 | 
						|
	st::lightButtonFg,
 | 
						|
	st::photoEditorRotateButton.ripple))
 | 
						|
, _paintCancel(base::make_unique_q<EdgeButton>(
 | 
						|
	_paintBottomButtons,
 | 
						|
	tr::lng_cancel(tr::now),
 | 
						|
	_buttonHeight,
 | 
						|
	true,
 | 
						|
	_bg,
 | 
						|
	st::activeButtonFg,
 | 
						|
	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::lightButtonFg,
 | 
						|
	st::photoEditorRotateButton.ripple)) {
 | 
						|
 | 
						|
	{
 | 
						|
		const auto &padding = st::photoEditorButtonBarPadding;
 | 
						|
		const auto w = st::photoEditorButtonBarWidth
 | 
						|
			- padding.left()
 | 
						|
			- padding.right();
 | 
						|
		_transformButtons->resize(w, _buttonHeight);
 | 
						|
		_paintBottomButtons->resize(w, _buttonHeight);
 | 
						|
		_paintTopButtons->resize(w, _buttonHeight);
 | 
						|
	}
 | 
						|
 | 
						|
	{
 | 
						|
		const auto icon = &st::photoEditorPaintIconActive;
 | 
						|
		_paintModeButtonActive->setIconOverride(icon, icon);
 | 
						|
	}
 | 
						|
	_paintModeButtonActive->setAttribute(Qt::WA_TransparentForMouseEvents);
 | 
						|
 | 
						|
	sizeValue(
 | 
						|
	) | rpl::start_with_next([=](const QSize &size) {
 | 
						|
		if (size.isEmpty()) {
 | 
						|
			return;
 | 
						|
		}
 | 
						|
 | 
						|
		const auto buttonsTop = bottomButtonsTop();
 | 
						|
 | 
						|
		const auto ¤t = _transformButtons->isHidden()
 | 
						|
			? _paintBottomButtons
 | 
						|
			: _transformButtons;
 | 
						|
 | 
						|
		current->moveToLeft(
 | 
						|
			(size.width() - current->width()) / 2,
 | 
						|
			buttonsTop);
 | 
						|
	}, lifetime());
 | 
						|
 | 
						|
	_mode.changes(
 | 
						|
	) | rpl::start_with_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::start_with_next([=](const QPoint &containerPos) {
 | 
						|
		_paintTopButtons->moveToLeft(
 | 
						|
			containerPos.x(),
 | 
						|
			containerPos.y()
 | 
						|
				- st::photoEditorControlsCenterSkip
 | 
						|
				- _paintTopButtons->height());
 | 
						|
	}, _paintBottomButtons->lifetime());
 | 
						|
 | 
						|
	_paintBottomButtons->shownValue(
 | 
						|
	) | rpl::start_with_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::start_with_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::start_with_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::start_with_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);
 | 
						|
 | 
						|
	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
 |