// 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 #include #include namespace Ui { namespace Platform { namespace { template void RemoveDuplicates(std::vector &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 controls, not_null window, rpl::lifetime &lifetime, Fn 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); } object_ptr IconTitleButtons::create( not_null parent, TitleControl control, const style::WindowTitle &st) { const auto make = [&]( QPointer &my, const style::IconButton &st) { Expects(!my); auto result = object_ptr(parent, st); my = result.data(); return result; }; switch (control) { case TitleControl::Minimize: return make(_minimize, st.minimize); case TitleControl::Maximize: return make(_maximizeRestore, st.maximize); case TitleControl::Close: return make(_close, st.close); } Unexpected("Control in IconTitleButtons::create."); } void IconTitleButtons::updateState( bool active, bool maximized, const style::WindowTitle &st) { if (_minimize) { const auto minimize = active ? &st.minimizeIconActive : &st.minimize.icon; const auto minimizeOver = active ? &st.minimizeIconActiveOver : &st.minimize.iconOver; _minimize->setIconOverride(minimize, minimizeOver); } if (_maximizeRestore) { if (maximized) { const auto restore = active ? &st.restoreIconActive : &st.restoreIcon; const auto restoreOver = active ? &st.restoreIconActiveOver : &st.restoreIconOver; _maximizeRestore->setIconOverride(restore, restoreOver); } else { const auto maximize = active ? &st.maximizeIconActive : &st.maximize.icon; const auto maximizeOver = active ? &st.maximizeIconActiveOver : &st.maximize.iconOver; _maximizeRestore->setIconOverride(maximize, maximizeOver); } } if (_close) { const auto close = active ? &st.closeIconActive : &st.close.icon; const auto closeOver = active ? &st.closeIconActiveOver : &st.close.iconOver; _close->setIconOverride(close, closeOver); } } TitleControls::TitleControls( not_null parent, const style::WindowTitle &st, Fn maximize) : TitleControls( parent, st, std::make_unique(), std::move(maximize)) { } TitleControls::TitleControls( not_null parent, const style::WindowTitle &st, std::unique_ptr buttons, Fn maximize) : _st(&st) , _buttons(std::move(buttons)) , _minimize(_buttons->create(parent, Control::Minimize, st)) , _maximizeRestore(_buttons->create(parent, Control::Maximize, st)) , _close(_buttons->create(parent, Control::Close, st)) , _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 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 TitleControls::parent() const { return static_cast(_close->parentWidget()); } not_null TitleControls::window() const { return _close->window(); } void TitleControls::init(Fn 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(nullptr); *winIdEventFilter = base::install_event_filter( window(), [=](not_null 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, 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, HitTestResult buttonTestResult, Control control) { const auto over = (testResult == buttonTestResult); if (const auto raw = button.data()) { raw->setSynteticOver(over); } _buttons->notifySynteticOver(control, over); }; update(_minimize, HitTestResult::Minimize, Control::Minimize); update( _maximizeRestore, HitTestResult::MaximizeRestore, Control::Maximize); update(_close, HitTestResult::Close, Control::Close); } void TitleControls::buttonDown(HitTestResult testResult) { const auto update = [&]( const object_ptr &button, HitTestResult buttonTestResult) { if (const auto raw = button.data()) { raw->setSynteticDown(testResult == buttonTestResult); } }; update(_minimize, HitTestResult::Minimize); update(_maximizeRestore, HitTestResult::MaximizeRestore); update(_close, HitTestResult::Close); } AbstractButton *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 &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() { _buttons->updateState(_activeState, _maximizedState, *_st); } DefaultTitleWidget::DefaultTitleWidget(not_null parent) : RpWidget(parent) , _controls(this, st::defaultWindowTitle) , _shadow(this, st::titleShadow) { setAttribute(Qt::WA_OpaquePaintEvent); } not_null 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 maximize) : wrap(parent) , controls(&wrap, st, std::move(maximize)) { } SeparateTitleControls::SeparateTitleControls( QWidget *parent, const style::WindowTitle &st, std::unique_ptr buttons, Fn maximize) : wrap(parent) , controls(&wrap, st, std::move(buttons), std::move(maximize)) { } std::unique_ptr SetupSeparateTitleControls( not_null window, const style::WindowTitle &st, Fn maximize, rpl::producer controlsTop) { return SetupSeparateTitleControls( window, std::make_unique( window->body(), st, std::move(maximize)), std::move(controlsTop)); } std::unique_ptr SetupSeparateTitleControls( not_null window, std::unique_ptr created, rpl::producer controlsTop) { const auto raw = created.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 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 created; } } // namespace Platform } // namespace Ui