523 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			523 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| // This file is part of Desktop App Toolkit,
 | |
| // a set of libraries for developing nice desktop applications.
 | |
| //
 | |
| // For license and copyright information please follow this link:
 | |
| // https://github.com/desktop-app/legal/blob/master/LEGAL
 | |
| //
 | |
| #include "ui/platform/ui_platform_window_title.h"
 | |
| 
 | |
| #include "ui/platform/ui_platform_utility.h"
 | |
| #include "ui/widgets/buttons.h"
 | |
| #include "ui/widgets/shadow.h"
 | |
| #include "ui/ui_utility.h"
 | |
| #include "ui/widgets/rp_window.h"
 | |
| #include "styles/style_widgets.h"
 | |
| #include "styles/palette.h"
 | |
| #include "base/algorithm.h"
 | |
| #include "base/event_filter.h"
 | |
| #include "base/platform/base_platform_info.h"
 | |
| 
 | |
| #include <QtGui/QPainter>
 | |
| #include <QtGui/QtEvents>
 | |
| #include <QtGui/QWindow>
 | |
| 
 | |
| namespace Ui {
 | |
| namespace Platform {
 | |
| namespace {
 | |
| 
 | |
| template <typename T>
 | |
| void RemoveDuplicates(std::vector<T> &v) {
 | |
| 	auto end = v.end();
 | |
| 	for (auto it = v.begin(); it != end; ++it) {
 | |
| 		end = std::remove(it + 1, end, *it);
 | |
| 	}
 | |
| 
 | |
| 	v.erase(end, v.end());
 | |
| }
 | |
| 
 | |
| } // namespace
 | |
| 
 | |
| bool SemiNativeSystemButtonProcessing() {
 | |
| 	return ::Platform::IsWindows11OrGreater();
 | |
| }
 | |
| 
 | |
| void SetupSemiNativeSystemButtons(
 | |
| 		not_null<TitleControls*> controls,
 | |
| 		not_null<RpWindow*> window,
 | |
| 		rpl::lifetime &lifetime,
 | |
| 		Fn<bool()> filter) {
 | |
| 	if (!SemiNativeSystemButtonProcessing()) {
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	window->systemButtonOver(
 | |
| 	) | rpl::filter([=](HitTestResult button) {
 | |
| 		return !filter || filter() || (button == HitTestResult::None);
 | |
| 	}) | rpl::start_with_next([=](HitTestResult button) {
 | |
| 		controls->buttonOver(button);
 | |
| 	}, lifetime);
 | |
| 
 | |
| 	window->systemButtonDown(
 | |
| 	) | rpl::filter([=](HitTestResult button) {
 | |
| 		return !filter || filter() || (button == HitTestResult::None);
 | |
| 	}) | rpl::start_with_next([=](HitTestResult button) {
 | |
| 		controls->buttonDown(button);
 | |
| 	}, lifetime);
 | |
| }
 | |
| 
 | |
| class TitleControls::Button final : public IconButton {
 | |
| public:
 | |
| 	using IconButton::IconButton;
 | |
| 
 | |
| 	void setOver(bool over) {
 | |
| 		IconButton::setOver(over, StateChangeSource::ByPress);
 | |
| 	}
 | |
| 	void setDown(bool down) {
 | |
| 		IconButton::setDown(
 | |
| 			down,
 | |
| 			StateChangeSource::ByPress,
 | |
| 			{},
 | |
| 			Qt::LeftButton);
 | |
| 	}
 | |
| };
 | |
| 
 | |
| TitleControls::TitleControls(
 | |
| 	not_null<RpWidget*> parent,
 | |
| 	const style::WindowTitle &st,
 | |
| 	Fn<void(bool maximized)> maximize)
 | |
| : _st(&st)
 | |
| , _minimize(parent, _st->minimize)
 | |
| , _maximizeRestore(parent, _st->maximize)
 | |
| , _close(parent, _st->close)
 | |
| , _maximizedState(parent->windowState()
 | |
| 	& (Qt::WindowMaximized | Qt::WindowFullScreen))
 | |
| , _activeState(parent->isActiveWindow()) {
 | |
| 	init(std::move(maximize));
 | |
| 
 | |
| 	_close->paintRequest(
 | |
| 	) | rpl::start_with_next([=] {
 | |
| 		const auto active = window()->isActiveWindow();
 | |
| 		if (_activeState != active) {
 | |
| 			_activeState = active;
 | |
| 			updateButtonsState();
 | |
| 		}
 | |
| 	}, _close->lifetime());
 | |
| }
 | |
| 
 | |
| void TitleControls::setStyle(const style::WindowTitle &st) {
 | |
| 	_st = &st;
 | |
| 	updateButtonsState();
 | |
| }
 | |
| 
 | |
| not_null<const style::WindowTitle*> TitleControls::st() const {
 | |
| 	return _st;
 | |
| }
 | |
| 
 | |
| QRect TitleControls::geometry() const {
 | |
| 	auto result = QRect();
 | |
| 	const auto add = [&](auto &&control) {
 | |
| 		if (!control->isHidden()) {
 | |
| 			result = result.united(control->geometry());
 | |
| 		}
 | |
| 	};
 | |
| 	add(_minimize);
 | |
| 	add(_maximizeRestore);
 | |
| 	add(_close);
 | |
| 	return result;
 | |
| }
 | |
| 
 | |
| not_null<RpWidget*> TitleControls::parent() const {
 | |
| 	return static_cast<RpWidget*>(_close->parentWidget());
 | |
| }
 | |
| 
 | |
| not_null<QWidget*> TitleControls::window() const {
 | |
| 	return _close->window();
 | |
| }
 | |
| 
 | |
| void TitleControls::init(Fn<void(bool maximized)> maximize) {
 | |
| 	_minimize->setClickedCallback([=] {
 | |
| 		const auto weak = MakeWeak(_minimize.data());
 | |
| 		window()->setWindowState(
 | |
| 			window()->windowState() | Qt::WindowMinimized);
 | |
| 		if (weak) {
 | |
| 			_minimize->clearState();
 | |
| 		}
 | |
| 	});
 | |
| 	_minimize->setPointerCursor(false);
 | |
| 	_maximizeRestore->setClickedCallback([=] {
 | |
| 		const auto weak = MakeWeak(_maximizeRestore.data());
 | |
| 		if (maximize) {
 | |
| 			maximize(!_maximizedState);
 | |
| 		} else {
 | |
| 			window()->setWindowState(_maximizedState
 | |
| 				? Qt::WindowNoState
 | |
| 				: Qt::WindowMaximized);
 | |
| 		}
 | |
| 		if (weak) {
 | |
| 			_maximizeRestore->clearState();
 | |
| 		}
 | |
| 	});
 | |
| 	_maximizeRestore->setPointerCursor(false);
 | |
| 	_close->setClickedCallback([=] {
 | |
| 		const auto weak = MakeWeak(_close.data());
 | |
| 		window()->close();
 | |
| 		if (weak) {
 | |
| 			_close->clearState();
 | |
| 		}
 | |
| 	});
 | |
| 	_close->setPointerCursor(false);
 | |
| 
 | |
| 	rpl::combine(
 | |
| 		parent()->widthValue(),
 | |
| 		TitleControlsLayoutValue()
 | |
| 	) | rpl::start_with_next([=] {
 | |
| 		updateControlsPosition();
 | |
| 	}, _close->lifetime());
 | |
| 
 | |
| 	subscribeToStateChanges();
 | |
| 
 | |
| 	_activeState = parent()->isActiveWindow();
 | |
| 	updateButtonsState();
 | |
| }
 | |
| 
 | |
| void TitleControls::subscribeToStateChanges() {
 | |
| 	const auto subscribe = [=] {
 | |
| 		QObject::connect(
 | |
| 			window()->windowHandle(),
 | |
| 			&QWindow::windowStateChanged,
 | |
| 			[=](Qt::WindowState state) { handleWindowStateChanged(state); });
 | |
| 	};
 | |
| 	if (window()->windowHandle()) {
 | |
| 		subscribe();
 | |
| 	} else {
 | |
| 		const auto winIdEventFilter = std::make_shared<QObject*>(nullptr);
 | |
| 		*winIdEventFilter = base::install_event_filter(
 | |
| 			window(),
 | |
| 			[=](not_null<QEvent*> e) {
 | |
| 				if (!*winIdEventFilter || e->type() != QEvent::WinIdChange) {
 | |
| 					return base::EventFilterResult::Continue;
 | |
| 				}
 | |
| 				subscribe();
 | |
| 				base::take(*winIdEventFilter)->deleteLater();
 | |
| 				return base::EventFilterResult::Continue;
 | |
| 			});
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void TitleControls::setResizeEnabled(bool enabled) {
 | |
| 	_resizeEnabled = enabled;
 | |
| 	updateControlsPosition();
 | |
| }
 | |
| 
 | |
| void TitleControls::raise() {
 | |
| 	_minimize->raise();
 | |
| 	_maximizeRestore->raise();
 | |
| 	_close->raise();
 | |
| }
 | |
| 
 | |
| HitTestResult TitleControls::hitTest(QPoint point, int padding) const {
 | |
| 	const auto test = [&](const object_ptr<Button> &button, bool close) {
 | |
| 		return button && button->geometry().marginsAdded(
 | |
| 			{ close ? padding : 0, padding, close ? padding : 0, 0 }
 | |
| 		).contains(point);
 | |
| 	};
 | |
| 	if (test(_minimize, false)) {
 | |
| 		return HitTestResult::Minimize;
 | |
| 	} else if (test(_maximizeRestore, false)) {
 | |
| 		return HitTestResult::MaximizeRestore;
 | |
| 	} else if (test(_close, true)) {
 | |
| 		return HitTestResult::Close;
 | |
| 	}
 | |
| 	return HitTestResult::None;
 | |
| }
 | |
| 
 | |
| void TitleControls::buttonOver(HitTestResult testResult) {
 | |
| 	const auto update = [&](
 | |
| 			const object_ptr<Button> &button,
 | |
| 			HitTestResult buttonTestResult) {
 | |
| 		if (const auto raw = button.data()) {
 | |
| 			raw->setOver(testResult == buttonTestResult);
 | |
| 		}
 | |
| 	};
 | |
| 	update(_minimize, HitTestResult::Minimize);
 | |
| 	update(_maximizeRestore, HitTestResult::MaximizeRestore);
 | |
| 	update(_close, HitTestResult::Close);
 | |
| }
 | |
| 
 | |
| void TitleControls::buttonDown(HitTestResult testResult) {
 | |
| 	const auto update = [&](
 | |
| 			const object_ptr<Button> &button,
 | |
| 			HitTestResult buttonTestResult) {
 | |
| 		if (const auto raw = button.data()) {
 | |
| 			raw->setDown(testResult == buttonTestResult);
 | |
| 		}
 | |
| 	};
 | |
| 	update(_minimize, HitTestResult::Minimize);
 | |
| 	update(_maximizeRestore, HitTestResult::MaximizeRestore);
 | |
| 	update(_close, HitTestResult::Close);
 | |
| }
 | |
| 
 | |
| TitleControls::Button *TitleControls::controlWidget(Control control) const {
 | |
| 	switch (control) {
 | |
| 	case Control::Minimize: return _minimize;
 | |
| 	case Control::Maximize: return _maximizeRestore;
 | |
| 	case Control::Close: return _close;
 | |
| 	}
 | |
| 
 | |
| 	return nullptr;
 | |
| }
 | |
| 
 | |
| void TitleControls::updateControlsPosition() {
 | |
| 	const auto controlsLayout = TitleControlsLayout();
 | |
| 	auto controlsLeft = controlsLayout.left;
 | |
| 	auto controlsRight = controlsLayout.right;
 | |
| 	const auto moveFromTo = [&](auto &from, auto &to) {
 | |
| 		for (const auto control : from) {
 | |
| 			if (!ranges::contains(to, control)) {
 | |
| 				to.push_back(control);
 | |
| 			}
 | |
| 		}
 | |
| 		from.clear();
 | |
| 	};
 | |
| 	if (ranges::contains(controlsLeft, Control::Close)) {
 | |
| 		moveFromTo(controlsRight, controlsLeft);
 | |
| 	} else if (ranges::contains(controlsRight, Control::Close)) {
 | |
| 		moveFromTo(controlsLeft, controlsRight);
 | |
| 	} else if (controlsLeft.size() > controlsRight.size()) {
 | |
| 		moveFromTo(controlsRight, controlsLeft);
 | |
| 	} else {
 | |
| 		moveFromTo(controlsLeft, controlsRight);
 | |
| 	}
 | |
| 
 | |
| 	const auto controlPresent = [&](Control control) {
 | |
| 		return ranges::contains(controlsLeft, control)
 | |
| 		|| ranges::contains(controlsRight, control);
 | |
| 	};
 | |
| 
 | |
| 	const auto eraseControl = [&](Control control) {
 | |
| 		controlsLeft.erase(
 | |
| 			ranges::remove(controlsLeft, control),
 | |
| 			end(controlsLeft));
 | |
| 
 | |
| 		controlsRight.erase(
 | |
| 			ranges::remove(controlsRight, control),
 | |
| 			end(controlsRight));
 | |
| 	};
 | |
| 
 | |
| 	if (!_resizeEnabled) {
 | |
| 		eraseControl(Control::Maximize);
 | |
| 	}
 | |
| 
 | |
| 	if (controlPresent(Control::Minimize)) {
 | |
| 		_minimize->show();
 | |
| 	} else {
 | |
| 		_minimize->hide();
 | |
| 	}
 | |
| 
 | |
| 	if (controlPresent(Control::Maximize)) {
 | |
| 		_maximizeRestore->show();
 | |
| 	} else {
 | |
| 		_maximizeRestore->hide();
 | |
| 	}
 | |
| 
 | |
| 	if (controlPresent(Control::Close)) {
 | |
| 		_close->show();
 | |
| 	} else {
 | |
| 		_close->hide();
 | |
| 	}
 | |
| 
 | |
| 	updateControlsPositionBySide(controlsLeft, false);
 | |
| 	updateControlsPositionBySide(controlsRight, true);
 | |
| }
 | |
| 
 | |
| void TitleControls::updateControlsPositionBySide(
 | |
| 		const std::vector<Control> &controls,
 | |
| 		bool right) {
 | |
| 	auto preparedControls = right
 | |
| 		? (ranges::views::reverse(controls) | ranges::to_vector)
 | |
| 		: controls;
 | |
| 
 | |
| 	RemoveDuplicates(preparedControls);
 | |
| 
 | |
| 	auto position = 0;
 | |
| 	for (const auto &control : preparedControls) {
 | |
| 		const auto widget = controlWidget(control);
 | |
| 		if (!widget) {
 | |
| 			continue;
 | |
| 		}
 | |
| 
 | |
| 		if (right) {
 | |
| 			widget->moveToRight(position, 0);
 | |
| 		} else {
 | |
| 			widget->moveToLeft(position, 0);
 | |
| 		}
 | |
| 
 | |
| 		position += widget->width();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void TitleControls::handleWindowStateChanged(Qt::WindowState state) {
 | |
| 	if (state == Qt::WindowMinimized) {
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	auto maximized = (state == Qt::WindowMaximized)
 | |
| 		|| (state == Qt::WindowFullScreen);
 | |
| 	if (_maximizedState != maximized) {
 | |
| 		_maximizedState = maximized;
 | |
| 		updateButtonsState();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void TitleControls::updateButtonsState() {
 | |
| 	const auto minimize = _activeState
 | |
| 		? &_st->minimizeIconActive
 | |
| 		: &_st->minimize.icon;
 | |
| 	const auto minimizeOver = _activeState
 | |
| 		? &_st->minimizeIconActiveOver
 | |
| 		: &_st->minimize.iconOver;
 | |
| 	_minimize->setIconOverride(minimize, minimizeOver);
 | |
| 	if (_maximizedState) {
 | |
| 		const auto restore = _activeState
 | |
| 			? &_st->restoreIconActive
 | |
| 			: &_st->restoreIcon;
 | |
| 		const auto restoreOver = _activeState
 | |
| 			? &_st->restoreIconActiveOver
 | |
| 			: &_st->restoreIconOver;
 | |
| 		_maximizeRestore->setIconOverride(restore, restoreOver);
 | |
| 	} else {
 | |
| 		const auto maximize = _activeState
 | |
| 			? &_st->maximizeIconActive
 | |
| 			: &_st->maximize.icon;
 | |
| 		const auto maximizeOver = _activeState
 | |
| 			? &_st->maximizeIconActiveOver
 | |
| 			: &_st->maximize.iconOver;
 | |
| 		_maximizeRestore->setIconOverride(maximize, maximizeOver);
 | |
| 	}
 | |
| 	const auto close = _activeState
 | |
| 		? &_st->closeIconActive
 | |
| 		: &_st->close.icon;
 | |
| 	const auto closeOver = _activeState
 | |
| 		? &_st->closeIconActiveOver
 | |
| 		: &_st->close.iconOver;
 | |
| 	_close->setIconOverride(close, closeOver);
 | |
| }
 | |
| 
 | |
| DefaultTitleWidget::DefaultTitleWidget(not_null<RpWidget*> parent)
 | |
| : RpWidget(parent)
 | |
| , _controls(this, st::defaultWindowTitle)
 | |
| , _shadow(this, st::titleShadow) {
 | |
| 	setAttribute(Qt::WA_OpaquePaintEvent);
 | |
| }
 | |
| 
 | |
| not_null<const style::WindowTitle*> DefaultTitleWidget::st() const {
 | |
| 	return _controls.st();
 | |
| }
 | |
| 
 | |
| QRect DefaultTitleWidget::controlsGeometry() const {
 | |
| 	return _controls.geometry();
 | |
| }
 | |
| 
 | |
| void DefaultTitleWidget::setText(const QString &text) {
 | |
| 	window()->setWindowTitle(text);
 | |
| }
 | |
| 
 | |
| void DefaultTitleWidget::setStyle(const style::WindowTitle &st) {
 | |
| 	_controls.setStyle(st);
 | |
| 	update();
 | |
| }
 | |
| 
 | |
| void DefaultTitleWidget::setResizeEnabled(bool enabled) {
 | |
| 	_controls.setResizeEnabled(enabled);
 | |
| }
 | |
| 
 | |
| void DefaultTitleWidget::paintEvent(QPaintEvent *e) {
 | |
| 	const auto active = window()->isActiveWindow();
 | |
| 	QPainter(this).fillRect(
 | |
| 		e->rect(),
 | |
| 		active ? _controls.st()->bgActive : _controls.st()->bg);
 | |
| }
 | |
| 
 | |
| void DefaultTitleWidget::resizeEvent(QResizeEvent *e) {
 | |
| 	_shadow->setGeometry(0, height() - st::lineWidth, width(), st::lineWidth);
 | |
| }
 | |
| 
 | |
| void DefaultTitleWidget::mousePressEvent(QMouseEvent *e) {
 | |
| 	if (e->button() == Qt::LeftButton) {
 | |
| 		_mousePressed = true;
 | |
| 	} else if (e->button() == Qt::RightButton) {
 | |
| 		ShowWindowMenu(window(), e->windowPos().toPoint());
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void DefaultTitleWidget::mouseReleaseEvent(QMouseEvent *e) {
 | |
| 	if (e->button() == Qt::LeftButton) {
 | |
| 		_mousePressed = false;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void DefaultTitleWidget::mouseMoveEvent(QMouseEvent *e) {
 | |
| 	if (_mousePressed) {
 | |
| 		window()->windowHandle()->startSystemMove();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void DefaultTitleWidget::mouseDoubleClickEvent(QMouseEvent *e) {
 | |
| 	const auto state = window()->windowState();
 | |
| 	if (state & Qt::WindowMaximized) {
 | |
| 		window()->setWindowState(state & ~Qt::WindowMaximized);
 | |
| 	} else {
 | |
| 		window()->setWindowState(state | Qt::WindowMaximized);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| SeparateTitleControls::SeparateTitleControls(
 | |
| 	QWidget *parent,
 | |
| 	const style::WindowTitle &st,
 | |
| 	Fn<void(bool maximized)> maximize)
 | |
| : wrap(parent)
 | |
| , controls(&wrap, st, std::move(maximize)) {
 | |
| }
 | |
| 
 | |
| std::unique_ptr<SeparateTitleControls> SetupSeparateTitleControls(
 | |
| 		not_null<RpWindow*> window,
 | |
| 		const style::WindowTitle &st,
 | |
| 		Fn<void(bool maximized)> maximize,
 | |
| 		rpl::producer<int> controlsTop) {
 | |
| 	auto result = std::make_unique<SeparateTitleControls>(
 | |
| 		window->body(),
 | |
| 		st,
 | |
| 		std::move(maximize));
 | |
| 
 | |
| 	const auto raw = result.get();
 | |
| 	auto &lifetime = raw->wrap.lifetime();
 | |
| 	rpl::combine(
 | |
| 		window->body()->widthValue(),
 | |
| 		window->additionalContentPaddingValue(),
 | |
| 		controlsTop ? std::move(controlsTop) : rpl::single(0)
 | |
| 	) | rpl::start_with_next([=](int width, int padding, int top) {
 | |
| 		raw->wrap.setGeometry(
 | |
| 			padding,
 | |
| 			top,
 | |
| 			width - 2 * padding,
 | |
| 			raw->controls.geometry().height());
 | |
| 	}, lifetime);
 | |
| 
 | |
| 	window->hitTestRequests(
 | |
| 	) | rpl::start_with_next([=](not_null<HitTestRequest*> request) {
 | |
| 		const auto origin = raw->wrap.pos();
 | |
| 		const auto relative = request->point - origin;
 | |
| 		const auto padding = window->additionalContentPadding();
 | |
| 		const auto controlsResult = raw->controls.hitTest(relative, padding);
 | |
| 		if (controlsResult != HitTestResult::None) {
 | |
| 			request->result = controlsResult;
 | |
| 		}
 | |
| 	}, lifetime);
 | |
| 
 | |
| 	SetupSemiNativeSystemButtons(&raw->controls, window, lifetime);
 | |
| 
 | |
| 	return result;
 | |
| }
 | |
| 
 | |
| } // namespace Platform
 | |
| } // namespace Ui
 | 
