diff --git a/CMakeLists.txt b/CMakeLists.txt index 9860dbe..e1976e4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,12 +69,16 @@ PRIVATE ui/layers/layer_manager.h ui/layers/layer_widget.cpp ui/layers/layer_widget.h + ui/paint/arcs.cpp + ui/paint/arcs.h ui/paint/blob.cpp ui/paint/blob.h ui/paint/blobs.cpp ui/paint/blobs.h ui/paint/blobs_linear.cpp ui/paint/blobs_linear.h + ui/platform/linux/ui_linux_wayland_integration.cpp + ui/platform/linux/ui_linux_wayland_integration.h ui/platform/linux/ui_window_linux.cpp ui/platform/linux/ui_window_linux.h ui/platform/linux/ui_utility_linux.cpp @@ -93,8 +97,11 @@ PRIVATE ui/platform/win/ui_window_win.h ui/platform/win/ui_utility_win.cpp ui/platform/win/ui_utility_win.h + ui/platform/ui_platform_window_title.cpp + ui/platform/ui_platform_window_title.h ui/platform/ui_platform_window.cpp ui/platform/ui_platform_window.h + ui/platform/ui_platform_utility.cpp ui/platform/ui_platform_utility.h ui/style/style_core.cpp ui/style/style_core.h @@ -146,8 +153,18 @@ PRIVATE ui/widgets/input_fields.h ui/widgets/labels.cpp ui/widgets/labels.h - ui/widgets/menu.cpp - ui/widgets/menu.h + ui/widgets/menu/menu.cpp + ui/widgets/menu/menu.h + ui/widgets/menu/menu_action.cpp + ui/widgets/menu/menu_action.h + ui/widgets/menu/menu_common.cpp + ui/widgets/menu/menu_common.h + ui/widgets/menu/menu_item_base.cpp + ui/widgets/menu/menu_item_base.h + ui/widgets/menu/menu_separator.cpp + ui/widgets/menu/menu_separator.h + ui/widgets/menu/menu_toggle.cpp + ui/widgets/menu/menu_toggle.h ui/widgets/popup_menu.cpp ui/widgets/popup_menu.h ui/widgets/scroll_area.cpp @@ -216,6 +233,11 @@ if (NOT DESKTOP_APP_USE_PACKAGED_FONTS) nice_target_sources(lib_ui ${src_loc} PRIVATE fonts/fonts.qrc) endif() +if (DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION) + remove_target_sources(Telegram ${src_loc} ui/platform/linux/ui_linux_wayland_integration.cpp) + nice_target_sources(Telegram ${src_loc} PRIVATE ui/platform/linux/ui_linux_wayland_integration_dummy.cpp) +endif() + target_include_directories(lib_ui PUBLIC ${src_loc} diff --git a/icons/calls/call_shadow_left.png b/icons/calls/call_shadow_left.png new file mode 100644 index 0000000..74864ad Binary files /dev/null and b/icons/calls/call_shadow_left.png differ diff --git a/icons/calls/call_shadow_left@2x.png b/icons/calls/call_shadow_left@2x.png new file mode 100644 index 0000000..6a0e6e4 Binary files /dev/null and b/icons/calls/call_shadow_left@2x.png differ diff --git a/icons/calls/call_shadow_left@3x.png b/icons/calls/call_shadow_left@3x.png new file mode 100644 index 0000000..30257eb Binary files /dev/null and b/icons/calls/call_shadow_left@3x.png differ diff --git a/icons/calls/call_shadow_top.png b/icons/calls/call_shadow_top.png new file mode 100644 index 0000000..653e0af Binary files /dev/null and b/icons/calls/call_shadow_top.png differ diff --git a/icons/calls/call_shadow_top@2x.png b/icons/calls/call_shadow_top@2x.png new file mode 100644 index 0000000..47c672c Binary files /dev/null and b/icons/calls/call_shadow_top@2x.png differ diff --git a/icons/calls/call_shadow_top@3x.png b/icons/calls/call_shadow_top@3x.png new file mode 100644 index 0000000..350db04 Binary files /dev/null and b/icons/calls/call_shadow_top@3x.png differ diff --git a/icons/calls/call_shadow_top_left.png b/icons/calls/call_shadow_top_left.png new file mode 100644 index 0000000..baba493 Binary files /dev/null and b/icons/calls/call_shadow_top_left.png differ diff --git a/icons/calls/call_shadow_top_left@2x.png b/icons/calls/call_shadow_top_left@2x.png new file mode 100644 index 0000000..0d9672f Binary files /dev/null and b/icons/calls/call_shadow_top_left@2x.png differ diff --git a/icons/calls/call_shadow_top_left@3x.png b/icons/calls/call_shadow_top_left@3x.png new file mode 100644 index 0000000..260bb26 Binary files /dev/null and b/icons/calls/call_shadow_top_left@3x.png differ diff --git a/ui/colors.palette b/ui/colors.palette index eeb5d4c..fab8513 100644 --- a/ui/colors.palette +++ b/ui/colors.palette @@ -636,6 +636,8 @@ sideBarBadgeBg: #5eb5f7; // filters side bar badge background sideBarBadgeBgMuted: #8393a3; // filters side bar unimportant badge background sideBarBadgeFg: #ffffff; // filters side bar badge text +songCoverOverlayFg: #00000066; // song cover overlay + // kotatogram ktgTopBarBg: topBarBg; // Kotatogram: top bar background ktgTopBarNameFg: dialogsNameFg; // Kotatogram: top bar name text diff --git a/ui/effects/cross_line.cpp b/ui/effects/cross_line.cpp index 4c70455..9eb95b6 100644 --- a/ui/effects/cross_line.cpp +++ b/ui/effects/cross_line.cpp @@ -43,11 +43,14 @@ void CrossLineAnimation::paint( _st.icon.paint(p, left, top, _st.icon.width()); } } else if (progress == 1.) { - if (_completeCross.isNull()) { + auto &complete = colorOverride + ? _completeCrossOverride + : _completeCross; + if (complete.isNull()) { fillFrame(progress, colorOverride); - _completeCross = _frame; + complete = _frame; } - p.drawImage(left, top, _completeCross); + p.drawImage(left, top, complete); } else { fillFrame(progress, colorOverride); p.drawImage(left, top, _frame); @@ -94,6 +97,8 @@ void CrossLineAnimation::fillFrame( void CrossLineAnimation::invalidate() { _completeCross = QImage(); + _completeCrossOverride = QImage(); + _strokePen = QPen(_st.fg, _st.stroke, Qt::SolidLine, Qt::RoundCap); } } // namespace Ui diff --git a/ui/effects/cross_line.h b/ui/effects/cross_line.h index d1336c7..f8a27ea 100644 --- a/ui/effects/cross_line.h +++ b/ui/effects/cross_line.h @@ -39,10 +39,11 @@ private: const style::CrossLineAnimation &_st; const bool _reversed; const QPen _transparentPen; - const QPen _strokePen; + QPen _strokePen; QLineF _line; QImage _frame; QImage _completeCross; + QImage _completeCrossOverride; }; diff --git a/ui/paint/arcs.cpp b/ui/paint/arcs.cpp new file mode 100644 index 0000000..34a2c28 --- /dev/null +++ b/ui/paint/arcs.cpp @@ -0,0 +1,233 @@ +// 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/paint/arcs.h" + +#include "ui/effects/animation_value.h" +#include "ui/painter.h" + +namespace Ui::Paint { +namespace { + +inline float64 InterpolateF(float a, float b, float64 b_ratio) { + return a + float64(b - a) * b_ratio; +}; + +QRectF InterpolatedRect(const QRectF &r1, const QRectF &r2, float64 ratio) { + return QRectF( + InterpolateF(r1.x(), r2.x(), ratio), + InterpolateF(r1.y(), r2.y(), ratio), + InterpolateF(r1.width(), r2.width(), ratio), + InterpolateF(r1.height(), r2.height(), ratio)); +} + +} // namespace + +ArcsAnimation::ArcsAnimation( + const style::ArcsAnimation &st, + std::vector thresholds, + float64 startValue, + Direction direction) +: _st(st) +, _direction(direction) +, _startAngle(16 + * (st.deltaAngle + + ((direction == Direction::Up) + ? 90 + : (direction == Direction::Down) + ? 270 + : (direction == Direction::Left) + ? 180 + : 0))) +, _spanAngle(-st.deltaAngle * 2 * 16) +, _emptyRect(computeArcRect(0)) +, _currentValue(startValue) { + initArcs(std::move(thresholds)); +} + +void ArcsAnimation::initArcs(std::vector thresholds) { + const auto count = thresholds.size(); + _arcs.reserve(count); + + for (auto i = 0; i < count; i++) { + const auto threshold = thresholds[i]; + const auto progress = (threshold > _currentValue) ? 1. : 0.; + auto arc = Arc{ + .rect = computeArcRect(i + 1), + .threshold = threshold, + .progress = progress, + }; + _arcs.push_back(std::move(arc)); + } +} + +bool ArcsAnimation::isHorizontal() const { + return _direction == Direction::Left || _direction == Direction::Right; +} + +QRectF ArcsAnimation::computeArcRect(int index) const { + const auto w = _st.startWidth + _st.deltaWidth * index; + const auto h = _st.startHeight + _st.deltaHeight * index; + if (isHorizontal()) { + auto rect = QRectF(0, -h / 2.0, w, h); + if (_direction == Direction::Right) { + rect.moveRight(index * _st.space); + } else { + rect.moveLeft(-index * _st.space); + } + return rect; + } else { + auto rect = QRectF(-w / 2.0, 0, w, h); + if (_direction == Direction::Up) { + rect.moveTop(-index * _st.space); + } else { + rect.moveBottom(index * _st.space); + } + return rect; + } + return QRectF(); +} + +void ArcsAnimation::update(crl::time now) { + for (auto &arc : _arcs) { + if (!isArcFinished(arc)) { + const auto progress = std::clamp( + (now - arc.startTime) / float64(_st.duration), + 0., + 1.); + arc.progress = (arc.threshold > _currentValue) + ? progress + : (1. - progress); + } + } + if (isFinished()) { + _stopUpdateRequests.fire({}); + } +} + + +void ArcsAnimation::setValue(float64 value) { + if (_currentValue == value) { + return; + } + const auto previousValue = _currentValue; + _currentValue = value; + if (!isFinished()) { + const auto now = crl::now(); + _startUpdateRequests.fire({}); + for (auto &arc : _arcs) { + updateArcStartTime(arc, previousValue, now); + } + } +} + +void ArcsAnimation::updateArcStartTime( + Arc &arc, + float64 previousValue, + crl::time now) { + if ((arc.progress == 0.) || (arc.progress == 1.)) { + arc.startTime = isArcFinished(arc) ? 0 : now; + return; + } + const auto isPreviousToHide = (arc.threshold <= previousValue); // 0 -> 1 + const auto isCurrentToHide = (arc.threshold <= _currentValue); + if (isPreviousToHide != isCurrentToHide) { + const auto passedTime = _st.duration * arc.progress; + const auto newDelta = isCurrentToHide + ? (_st.duration - passedTime) + : passedTime; + arc.startTime = now - newDelta; + } +} + +float ArcsAnimation::width() const { + if (_arcs.empty()) { + return 0; + } + for (const auto &arc : ranges::view::reverse(_arcs)) { + if ((arc.progress != 1.)) { + return arc.rect.x() + arc.rect.width(); + } + } + return 0; +} + +float ArcsAnimation::finishedWidth() const { + if (_arcs.empty()) { + return 0; + } + for (const auto &arc : ranges::view::reverse(_arcs)) { + if (arc.threshold <= _currentValue) { + return arc.rect.x() + arc.rect.width(); + } + } + return 0; +} + +float ArcsAnimation::maxWidth() const { + if (_arcs.empty()) { + return 0; + } + const auto &r = _arcs.back().rect; + return r.x() + r.width(); +} + +float ArcsAnimation::height() const { + return _arcs.empty() + ? 0 + : _arcs.back().rect.height(); +} + +rpl::producer<> ArcsAnimation::startUpdateRequests() { + return _startUpdateRequests.events(); +} + +rpl::producer<> ArcsAnimation::stopUpdateRequests() { + return _stopUpdateRequests.events(); +} + +bool ArcsAnimation::isFinished() const { + return ranges::all_of( + _arcs, + [=](const Arc &arc) { return isArcFinished(arc); }); +} + +bool ArcsAnimation::isArcFinished(const Arc &arc) const { + return ((arc.threshold > _currentValue) && (arc.progress == 1.)) + || ((arc.threshold <= _currentValue) && (arc.progress == 0.)); +} + +void ArcsAnimation::paint(Painter &p, std::optional colorOverride) { + PainterHighQualityEnabler hq(p); + QPen pen; + if (_strokeRatio) { + pen.setWidthF(_st.stroke * _strokeRatio); + } else { + pen.setWidth(_st.stroke); + } + pen.setCapStyle(Qt::RoundCap); + pen.setColor(colorOverride ? (*colorOverride) : _st.fg->c); + p.setPen(pen); + for (auto i = 0; i < _arcs.size(); i++) { + const auto &arc = _arcs[i]; + const auto previousRect = (!i) ? _emptyRect : _arcs[i - 1].rect; + const auto progress = arc.progress; + const auto opactity = (1. - progress); + p.setOpacity(opactity * opactity); + const auto rect = (progress == 0.) + ? arc.rect + : (progress == 1.) + ? previousRect + : InterpolatedRect(arc.rect, previousRect, progress); + p.drawArc(rect, _startAngle, _spanAngle); + } +} + +void ArcsAnimation::setStrokeRatio(float ratio) { + _strokeRatio = ratio; +} + +} // namespace Ui::Paint diff --git a/ui/paint/arcs.h b/ui/paint/arcs.h new file mode 100644 index 0000000..022b6d6 --- /dev/null +++ b/ui/paint/arcs.h @@ -0,0 +1,85 @@ +// 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 +// +#pragma once + +#include "styles/style_widgets.h" + +class Painter; + +namespace Ui::Paint { + +class ArcsAnimation { +public: + + enum class Direction { + Up, + Down, + Left, + Right, + }; + + ArcsAnimation( + const style::ArcsAnimation &st, + std::vector thresholds, + float64 startValue, + Direction direction); + + void paint( + Painter &p, + std::optional colorOverride = std::nullopt); + + void setValue(float64 value); + + rpl::producer<> startUpdateRequests(); + rpl::producer<> stopUpdateRequests(); + + void update(crl::time now); + + bool isFinished() const; + + float width() const; + float maxWidth() const; + float finishedWidth() const; + float height() const; + + void setStrokeRatio(float ratio); + +private: + struct Arc { + QRectF rect; + float threshold; + crl::time startTime = 0; + float64 progress = 0.; + }; + + void initArcs(std::vector thresholds); + QRectF computeArcRect(int index) const; + bool isHorizontal() const; + + bool isArcFinished(const Arc &arc) const; + void updateArcStartTime( + Arc &arc, + float64 previousValue, + crl::time now); + + const style::ArcsAnimation &_st; + const Direction _direction; + const int _startAngle; + const int _spanAngle; + const QRectF _emptyRect; + + float64 _currentValue = 0.; + float _strokeRatio = 0.; + + rpl::event_stream<> _startUpdateRequests; + rpl::event_stream<> _stopUpdateRequests; + + std::vector _arcs; + +}; + +} // namespace Ui::Paint diff --git a/ui/paint/blob.cpp b/ui/paint/blob.cpp index b7f783e..22ccf19 100644 --- a/ui/paint/blob.cpp +++ b/ui/paint/blob.cpp @@ -52,11 +52,12 @@ void Blob::generateSingleValues(int i) { + kSegmentSpeedDiff * std::abs(RandomAdditional()); } -void Blob::update(float level, float speedScale) { +void Blob::update(float level, float speedScale, float64 rate) { for (auto i = 0; i < _segmentsCount; i++) { auto &segment = segmentAt(i); - segment.progress += (segment.speed * _minSpeed) - + level * segment.speed * _maxSpeed * speedScale; + segment.progress += (_minSpeed + level * _maxSpeed * speedScale) + * segment.speed + * rate; if (segment.progress >= 1) { generateSingleValues(i); generateTwoValues(i); @@ -152,9 +153,9 @@ void RadialBlob::generateTwoValues(int i) { radius.setNext(_radiuses.min + std::abs(RandomAdditional()) * radDiff); } -void RadialBlob::update(float level, float speedScale) { +void RadialBlob::update(float level, float speedScale, float64 rate) { _scale = level; - Blob::update(level, speedScale); + Blob::update(level, speedScale, rate); } Blob::Segment &RadialBlob::segmentAt(int i) { diff --git a/ui/paint/blob.h b/ui/paint/blob.h index e48e621..805914b 100644 --- a/ui/paint/blob.h +++ b/ui/paint/blob.h @@ -20,7 +20,7 @@ public: Blob(int n, float minSpeed = 0, float maxSpeed = 0); virtual ~Blob() = default; - void update(float level, float speedScale); + void update(float level, float speedScale, float64 rate); void generateBlob(); void setRadiuses(Radiuses values); @@ -59,7 +59,7 @@ public: RadialBlob(int n, float minScale, float minSpeed = 0, float maxSpeed = 0); void paint(Painter &p, const QBrush &brush, float outerScale = 1.); - void update(float level, float speedScale); + void update(float level, float speedScale, float64 rate); private: struct Segment : Blob::Segment { diff --git a/ui/paint/blobs.cpp b/ui/paint/blobs.cpp index 6bce7f6..903f5d9 100644 --- a/ui/paint/blobs.cpp +++ b/ui/paint/blobs.cpp @@ -10,6 +10,13 @@ namespace Ui::Paint { +namespace { + +constexpr auto kRateLimitF = 1000. / 60.; +constexpr auto kRateLimit = int(kRateLimitF + 0.5); // Round. + +} // namespace + Blobs::Blobs( std::vector blobDatas, float levelDuration, @@ -76,7 +83,6 @@ void Blobs::resetLevel() { void Blobs::paint(Painter &p, const QBrush &brush, float outerScale) { const auto opacity = p.opacity(); for (auto i = 0; i < _blobs.size(); i++) { - _blobs[i].update(_levelValue.current(), _blobDatas[i].speedScale); const auto alpha = _blobDatas[i].alpha; if (alpha != 1.) { p.setOpacity(opacity * alpha); @@ -89,7 +95,15 @@ void Blobs::paint(Painter &p, const QBrush &brush, float outerScale) { } void Blobs::updateLevel(crl::time dt) { - _levelValue.update((dt > 20) ? 17 : dt); + const auto limitedDt = (dt > 20) ? kRateLimit : dt; + _levelValue.update(limitedDt); + + for (auto i = 0; i < _blobs.size(); i++) { + _blobs[i].update( + _levelValue.current(), + _blobDatas[i].speedScale, + limitedDt / kRateLimitF); + } } float64 Blobs::currentLevel() const { diff --git a/ui/paint/blobs_linear.cpp b/ui/paint/blobs_linear.cpp index 37d92ec..1f46e1f 100644 --- a/ui/paint/blobs_linear.cpp +++ b/ui/paint/blobs_linear.cpp @@ -10,6 +10,13 @@ namespace Ui::Paint { +namespace { + +constexpr auto kRateLimitF = 1000. / 60.; +constexpr auto kRateLimit = int(kRateLimitF + 0.5); // Round. + +} // namespace + LinearBlobs::LinearBlobs( std::vector blobDatas, float levelDuration, @@ -75,7 +82,6 @@ void LinearBlobs::paint(Painter &p, const QBrush &brush, int width) { PainterHighQualityEnabler hq(p); const auto opacity = p.opacity(); for (auto i = 0; i < _blobs.size(); i++) { - _blobs[i].update(_levelValue.current(), _blobDatas[i].speedScale); const auto alpha = _blobDatas[i].alpha; if (alpha != 1.) { p.setOpacity(opacity * alpha); @@ -88,8 +94,8 @@ void LinearBlobs::paint(Painter &p, const QBrush &brush, int width) { } void LinearBlobs::updateLevel(crl::time dt) { - const auto d = (dt > 20) ? 17 : dt; - _levelValue.update(d); + const auto limitedDt = (dt > 20) ? kRateLimit : dt; + _levelValue.update(limitedDt); const auto level = (float)currentLevel(); for (auto i = 0; i < _blobs.size(); i++) { @@ -97,6 +103,10 @@ void LinearBlobs::updateLevel(crl::time dt) { _blobs[i].setRadiuses({ data.minRadius, data.idleRadius + (data.maxRadius - data.idleRadius) * level }); + _blobs[i].update( + _levelValue.current(), + data.speedScale, + limitedDt / kRateLimitF); } } diff --git a/ui/platform/linux/ui_linux_wayland_integration.cpp b/ui/platform/linux/ui_linux_wayland_integration.cpp new file mode 100644 index 0000000..7cb7dd1 --- /dev/null +++ b/ui/platform/linux/ui_linux_wayland_integration.cpp @@ -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 "ui/platform/linux/ui_linux_wayland_integration.h" + +#include "base/platform/base_platform_info.h" + +#include + +#include +#include +#include + +using QtWaylandClient::QWaylandWindow; + +namespace Ui { +namespace Platform { + +WaylandIntegration::WaylandIntegration() { +} + +WaylandIntegration *WaylandIntegration::Instance() { + if (!::Platform::IsWayland()) return nullptr; + static WaylandIntegration instance; + return &instance; +} + +bool WaylandIntegration::showWindowMenu(QWindow *window) { + if (const auto waylandWindow = static_cast( + window->handle())) { + if (const auto seat = waylandWindow->display()->lastInputDevice()) { + if (const auto shellSurface = waylandWindow->shellSurface()) { + return shellSurface->showWindowMenu(seat); + } + } + } + + return false; +} + +} // namespace Platform +} // namespace Ui diff --git a/ui/platform/linux/ui_linux_wayland_integration.h b/ui/platform/linux/ui_linux_wayland_integration.h new file mode 100644 index 0000000..bbc2331 --- /dev/null +++ b/ui/platform/linux/ui_linux_wayland_integration.h @@ -0,0 +1,25 @@ +/* +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 + +class QWindow; + +namespace Ui { +namespace Platform { + +class WaylandIntegration { +public: + static WaylandIntegration *Instance(); + bool showWindowMenu(QWindow *window); + +private: + WaylandIntegration(); +}; + +} // namespace Platform +} // namespace Ui diff --git a/ui/platform/linux/ui_linux_wayland_integration_dummy.cpp b/ui/platform/linux/ui_linux_wayland_integration_dummy.cpp new file mode 100644 index 0000000..11abd62 --- /dev/null +++ b/ui/platform/linux/ui_linux_wayland_integration_dummy.cpp @@ -0,0 +1,29 @@ +/* +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 "ui/platform/linux/ui_linux_wayland_integration.h" + +#include "base/platform/base_platform_info.h" + +namespace Ui { +namespace Platform { + +WaylandIntegration::WaylandIntegration() { +} + +WaylandIntegration *WaylandIntegration::Instance() { + if (!::Platform::IsWayland()) return nullptr; + static WaylandIntegration instance; + return &instance; +} + +bool WaylandIntegration::showWindowMenu(QWindow *window) { + return false; +} + +} // namespace Platform +} // namespace Ui diff --git a/ui/platform/linux/ui_utility_linux.cpp b/ui/platform/linux/ui_utility_linux.cpp index 01566c5..933ea21 100644 --- a/ui/platform/linux/ui_utility_linux.cpp +++ b/ui/platform/linux/ui_utility_linux.cpp @@ -8,16 +8,150 @@ #include "ui/ui_log.h" #include "base/platform/base_platform_info.h" +#include "base/platform/linux/base_linux_gtk_integration.h" +#include "ui/platform/linux/ui_linux_wayland_integration.h" +#include "base/const_string.h" #include "base/qt_adapters.h" #include "base/flat_set.h" +#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION +#include "base/platform/linux/base_linux_xcb_utilities.h" +#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION + #include #include +#include #include #include +Q_DECLARE_METATYPE(QMargins); + namespace Ui { namespace Platform { +namespace { + +constexpr auto kXCBFrameExtentsAtomName = "_GTK_FRAME_EXTENTS"_cs; + +constexpr auto kXDGDesktopPortalService = "org.freedesktop.portal.Desktop"_cs; +constexpr auto kXDGDesktopPortalObjectPath = "/org/freedesktop/portal/desktop"_cs; +constexpr auto kSettingsPortalInterface = "org.freedesktop.portal.Settings"_cs; + +#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION +bool SetXCBFrameExtents(QWindow *window, const QMargins &extents) { + const auto connection = base::Platform::XCB::GetConnectionFromQt(); + if (!connection) { + return false; + } + + const auto frameExtentsAtom = base::Platform::XCB::GetAtom( + connection, + kXCBFrameExtentsAtomName.utf16()); + + if (!frameExtentsAtom.has_value()) { + return false; + } + + const auto extentsVector = std::vector{ + uint(extents.left()), + uint(extents.right()), + uint(extents.top()), + uint(extents.bottom()), + }; + + xcb_change_property( + connection, + XCB_PROP_MODE_REPLACE, + window->winId(), + *frameExtentsAtom, + XCB_ATOM_CARDINAL, + 32, + extentsVector.size(), + extentsVector.data()); + + return true; +} + +bool UnsetXCBFrameExtents(QWindow *window) { + const auto connection = base::Platform::XCB::GetConnectionFromQt(); + if (!connection) { + return false; + } + + const auto frameExtentsAtom = base::Platform::XCB::GetAtom( + connection, + kXCBFrameExtentsAtomName.utf16()); + + if (!frameExtentsAtom.has_value()) { + return false; + } + + xcb_delete_property( + connection, + window->winId(), + *frameExtentsAtom); + + return true; +} + +bool ShowXCBWindowMenu(QWindow *window) { + const auto connection = base::Platform::XCB::GetConnectionFromQt(); + if (!connection) { + return false; + } + + const auto root = base::Platform::XCB::GetRootWindowFromQt(); + if (!root.has_value()) { + return false; + } + + const auto showWindowMenuAtom = base::Platform::XCB::GetAtom( + connection, + "_GTK_SHOW_WINDOW_MENU"); + + if (!showWindowMenuAtom.has_value()) { + return false; + } + + const auto globalPos = QCursor::pos(); + + xcb_client_message_event_t xev; + xev.response_type = XCB_CLIENT_MESSAGE; + xev.type = *showWindowMenuAtom; + xev.sequence = 0; + xev.window = window->winId(); + xev.format = 32; + xev.data.data32[0] = 0; + xev.data.data32[1] = globalPos.x(); + xev.data.data32[2] = globalPos.y(); + xev.data.data32[3] = 0; + xev.data.data32[4] = 0; + + xcb_ungrab_pointer(connection, XCB_CURRENT_TIME); + xcb_send_event( + connection, + false, + *root, + XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT + | XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY, + reinterpret_cast(&xev)); + + return true; +} +#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION + +TitleControls::Control GtkKeywordToTitleControl(const QString &keyword) { + if (keyword == qstr("minimize")) { + return TitleControls::Control::Minimize; + } else if (keyword == qstr("maximize")) { + return TitleControls::Control::Maximize; + } else if (keyword == qstr("close")) { + return TitleControls::Control::Close; + } + + return TitleControls::Control::Unknown; +} + +} // namespace bool IsApplicationActive() { return QApplication::activeWindow() != nullptr; @@ -50,5 +184,130 @@ bool TranslucentWindowsSupported(QPoint globalPosition) { void IgnoreAllActivation(not_null widget) { } +bool WindowExtentsSupported() { +#ifdef DESKTOP_APP_QT_PATCHED + if (::Platform::IsWayland()) { + return true; + } +#endif // DESKTOP_APP_QT_PATCHED + +#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION + namespace XCB = base::Platform::XCB; + if (!::Platform::IsWayland() + && XCB::IsSupportedByWM(kXCBFrameExtentsAtomName.utf16())) { + return true; + } +#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION + + return false; +} + +bool SetWindowExtents(QWindow *window, const QMargins &extents) { + if (::Platform::IsWayland()) { +#ifdef DESKTOP_APP_QT_PATCHED + window->setProperty("WaylandCustomMargins", QVariant::fromValue(extents)); + return true; +#else // DESKTOP_APP_QT_PATCHED + return false; +#endif // !DESKTOP_APP_QT_PATCHED + } else { +#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION + return SetXCBFrameExtents(window, extents); +#else // !DESKTOP_APP_DISABLE_X11_INTEGRATION + return false; +#endif // DESKTOP_APP_DISABLE_X11_INTEGRATION + } +} + +bool UnsetWindowExtents(QWindow *window) { + if (::Platform::IsWayland()) { +#ifdef DESKTOP_APP_QT_PATCHED + window->setProperty("WaylandCustomMargins", QVariant()); + return true; +#else // DESKTOP_APP_QT_PATCHED + return false; +#endif // !DESKTOP_APP_QT_PATCHED + } else { +#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION + return UnsetXCBFrameExtents(window); +#else // !DESKTOP_APP_DISABLE_X11_INTEGRATION + return false; +#endif // DESKTOP_APP_DISABLE_X11_INTEGRATION + } +} + +bool ShowWindowMenu(QWindow *window) { + if (const auto integration = WaylandIntegration::Instance()) { + return integration->showWindowMenu(window); + } else { +#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION + return ShowXCBWindowMenu(window); +#else // !DESKTOP_APP_DISABLE_X11_INTEGRATION + return false; +#endif // DESKTOP_APP_DISABLE_X11_INTEGRATION + } +} + +TitleControls::Layout TitleControlsLayout() { + const auto gtkResult = []() -> std::optional { + const auto integration = base::Platform::GtkIntegration::Instance(); + if (!integration || !integration->checkVersion(3, 12, 0)) { + return std::nullopt; + } + + const auto decorationLayoutSetting = integration->getStringSetting( + "gtk-decoration-layout"); + + if (!decorationLayoutSetting.has_value()) { + return std::nullopt; + } + + const auto decorationLayout = decorationLayoutSetting->split(':'); + + std::vector controlsLeft; + ranges::transform( + decorationLayout[0].split(','), + ranges::back_inserter(controlsLeft), + GtkKeywordToTitleControl); + + std::vector controlsRight; + if (decorationLayout.size() > 1) { + ranges::transform( + decorationLayout[1].split(','), + ranges::back_inserter(controlsRight), + GtkKeywordToTitleControl); + } + + return TitleControls::Layout{ + .left = controlsLeft, + .right = controlsRight + }; + }(); + + if (gtkResult.has_value()) { + return *gtkResult; + } + +#ifdef __HAIKU__ + return TitleControls::Layout{ + .left = { + TitleControls::Control::Close, + }, + .right = { + TitleControls::Control::Minimize, + TitleControls::Control::Maximize, + } + }; +#else // __HAIKU__ + return TitleControls::Layout{ + .right = { + TitleControls::Control::Minimize, + TitleControls::Control::Maximize, + TitleControls::Control::Close, + } + }; +#endif // !__HAIKU__ +} + } // namespace Platform } // namespace Ui diff --git a/ui/platform/linux/ui_utility_linux.h b/ui/platform/linux/ui_utility_linux.h index c9081fc..c81a814 100644 --- a/ui/platform/linux/ui_utility_linux.h +++ b/ui/platform/linux/ui_utility_linux.h @@ -6,6 +6,8 @@ // #pragma once +#include "ui/platform/ui_platform_utility.h" + class QPainter; class QPaintEvent; diff --git a/ui/platform/mac/ui_utility_mac.h b/ui/platform/mac/ui_utility_mac.h index 7d4bffe..97f3c11 100644 --- a/ui/platform/mac/ui_utility_mac.h +++ b/ui/platform/mac/ui_utility_mac.h @@ -6,6 +6,7 @@ // #pragma once +#include "ui/platform/ui_platform_utility.h" #include "base/platform/base_platform_info.h" #include @@ -23,5 +24,21 @@ inline constexpr bool UseMainQueueGeneric() { return ::Platform::IsMacStoreBuild(); } +inline bool WindowExtentsSupported() { + return false; +} + +inline bool SetWindowExtents(QWindow *window, const QMargins &extents) { + return false; +} + +inline bool UnsetWindowExtents(QWindow *window) { + return false; +} + +inline bool ShowWindowMenu(QWindow *window) { + return false; +} + } // namespace Platform } // namespace Ui diff --git a/ui/platform/mac/ui_utility_mac.mm b/ui/platform/mac/ui_utility_mac.mm index 46aeb11..fd8b492 100644 --- a/ui/platform/mac/ui_utility_mac.mm +++ b/ui/platform/mac/ui_utility_mac.mm @@ -99,5 +99,15 @@ void DrainMainQueue() { void IgnoreAllActivation(not_null widget) { } +TitleControls::Layout TitleControlsLayout() { + return TitleControls::Layout{ + .left = { + TitleControls::Control::Close, + TitleControls::Control::Minimize, + TitleControls::Control::Maximize, + } + }; +} + } // namespace Platform } // namespace Ui diff --git a/ui/platform/ui_platform_utility.cpp b/ui/platform/ui_platform_utility.cpp new file mode 100644 index 0000000..2d07ec5 --- /dev/null +++ b/ui/platform/ui_platform_utility.cpp @@ -0,0 +1,26 @@ +// 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_utility.h" + +namespace Ui { +namespace Platform { +namespace { + +rpl::event_stream<> TitleControlsLayoutChanges; + +} // namespace + +rpl::producer<> TitleControlsLayoutChanged() { + return TitleControlsLayoutChanges.events(); +} + +void NotifyTitleControlsLayoutChanged() { + TitleControlsLayoutChanges.fire({}); +} + +} // namespace Platform +} // namespace Ui diff --git a/ui/platform/ui_platform_utility.h b/ui/platform/ui_platform_utility.h index a8eaedd..40dcdff 100644 --- a/ui/platform/ui_platform_utility.h +++ b/ui/platform/ui_platform_utility.h @@ -6,6 +6,8 @@ // #pragma once +#include "ui/platform/ui_platform_window_title.h" + class QPoint; class QPainter; class QPaintEvent; @@ -30,6 +32,15 @@ void IgnoreAllActivation(not_null widget); [[nodiscard]] constexpr bool UseMainQueueGeneric(); void DrainMainQueue(); // Needed only if UseMainQueueGeneric() is false. +[[nodiscard]] bool WindowExtentsSupported(); +bool SetWindowExtents(QWindow *window, const QMargins &extents); +bool UnsetWindowExtents(QWindow *window); +bool ShowWindowMenu(QWindow *window); + +[[nodiscard]] TitleControls::Layout TitleControlsLayout(); +[[nodiscard]] rpl::producer<> TitleControlsLayoutChanged(); +void NotifyTitleControlsLayoutChanged(); + } // namespace Platform } // namespace Ui diff --git a/ui/platform/ui_platform_window.cpp b/ui/platform/ui_platform_window.cpp index c1b88eb..38921e2 100644 --- a/ui/platform/ui_platform_window.cpp +++ b/ui/platform/ui_platform_window.cpp @@ -6,21 +6,31 @@ // #include "ui/platform/ui_platform_window.h" +#include "ui/platform/ui_platform_window_title.h" +#include "ui/platform/ui_platform_utility.h" #include "ui/widgets/window.h" +#include "ui/widgets/shadow.h" +#include "ui/painter.h" +#include "styles/style_widgets.h" +#include "styles/style_layers.h" +#include #include #include namespace Ui { namespace Platform { +namespace { + +[[nodiscard]] const style::Shadow &Shadow() { + return st::callShadow; +} + +} // namespace BasicWindowHelper::BasicWindowHelper(not_null window) : _window(window) { -#if (QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)) _window->setWindowFlag(Qt::Window); -#else // Qt >= 5.9 - _window->setWindowFlags(_window->windowFlags() | Qt::Window); -#endif // Qt >= 5.9 } not_null BasicWindowHelper::body() { @@ -100,8 +110,6 @@ void BasicWindowHelper::setupBodyTitleAreaEvents() { && (static_cast(e.get())->button() == Qt::LeftButton)) { _mousePressed = true; - -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) || defined DESKTOP_APP_QT_PATCHED } else if (e->type() == QEvent::MouseMove) { const auto mouseEvent = static_cast(e.get()); if (_mousePressed @@ -109,7 +117,6 @@ void BasicWindowHelper::setupBodyTitleAreaEvents() { && !_window->isFullScreen() #endif // !Q_OS_WIN && (hitTest() & WindowTitleHitTestFlag::Move)) { - #ifdef Q_OS_WIN if (_window->isFullScreen()) { // On Windows we just jump out of fullscreen @@ -121,10 +128,270 @@ void BasicWindowHelper::setupBodyTitleAreaEvents() { _mousePressed = false; _window->windowHandle()->startSystemMove(); } -#endif // Qt >= 5.15 || DESKTOP_APP_QT_PATCHED } }, body()->lifetime()); } +DefaultWindowHelper::DefaultWindowHelper(not_null window) +: BasicWindowHelper(window) +, _title(Ui::CreateChild(window.get())) +, _body(Ui::CreateChild(window.get())) { + init(); +} + +void DefaultWindowHelper::init() { + window()->setWindowFlag(Qt::FramelessWindowHint); + + if (WindowExtentsSupported()) { + window()->setAttribute(Qt::WA_TranslucentBackground); + } + + window()->widthValue( + ) | rpl::start_with_next([=](int width) { + const auto area = resizeArea(); + _title->setGeometry( + area.left(), + area.top(), + width - area.left() - area.right(), + _title->st()->height); + }, _title->lifetime()); + + rpl::combine( + window()->sizeValue(), + _title->heightValue() + ) | rpl::start_with_next([=](QSize size, int titleHeight) { + const auto area = resizeArea(); + + const auto sizeWithoutMargins = size + .shrunkBy({ 0, titleHeight, 0, 0 }) + .shrunkBy(area); + + const auto topLeft = QPoint( + area.left(), + area.top() + titleHeight); + + _body->setGeometry(QRect(topLeft, sizeWithoutMargins)); + }, _body->lifetime()); + + window()->paintRequest( + ) | rpl::start_with_next([=] { + const auto area = resizeArea(); + + if (area.isNull()) { + return; + } + + Painter p(window()); + + if (hasShadow()) { + Ui::Shadow::paint( + p, + QRect(QPoint(), window()->size()).marginsRemoved(area), + window()->width(), + Shadow()); + } else { + paintBorders(p); + } + }, window()->lifetime()); + + window()->shownValue( + ) | rpl::start_with_next([=](bool shown) { + if (shown) { + updateWindowExtents(); + } + }, window()->lifetime()); + + window()->events() | rpl::start_with_next([=](not_null e) { + if (e->type() == QEvent::MouseButtonPress) { + const auto mouseEvent = static_cast(e.get()); + const auto currentPoint = mouseEvent->windowPos().toPoint(); + const auto edges = edgesFromPos(currentPoint); + + if (mouseEvent->button() == Qt::LeftButton && edges) { + window()->windowHandle()->startSystemResize(edges); + } + } else if (e->type() == QEvent::Move + || e->type() == QEvent::Resize + || e->type() == QEvent::WindowStateChange) { + updateWindowExtents(); + } + }, window()->lifetime()); + + QCoreApplication::instance()->installEventFilter(this); +} + +not_null DefaultWindowHelper::body() { + return _body; +} + +bool DefaultWindowHelper::hasShadow() const { + const auto center = window()->geometry().center(); + return WindowExtentsSupported() && TranslucentWindowsSupported(center); +} + +QMargins DefaultWindowHelper::resizeArea() const { + if (window()->isMaximized() || window()->isFullScreen()) { + return QMargins(); + } + + return Shadow().extend; +} + +Qt::Edges DefaultWindowHelper::edgesFromPos(const QPoint &pos) const { + const auto area = resizeArea(); + + if (area.isNull()) { + return Qt::Edges(); + } else if (pos.x() <= area.left()) { + if (pos.y() <= area.top()) { + return Qt::LeftEdge | Qt::TopEdge; + } else if (pos.y() >= (window()->height() - area.bottom())) { + return Qt::LeftEdge | Qt::BottomEdge; + } + + return Qt::LeftEdge; + } else if (pos.x() >= (window()->width() - area.right())) { + if (pos.y() <= area.top()) { + return Qt::RightEdge | Qt::TopEdge; + } else if (pos.y() >= (window()->height() - area.bottom())) { + return Qt::RightEdge | Qt::BottomEdge; + } + + return Qt::RightEdge; + } else if (pos.y() <= area.top()) { + return Qt::TopEdge; + } else if (pos.y() >= (window()->height() - area.bottom())) { + return Qt::BottomEdge; + } + + return Qt::Edges(); +} + +bool DefaultWindowHelper::eventFilter(QObject *obj, QEvent *e) { + // doesn't work with RpWidget::events() for some reason + if (e->type() == QEvent::MouseMove + && obj->isWidgetType() + && static_cast(window()) == static_cast(obj)) { + const auto mouseEvent = static_cast(e); + const auto currentPoint = mouseEvent->windowPos().toPoint(); + const auto edges = edgesFromPos(currentPoint); + + if (mouseEvent->buttons() == Qt::NoButton) { + updateCursor(edges); + } + } + + return QObject::eventFilter(obj, e); +} + +void DefaultWindowHelper::setTitle(const QString &title) { + _title->setText(title); + window()->setWindowTitle(title); +} + +void DefaultWindowHelper::setTitleStyle(const style::WindowTitle &st) { + const auto area = resizeArea(); + _title->setStyle(st); + _title->setGeometry( + area.left(), + area.top(), + window()->width() - area.left() - area.right(), + _title->st()->height); +} + +void DefaultWindowHelper::setMinimumSize(QSize size) { + const auto sizeWithMargins = size + .grownBy({ 0, _title->height(), 0, 0 }) + .grownBy(resizeArea()); + window()->setMinimumSize(sizeWithMargins); +} + +void DefaultWindowHelper::setFixedSize(QSize size) { + const auto sizeWithMargins = size + .grownBy({ 0, _title->height(), 0, 0 }) + .grownBy(resizeArea()); + window()->setFixedSize(sizeWithMargins); + _title->setResizeEnabled(false); +} + +void DefaultWindowHelper::setGeometry(QRect rect) { + window()->setGeometry(rect + .marginsAdded({ 0, _title->height(), 0, 0 }) + .marginsAdded(resizeArea())); +} + +void DefaultWindowHelper::paintBorders(QPainter &p) { + const auto titleBackground = window()->isActiveWindow() + ? _title->st()->bgActive + : _title->st()->bg; + + const auto defaultTitleBackground = window()->isActiveWindow() + ? st::defaultWindowTitle.bgActive + : st::defaultWindowTitle.bg; + + const auto borderColor = QBrush(titleBackground).isOpaque() + ? titleBackground + : defaultTitleBackground; + + const auto area = resizeArea(); + + p.fillRect( + 0, + area.top(), + area.left(), + window()->height() - area.top() - area.bottom(), + borderColor); + + p.fillRect( + window()->width() - area.right(), + area.top(), + area.right(), + window()->height() - area.top() - area.bottom(), + borderColor); + + p.fillRect( + 0, + 0, + window()->width(), + area.top(), + borderColor); + + p.fillRect( + 0, + window()->height() - area.bottom(), + window()->width(), + area.bottom(), + borderColor); +} + +void DefaultWindowHelper::updateWindowExtents() { + if (hasShadow()) { + Platform::SetWindowExtents( + window()->windowHandle(), + resizeArea()); + + _extentsSet = true; + } else if (_extentsSet) { + Platform::UnsetWindowExtents(window()->windowHandle()); + _extentsSet = false; + } +} + +void DefaultWindowHelper::updateCursor(Qt::Edges edges) { + if (((edges & Qt::LeftEdge) && (edges & Qt::TopEdge)) + || ((edges & Qt::RightEdge) && (edges & Qt::BottomEdge))) { + window()->setCursor(QCursor(Qt::SizeFDiagCursor)); + } else if (((edges & Qt::LeftEdge) && (edges & Qt::BottomEdge)) + || ((edges & Qt::RightEdge) && (edges & Qt::TopEdge))) { + window()->setCursor(QCursor(Qt::SizeBDiagCursor)); + } else if ((edges & Qt::LeftEdge) || (edges & Qt::RightEdge)) { + window()->setCursor(QCursor(Qt::SizeHorCursor)); + } else if ((edges & Qt::TopEdge) || (edges & Qt::BottomEdge)) { + window()->setCursor(QCursor(Qt::SizeVerCursor)); + } else { + window()->unsetCursor(); + } +} + } // namespace Platform } // namespace Ui diff --git a/ui/platform/ui_platform_window.h b/ui/platform/ui_platform_window.h index 0844545..46e4e9c 100644 --- a/ui/platform/ui_platform_window.h +++ b/ui/platform/ui_platform_window.h @@ -20,6 +20,8 @@ using WindowTitleHitTestFlags = base::flags; namespace Platform { +class DefaultTitleWidget; + class BasicWindowHelper { public: explicit BasicWindowHelper(not_null window); @@ -57,6 +59,35 @@ private: }; +class DefaultWindowHelper final : public QObject, public BasicWindowHelper { +public: + explicit DefaultWindowHelper(not_null window); + + not_null body() override; + void setTitle(const QString &title) override; + void setTitleStyle(const style::WindowTitle &st) override; + void setMinimumSize(QSize size) override; + void setFixedSize(QSize size) override; + void setGeometry(QRect rect) override; + +protected: + bool eventFilter(QObject *obj, QEvent *e) override; + +private: + void init(); + [[nodiscard]] bool hasShadow() const; + [[nodiscard]] QMargins resizeArea() const; + [[nodiscard]] Qt::Edges edgesFromPos(const QPoint &pos) const; + void paintBorders(QPainter &p); + void updateWindowExtents(); + void updateCursor(Qt::Edges edges); + + const not_null _title; + const not_null _body; + bool _extentsSet = false; + +}; + [[nodiscard]] std::unique_ptr CreateSpecialWindowHelper( not_null window); @@ -65,7 +96,7 @@ private: if (auto special = CreateSpecialWindowHelper(window)) { return special; } - return std::make_unique(window); + return std::make_unique(window); } } // namespace Platform diff --git a/ui/platform/ui_platform_window_title.cpp b/ui/platform/ui_platform_window_title.cpp new file mode 100644 index 0000000..1079cfc --- /dev/null +++ b/ui/platform/ui_platform_window_title.cpp @@ -0,0 +1,353 @@ +// 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 "styles/style_widgets.h" +#include "styles/palette.h" +#include "base/algorithm.h" +#include "base/event_filter.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 + +TitleControls::TitleControls( + not_null parent, + const style::WindowTitle &st, + Fn 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 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([=] { + window()->setWindowState( + window()->windowState() | Qt::WindowMinimized); + _minimize->clearState(); + }); + _minimize->setPointerCursor(false); + _maximizeRestore->setClickedCallback([=] { + if (maximize) { + maximize(!_maximizedState); + } else { + window()->setWindowState(_maximizedState + ? Qt::WindowNoState + : Qt::WindowMaximized); + } + _maximizeRestore->clearState(); + }); + _maximizeRestore->setPointerCursor(false); + _close->setClickedCallback([=] { + window()->close(); + _close->clearState(); + }); + _close->setPointerCursor(false); + + parent()->widthValue( + ) | rpl::start_with_next([=](int width) { + updateControlsPosition(); + }, _close->lifetime()); + + TitleControlsLayoutChanged( + ) | rpl::start_with_next([=] { + updateControlsPosition(); + }, _close->lifetime()); + + 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; + } + + QObject::connect( + window()->windowHandle(), + &QWindow::windowStateChanged, + [=](Qt::WindowState state) { + handleWindowStateChanged(state); + }); + + base::take(*winIdEventFilter)->deleteLater(); + return base::EventFilterResult::Continue; + }); + + _activeState = parent()->isActiveWindow(); + updateButtonsState(); +} + +void TitleControls::setResizeEnabled(bool enabled) { + _resizeEnabled = enabled; + updateControlsPosition(); +} + +void TitleControls::raise() { + _minimize->raise(); + _maximizeRestore->raise(); + _close->raise(); +} + +Ui::IconButton *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 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::view::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 parent) +: RpWidget(parent) +, _controls(this, st::defaultWindowTitle) +, _shadow(this, st::titleShadow) { + setAttribute(Qt::WA_OpaquePaintEvent); +} + +not_null DefaultTitleWidget::st() const { + return _controls.st(); +} + +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()->windowHandle()); + } +} + +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); + } +} + +} // namespace Platform +} // namespace Ui diff --git a/ui/platform/ui_platform_window_title.h b/ui/platform/ui_platform_window_title.h new file mode 100644 index 0000000..af66155 --- /dev/null +++ b/ui/platform/ui_platform_window_title.h @@ -0,0 +1,101 @@ +// 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 +// +#pragma once + +#include "ui/rp_widget.h" +#include "base/object_ptr.h" + +#include +#include + +namespace style { +struct WindowTitle; +} // namespace style + +namespace Ui { + +class IconButton; +class PlainShadow; + +namespace Platform { + +class TitleControls final { +public: + TitleControls( + not_null parent, + const style::WindowTitle &st, + Fn maximize = nullptr); + + void setStyle(const style::WindowTitle &st); + [[nodiscard]] not_null st() const; + [[nodiscard]] QRect geometry() const; + void setResizeEnabled(bool enabled); + void raise(); + + enum class Control { + Unknown, + Minimize, + Maximize, + Close, + }; + + struct Layout { + std::vector left; + std::vector right; + }; + +private: + [[nodiscard]] not_null parent() const; + [[nodiscard]] not_null window() const; + [[nodiscard]] Ui::IconButton *controlWidget(Control control) const; + + void init(Fn maximize); + void updateButtonsState(); + void updateControlsPosition(); + void updateControlsPositionBySide( + const std::vector &controls, + bool right); + void handleWindowStateChanged(Qt::WindowState state = Qt::WindowNoState); + + not_null _st; + + object_ptr _minimize; + object_ptr _maximizeRestore; + object_ptr _close; + + bool _maximizedState = false; + bool _activeState = false; + bool _resizeEnabled = true; + +}; + +class DefaultTitleWidget : public RpWidget { +public: + explicit DefaultTitleWidget(not_null parent); + + [[nodiscard]] not_null st() const; + void setText(const QString &text); + void setStyle(const style::WindowTitle &st); + void setResizeEnabled(bool enabled); + +protected: + void paintEvent(QPaintEvent *e) override; + void resizeEvent(QResizeEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void mouseDoubleClickEvent(QMouseEvent *e) override; + +private: + TitleControls _controls; + object_ptr _shadow; + bool _mousePressed = false; + +}; + +} // namespace Platform +} // namespace Ui diff --git a/ui/platform/win/ui_utility_win.cpp b/ui/platform/win/ui_utility_win.cpp index 6af98ea..4c4cfcb 100644 --- a/ui/platform/win/ui_utility_win.cpp +++ b/ui/platform/win/ui_utility_win.cpp @@ -6,10 +6,11 @@ // #include "ui/platform/win/ui_utility_win.h" -#include - #include "base/platform/win/base_windows_h.h" +#include +#include + namespace Ui { namespace Platform { @@ -44,5 +45,27 @@ void IgnoreAllActivation(not_null widget) { ShowWindow(handle, SW_SHOW); } +bool ShowWindowMenu(QWindow *window) { + const auto pos = QCursor::pos(); + + SendMessage( + HWND(window->winId()), + WM_SYSCOMMAND, + SC_MOUSEMENU, + MAKELPARAM(pos.x(), pos.y())); + + return true; +} + +TitleControls::Layout TitleControlsLayout() { + return TitleControls::Layout{ + .right = { + TitleControls::Control::Minimize, + TitleControls::Control::Maximize, + TitleControls::Control::Close, + } + }; +} + } // namespace Platform } // namespace Ui diff --git a/ui/platform/win/ui_utility_win.h b/ui/platform/win/ui_utility_win.h index a7a1a14..d836b49 100644 --- a/ui/platform/win/ui_utility_win.h +++ b/ui/platform/win/ui_utility_win.h @@ -6,6 +6,8 @@ // #pragma once +#include "ui/platform/ui_platform_utility.h" + #include class QPainter; @@ -40,5 +42,17 @@ inline constexpr bool UseMainQueueGeneric() { return true; } +inline bool WindowExtentsSupported() { + return false; +} + +inline bool SetWindowExtents(QWindow *window, const QMargins &extents) { + return false; +} + +inline bool UnsetWindowExtents(QWindow *window) { + return false; +} + } // namespace Platform } // namespace Ui diff --git a/ui/platform/win/ui_window_title_win.cpp b/ui/platform/win/ui_window_title_win.cpp index c28cf16..ba578f9 100644 --- a/ui/platform/win/ui_window_title_win.cpp +++ b/ui/platform/win/ui_window_title_win.cpp @@ -19,170 +19,6 @@ namespace Ui { namespace Platform { -TitleControls::TitleControls( - not_null parent, - const style::WindowTitle &st, - Fn 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 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([=] { - window()->setWindowState( - window()->windowState() | Qt::WindowMinimized); - _minimize->clearState(); - }); - _minimize->setPointerCursor(false); - _maximizeRestore->setClickedCallback([=] { - if (maximize) { - maximize(!_maximizedState); - } else { - window()->setWindowState(_maximizedState - ? Qt::WindowNoState - : Qt::WindowMaximized); - } - _maximizeRestore->clearState(); - }); - _maximizeRestore->setPointerCursor(false); - _close->setClickedCallback([=] { - window()->close(); - _close->clearState(); - }); - _close->setPointerCursor(false); - - parent()->widthValue( - ) | rpl::start_with_next([=](int width) { - updateControlsPosition(); - }, _close->lifetime()); - - window()->createWinId(); - QObject::connect( - window()->windowHandle(), - &QWindow::windowStateChanged, - [=](Qt::WindowState state) { handleWindowStateChanged(state); }); - _activeState = parent()->isActiveWindow(); - updateButtonsState(); -} - -void TitleControls::setResizeEnabled(bool enabled) { - _resizeEnabled = enabled; - updateControlsVisibility(); -} - -void TitleControls::raise() { - _minimize->raise(); - _maximizeRestore->raise(); - _close->raise(); -} - -void TitleControls::updateControlsPosition() { - auto right = 0; - _close->moveToRight(right, 0); right += _close->width(); - _maximizeRestore->moveToRight(right, 0); - if (_resizeEnabled) { - right += _maximizeRestore->width(); - } - _minimize->moveToRight(right, 0); -} - -void TitleControls::updateControlsVisibility() { - _maximizeRestore->setVisible(_resizeEnabled); - updateControlsPosition(); -} - -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); -} - TitleWidget::TitleWidget(not_null parent) : RpWidget(parent) , _controls(this, st::defaultWindowTitle) diff --git a/ui/platform/win/ui_window_title_win.h b/ui/platform/win/ui_window_title_win.h index 6515065..bc5fed6 100644 --- a/ui/platform/win/ui_window_title_win.h +++ b/ui/platform/win/ui_window_title_win.h @@ -6,6 +6,7 @@ // #pragma once +#include "ui/platform/ui_platform_window_title.h" #include "ui/rp_widget.h" #include "base/object_ptr.h" @@ -38,41 +39,6 @@ enum class HitTestResult { TopLeft, }; -class TitleControls final { -public: - TitleControls( - not_null parent, - const style::WindowTitle &st, - Fn maximize = nullptr); - - void setStyle(const style::WindowTitle &st); - [[nodiscard]] not_null st() const; - [[nodiscard]] QRect geometry() const; - void setResizeEnabled(bool enabled); - void raise(); - -private: - [[nodiscard]] not_null parent() const; - [[nodiscard]] not_null window() const; - - void init(Fn maximize); - void updateControlsVisibility(); - void updateButtonsState(); - void updateControlsPosition(); - void handleWindowStateChanged(Qt::WindowState state = Qt::WindowNoState); - - not_null _st; - - object_ptr _minimize; - object_ptr _maximizeRestore; - object_ptr _close; - - bool _maximizedState = false; - bool _activeState = false; - bool _resizeEnabled = true; - -}; - class TitleWidget : public RpWidget { public: explicit TitleWidget(not_null parent); diff --git a/ui/rp_widget.cpp b/ui/rp_widget.cpp index 8646c92..16f8919 100644 --- a/ui/rp_widget.cpp +++ b/ui/rp_widget.cpp @@ -160,8 +160,10 @@ bool RpWidgetMethods::handleEvent(QEvent *event) { return eventHook(event); } -RpWidgetMethods::Initer::Initer(QWidget *parent) { - parent->setGeometry(0, 0, 0, 0); +RpWidgetMethods::Initer::Initer(QWidget *parent, bool setZeroGeometry) { + if (setZeroGeometry) { + parent->setGeometry(0, 0, 0, 0); + } } void RpWidgetMethods::visibilityChangedHook(bool wasVisible, bool nowVisible) { diff --git a/ui/rp_widget.h b/ui/rp_widget.h index 8cffbe5..f6ce3c2 100644 --- a/ui/rp_widget.h +++ b/ui/rp_widget.h @@ -243,7 +243,7 @@ using RpWidgetParent = std::conditional_t< TWidget, TWidgetHelper>; -template +template class RpWidgetWrap; class RpWidgetMethods { @@ -281,7 +281,7 @@ protected: virtual bool eventHook(QEvent *event) = 0; private: - template + template friend class RpWidgetWrap; struct EventStreams { @@ -292,7 +292,7 @@ private: rpl::event_stream<> alive; }; struct Initer { - Initer(QWidget *parent); + Initer(QWidget *parent, bool setZeroGeometry); }; virtual void callSetVisible(bool visible) = 0; @@ -310,11 +310,15 @@ private: }; -template +struct RpWidgetDefaultTraits { + static constexpr bool kSetZeroGeometry = true; +}; + +template class RpWidgetWrap : public RpWidgetParent , public RpWidgetMethods { - using Self = RpWidgetWrap; + using Self = RpWidgetWrap; using Parent = RpWidgetParent; public: @@ -362,7 +366,7 @@ private: return this->isHidden(); } - Initer _initer = { this }; + Initer _initer = { this, Traits::kSetZeroGeometry }; }; diff --git a/ui/widgets/buttons.cpp b/ui/widgets/buttons.cpp index d770d0e..2760a79 100644 --- a/ui/widgets/buttons.cpp +++ b/ui/widgets/buttons.cpp @@ -79,6 +79,10 @@ RippleButton::RippleButton(QWidget *parent, const style::RippleAnimation &st) void RippleButton::clearState() { AbstractButton::clearState(); + finishAnimating(); +} + +void RippleButton::finishAnimating() { if (_ripple) { _ripple.reset(); update(); @@ -116,6 +120,13 @@ void RippleButton::setForceRippled( update(); } +void RippleButton::paintRipple( + QPainter &p, + const QPoint &point, + const QColor *colorOverride) { + paintRipple(p, point.x(), point.y(), colorOverride); +} + void RippleButton::paintRipple(QPainter &p, int x, int y, const QColor *colorOverride) { if (_ripple) { _ripple->paint(p, x, y, width(), colorOverride); @@ -195,6 +206,11 @@ void FlatButton::setWidth(int w) { resize(_width, height()); } +void FlatButton::setColorOverride(std::optional color) { + _colorOverride = color; + update(); +} + int32 FlatButton::textWidth() const { return _st.font->width(_text); } @@ -214,7 +230,11 @@ void FlatButton::paintEvent(QPaintEvent *e) { p.setFont(isOver() ? _st.overFont : _st.font); p.setRenderHint(QPainter::TextAntialiasing); - p.setPen(isOver() ? _st.overColor : _st.color); + if (_colorOverride) { + p.setPen(*_colorOverride); + } else { + p.setPen(isOver() ? _st.overColor : _st.color); + } const auto textRect = inner.marginsRemoved( _textMargins @@ -361,7 +381,7 @@ void RoundButton::paintEvent(QPaintEvent *e) { drawRect(_roundRectOver); } - paintRipple(p, rounded.x(), rounded.y()); + paintRipple(p, rounded.topLeft()); p.setFont(_st.font); const auto textTop = _st.padding.top() + _st.textTop; @@ -392,7 +412,10 @@ void RoundButton::paintEvent(QPaintEvent *e) { _numbers->paint(p, textLeft, textTop, width()); } if (!_st.icon.empty()) { - _st.icon.paint(p, QPoint(iconLeft, iconTop), width()); + const auto ¤t = ((over || down) && !_st.iconOver.empty()) + ? _st.iconOver + : _st.icon; + current.paint(p, QPoint(iconLeft, iconTop), width()); } } @@ -431,7 +454,7 @@ void IconButton::setRippleColorOverride(const style::color *colorOverride) { void IconButton::paintEvent(QPaintEvent *e) { Painter p(this); - paintRipple(p, _st.rippleAreaPosition.x(), _st.rippleAreaPosition.y(), _rippleColorOverride ? &(*_rippleColorOverride)->c : nullptr); + paintRipple(p, _st.rippleAreaPosition, _rippleColorOverride ? &(*_rippleColorOverride)->c : nullptr); auto down = isDown(); auto overIconOpacity = (down || forceRippled()) ? 1. : _a_over.value(isOver() ? 1. : 0.); @@ -547,7 +570,7 @@ void CrossButton::paintEvent(QPaintEvent *e) { auto shown = _showAnimation.value(_shown ? 1. : 0.); p.setOpacity(shown); - paintRipple(p, _st.crossPosition.x(), _st.crossPosition.y()); + paintRipple(p, _st.crossPosition); auto loading = 0.; if (_loadingAnimation.animating()) { diff --git a/ui/widgets/buttons.h b/ui/widgets/buttons.h index d654d2f..9b1e3f8 100644 --- a/ui/widgets/buttons.h +++ b/ui/widgets/buttons.h @@ -58,7 +58,17 @@ public: void clearState() override; - void paintRipple(QPainter &p, int x, int y, const QColor *colorOverride = nullptr); + void paintRipple( + QPainter &p, + const QPoint &point, + const QColor *colorOverride = nullptr); + void paintRipple( + QPainter &p, + int x, + int y, + const QColor *colorOverride = nullptr); + + void finishAnimating(); ~RippleButton(); @@ -84,6 +94,7 @@ public: void setText(const QString &text); void setWidth(int w); + void setColorOverride(std::optional color); void setTextMargins(QMargins margins); int32 textWidth() const; @@ -97,6 +108,7 @@ private: QString _text; QMargins _textMargins; int _width = 0; + std::optional _colorOverride; const style::FlatButton &_st; diff --git a/ui/widgets/call_button.cpp b/ui/widgets/call_button.cpp index 25bf401..638c67e 100644 --- a/ui/widgets/call_button.cpp +++ b/ui/widgets/call_button.cpp @@ -153,7 +153,7 @@ void CallButton::paintEvent(QPaintEvent *e) { } else { rippleColorInterpolated = anim::color(_stFrom->button.ripple.color, _stTo->button.ripple.color, _progress); } - paintRipple(p, _stFrom->button.rippleAreaPosition.x(), _stFrom->button.rippleAreaPosition.y(), rippleColorOverride); + paintRipple(p, _stFrom->button.rippleAreaPosition, rippleColorOverride); auto positionFrom = iconPosition(_stFrom); if (paintFrom) { diff --git a/ui/widgets/call_mute_button.cpp b/ui/widgets/call_mute_button.cpp index 15e3820..10c5062 100644 --- a/ui/widgets/call_mute_button.cpp +++ b/ui/widgets/call_mute_button.cpp @@ -328,6 +328,15 @@ BlobsWidget::BlobsWidget( void BlobsWidget::init() { setAttribute(Qt::WA_TransparentForMouseEvents); + const auto cutRect = [](Painter &p, const QRectF &r) { + p.save(); + p.setOpacity(1.); + p.setBrush(st::groupCallBg); + p.setCompositionMode(QPainter::CompositionMode_Source); + p.drawEllipse(r); + p.restore(); + }; + { const auto s = _blobs.maxRadius() * 2 * kGlowPaddingFactor; resize(s, s); @@ -351,6 +360,8 @@ void BlobsWidget::init() { Painter p(this); PainterHighQualityEnabler hq(p); + p.setPen(Qt::NoPen); + // Glow. const auto s = kGlowMinScale + (1. - kGlowMinScale) * _blobs.currentLevel(); @@ -369,37 +380,41 @@ void BlobsWidget::init() { _switchConnectingProgress / kBlobPartAnimation))) : _blobsScaleEnter; _blobs.paint(p, _blobBrush, scale); + p.translate(-_center, -_center); + + if (scale < 1.) { + cutRect(p, _circleRect); + } // Main circle. - p.translate(-_center, -_center); - p.setPen(Qt::NoPen); - p.setBrush(_blobBrush); - p.drawEllipse(_circleRect); + const auto circleProgress = + Clamp(_switchConnectingProgress - kBlobPartAnimation) + / kFillCirclePartAnimation; + const auto skipColoredCircle = (circleProgress == 1.); + + if (!skipColoredCircle) { + p.setBrush(_blobBrush); + p.drawEllipse(_circleRect); + } if (_switchConnectingProgress > 0.) { p.resetTransform(); - const auto circleProgress = - Clamp(_switchConnectingProgress - kBlobPartAnimation) - / kFillCirclePartAnimation; - const auto mF = (_circleRect.width() / 2) * (1. - circleProgress); const auto cutOutRect = _circleRect.marginsRemoved( QMarginsF(mF, mF, mF, mF)); - p.setPen(Qt::NoPen); - p.setBrush(st::callConnectingRadial.color); - p.setOpacity(circleProgress); - p.drawEllipse(_circleRect); + if (!skipColoredCircle) { + p.setBrush(st::callConnectingRadial.color); + p.setOpacity(circleProgress); + p.drawEllipse(_circleRect); + } p.setOpacity(1.); + + cutRect(p, cutOutRect); + p.setBrush(st::callIconBg); - - p.save(); - p.setCompositionMode(QPainter::CompositionMode_Source); - p.drawEllipse(cutOutRect); - p.restore(); - p.drawEllipse(cutOutRect); } }, lifetime()); @@ -553,25 +568,33 @@ void CallMuteButton::init() { }, _centerLabel->lifetime()); _centerLabel->setAttribute(Qt::WA_TransparentForMouseEvents); - _radialInfo.rawShowProgress.value( - ) | rpl::start_with_next([=](float64 value) { + rpl::combine( + _radialInfo.rawShowProgress.value(), + anim::Disables() + ) | rpl::start_with_next([=](float64 value, bool disabled) { auto &info = _radialInfo; info.realShowProgress = (1. - value) / kRadialEndPartAnimation; - if (((value == 0.) || anim::Disabled()) && _radial) { + const auto guard = gsl::finally([&] { + _content->update(); + }); + + if (((value == 0.) || disabled) && _radial) { _radial->stop(); _radial = nullptr; return; } - if ((value > 0.) && !anim::Disabled() && !_radial) { + if ((value > 0.) && !disabled && !_radial) { _radial = std::make_unique( [=] { _content->update(); }, _radialInfo.st); _radial->start(); } if ((info.realShowProgress < 1.) && !info.isDirectionToShow) { - _radial->stop(anim::type::instant); - _radial->start(); + if (_radial) { + _radial->stop(anim::type::instant); + _radial->start(); + } info.state = std::nullopt; return; } @@ -721,6 +744,8 @@ void CallMuteButton::init() { const auto to = r.arcFrom - kRadialFinishArcShift; ComputeRadialFinish(r.arcFrom, radialProgress, to); ComputeRadialFinish(r.arcLength, radialProgress); + } else { + r.arcLength = RadialState::kFull; } const auto opacity = (radialProgress > kOverlapProgressRadialHide) diff --git a/ui/widgets/checkbox.cpp b/ui/widgets/checkbox.cpp index eefca13..29014e9 100644 --- a/ui/widgets/checkbox.cpp +++ b/ui/widgets/checkbox.cpp @@ -320,7 +320,8 @@ void RadioView::paint(Painter &p, int left, int top, int outerWidth) { p.setBrush(_st->bg); //int32 skip = qCeil(_st->thickness / 2.); //p.drawEllipse(_checkRect.marginsRemoved(QMargins(skip, skip, skip, skip))); - p.drawEllipse(style::rtlrect(QRectF(left, top, _st->diameter, _st->diameter).marginsRemoved(QMarginsF(_st->thickness / 2., _st->thickness / 2., _st->thickness / 2., _st->thickness / 2.)), outerWidth)); + const auto skip = (_st->outerSkip / 10.) + (_st->thickness / 2); + p.drawEllipse(style::rtlrect(QRectF(left, top, _st->diameter, _st->diameter).marginsRemoved(QMarginsF(skip, skip, skip, skip)), outerWidth)); if (toggled > 0) { p.setPen(Qt::NoPen); @@ -332,7 +333,7 @@ void RadioView::paint(Painter &p, int left, int top, int outerWidth) { ? anim::brush(*_untoggledOverride, _st->toggledFg, toggled) : anim::brush(_st->untoggledFg, _st->toggledFg, toggled))); - auto skip0 = _st->diameter / 2., skip1 = _st->skip / 10., checkSkip = skip0 * (1. - toggled) + skip1 * toggled; + const auto skip0 = _st->diameter / 2., skip1 = _st->skip / 10., checkSkip = skip0 * (1. - toggled) + skip1 * toggled; p.drawEllipse(style::rtlrect(QRectF(left, top, _st->diameter, _st->diameter).marginsRemoved(QMarginsF(checkSkip, checkSkip, checkSkip, checkSkip)), outerWidth)); //int32 fskip = qFloor(checkSkip), cskip = qCeil(checkSkip); //if (2 * fskip < _checkRect.width()) { @@ -569,11 +570,7 @@ void Checkbox::paintEvent(QPaintEvent *e) { p.setOpacity(_st.disabledOpacity); } else { auto color = anim::color(_st.rippleBg, _st.rippleBgActive, active); - paintRipple( - p, - check.x() + _st.rippleAreaPosition.x(), - check.y() + _st.rippleAreaPosition.y(), - &color); + paintRipple(p, check.topLeft() + _st.rippleAreaPosition, &color); } auto realCheckRect = myrtlrect(check); diff --git a/ui/widgets/dropdown_menu.cpp b/ui/widgets/dropdown_menu.cpp index 9e69974..a8f1960 100644 --- a/ui/widgets/dropdown_menu.cpp +++ b/ui/widgets/dropdown_menu.cpp @@ -12,7 +12,7 @@ namespace Ui { DropdownMenu::DropdownMenu(QWidget *parent, const style::DropdownMenu &st) : InnerDropdown(parent, st.wrap) , _st(st) { - _menu = setOwnedWidget(object_ptr(this, _st.menu)); + _menu = setOwnedWidget(object_ptr(this, _st.menu)); init(); } @@ -33,12 +33,15 @@ DropdownMenu::DropdownMenu(QWidget *parent, const style::DropdownMenu &st) : Inn void DropdownMenu::init() { InnerDropdown::setHiddenCallback([this] { hideFinish(); }); - _menu->setResizedCallback([this] { resizeToContent(); }); - _menu->setActivatedCallback([this](QAction *action, int actionTop, TriggeredSource source) { - handleActivated(action, actionTop, source); + _menu->resizesFromInner( + ) | rpl::start_with_next([=] { + resizeToContent(); + }, _menu->lifetime()); + _menu->setActivatedCallback([this](const Menu::CallbackData &data) { + handleActivated(data); }); - _menu->setTriggeredCallback([this](QAction *action, int actionTop, TriggeredSource source) { - handleTriggered(action, actionTop, source); + _menu->setTriggeredCallback([this](const Menu::CallbackData &data) { + handleTriggered(data); }); _menu->setKeyPressDelegate([this](int key) { return handleKeyPress(key); }); _menu->setMouseMoveDelegate([this](QPoint globalPosition) { handleMouseMove(globalPosition); }); @@ -50,8 +53,9 @@ void DropdownMenu::init() { hide(); } -not_null DropdownMenu::addAction(const QString &text, const QObject *receiver, const char* member, const style::icon *icon, const style::icon *iconOver) { - return _menu->addAction(text, receiver, member, icon, iconOver); +not_null DropdownMenu::addAction( + base::unique_qptr widget) { + return _menu->addAction(std::move(widget)); } not_null DropdownMenu::addAction(const QString &text, Fn callback, const style::icon *icon, const style::icon *iconOver) { @@ -73,9 +77,13 @@ const std::vector> &DropdownMenu::actions() const { return _menu->actions(); } -void DropdownMenu::handleActivated(QAction *action, int actionTop, TriggeredSource source) { - if (source == TriggeredSource::Mouse) { - if (!popupSubmenuFromAction(action, actionTop, source)) { +bool DropdownMenu::empty() const { + return _menu->empty(); +} + +void DropdownMenu::handleActivated(const Menu::CallbackData &data) { + if (data.source == TriggeredSource::Mouse) { + if (!popupSubmenuFromAction(data)) { if (auto currentSubmenu = base::take(_activeSubmenu)) { currentSubmenu->hideMenu(true); } @@ -83,11 +91,11 @@ void DropdownMenu::handleActivated(QAction *action, int actionTop, TriggeredSour } } -void DropdownMenu::handleTriggered(QAction *action, int actionTop, TriggeredSource source) { - if (!popupSubmenuFromAction(action, actionTop, source)) { +void DropdownMenu::handleTriggered(const Menu::CallbackData &data) { + if (!popupSubmenuFromAction(data)) { hideMenu(); _triggering = true; - emit action->trigger(); + emit data.action->trigger(); _triggering = false; if (_deleteLater) { _deleteLater = false; @@ -97,7 +105,7 @@ void DropdownMenu::handleTriggered(QAction *action, int actionTop, TriggeredSour } // Not ready with submenus yet. -bool DropdownMenu::popupSubmenuFromAction(QAction *action, int actionTop, TriggeredSource source) { +bool DropdownMenu::popupSubmenuFromAction(const Menu::CallbackData &data) { //if (auto submenu = _submenus.value(action)) { // if (_activeSubmenu == submenu) { // submenu->hideMenu(true); @@ -126,9 +134,9 @@ bool DropdownMenu::popupSubmenuFromAction(QAction *action, int actionTop, Trigge // } //} -void DropdownMenu::forwardKeyPress(int key) { - if (!handleKeyPress(key)) { - _menu->handleKeyPress(key); +void DropdownMenu::forwardKeyPress(not_null e) { + if (!handleKeyPress(e->key())) { + _menu->handleKeyPress(e); } } @@ -185,7 +193,7 @@ void DropdownMenu::hideEvent(QHideEvent *e) { } void DropdownMenu::keyPressEvent(QKeyEvent *e) { - forwardKeyPress(e->key()); + forwardKeyPress(e); } void DropdownMenu::mouseMoveEvent(QMouseEvent *e) { diff --git a/ui/widgets/dropdown_menu.h b/ui/widgets/dropdown_menu.h index 27a9ded..166616f 100644 --- a/ui/widgets/dropdown_menu.h +++ b/ui/widgets/dropdown_menu.h @@ -8,7 +8,7 @@ #include "styles/style_widgets.h" #include "ui/widgets/inner_dropdown.h" -#include "ui/widgets/menu.h" +#include "ui/widgets/menu/menu.h" namespace Ui { @@ -18,7 +18,7 @@ class DropdownMenu : public InnerDropdown { public: DropdownMenu(QWidget *parent, const style::DropdownMenu &st = st::defaultDropdownMenu); - not_null addAction(const QString &text, const QObject *receiver, const char* member, const style::icon *icon = nullptr, const style::icon *iconOver = nullptr); + not_null addAction(base::unique_qptr widget); not_null addAction(const QString &text, Fn callback, const style::icon *icon = nullptr, const style::icon *iconOver = nullptr); not_null addSeparator(); void clearActions(); @@ -28,6 +28,7 @@ public: } const std::vector> &actions() const; + bool empty() const; ~DropdownMenu(); @@ -56,9 +57,9 @@ private: void hideFinish(); using TriggeredSource = Menu::TriggeredSource; - void handleActivated(QAction *action, int actionTop, TriggeredSource source); - void handleTriggered(QAction *action, int actionTop, TriggeredSource source); - void forwardKeyPress(int key); + void handleActivated(const Menu::CallbackData &data); + void handleTriggered(const Menu::CallbackData &data); + void forwardKeyPress(not_null e); bool handleKeyPress(int key); void forwardMouseMove(QPoint globalPosition) { _menu->handleMouseMove(globalPosition); @@ -74,14 +75,14 @@ private: void handleMouseRelease(QPoint globalPosition); using SubmenuPointer = QPointer; - bool popupSubmenuFromAction(QAction *action, int actionTop, TriggeredSource source); + bool popupSubmenuFromAction(const Menu::CallbackData &data); void popupSubmenu(SubmenuPointer submenu, int actionTop, TriggeredSource source); void showMenu(const QPoint &p, DropdownMenu *parent, TriggeredSource source); const style::DropdownMenu &_st; Fn _hiddenCallback; - QPointer _menu; + QPointer _menu; // Not ready with submenus yet. //using Submenus = QMap; diff --git a/ui/widgets/input_fields.cpp b/ui/widgets/input_fields.cpp index ab9416b..51bb098 100644 --- a/ui/widgets/input_fields.cpp +++ b/ui/widgets/input_fields.cpp @@ -2659,12 +2659,18 @@ bool InputField::ShouldSubmit( } void InputField::keyPressEventInner(QKeyEvent *e) { - bool shift = e->modifiers().testFlag(Qt::ShiftModifier), alt = e->modifiers().testFlag(Qt::AltModifier); - bool macmeta = Platform::IsMac() && e->modifiers().testFlag(Qt::ControlModifier) && !e->modifiers().testFlag(Qt::MetaModifier) && !e->modifiers().testFlag(Qt::AltModifier); - bool ctrl = e->modifiers().testFlag(Qt::ControlModifier) || e->modifiers().testFlag(Qt::MetaModifier); - bool enterSubmit = (_mode != Mode::MultiLine) + const auto shift = e->modifiers().testFlag(Qt::ShiftModifier); + const auto alt = e->modifiers().testFlag(Qt::AltModifier); + const auto macmeta = Platform::IsMac() + && e->modifiers().testFlag(Qt::ControlModifier) + && !e->modifiers().testFlag(Qt::MetaModifier) + && !e->modifiers().testFlag(Qt::AltModifier); + const auto ctrl = e->modifiers().testFlag(Qt::ControlModifier) + || e->modifiers().testFlag(Qt::MetaModifier); + const auto enterSubmit = (_mode != Mode::MultiLine) || ShouldSubmit(_submitSettings, e->modifiers()); - bool enter = (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return); + const auto enter = (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return); + const auto backspace = (e->key() == Qt::Key_Backspace); if (e->key() == Qt::Key_Left || e->key() == Qt::Key_Right || e->key() == Qt::Key_Up @@ -2674,12 +2680,12 @@ void InputField::keyPressEventInner(QKeyEvent *e) { _reverseMarkdownReplacement = false; } - if (macmeta && e->key() == Qt::Key_Backspace) { + if (macmeta && backspace) { QTextCursor tc(textCursor()), start(tc); start.movePosition(QTextCursor::StartOfLine); tc.setPosition(start.position(), QTextCursor::KeepAnchor); tc.removeSelectedText(); - } else if (e->key() == Qt::Key_Backspace + } else if (backspace && e->modifiers() == 0 && revertFormatReplace()) { e->accept(); @@ -2716,12 +2722,22 @@ void InputField::keyPressEventInner(QKeyEvent *e) { } else { const auto text = e->text(); const auto oldPosition = textCursor().position(); - if (enter && ctrl) { - e->setModifiers(e->modifiers() & ~Qt::ControlModifier); - } else if (enter && shift) { - e->setModifiers(e->modifiers() & ~Qt::ShiftModifier); + const auto oldModifiers = e->modifiers(); + const auto allowedModifiers = (enter && ctrl) + ? (~Qt::ControlModifier) + : (enter && shift) + ? (~Qt::ShiftModifier) + : (backspace && Platform::IsLinux()) + ? (Qt::ControlModifier) + : oldModifiers; + const auto changeModifiers = (oldModifiers & ~allowedModifiers) != 0; + if (changeModifiers) { + e->setModifiers(oldModifiers & allowedModifiers); } _inner->QTextEdit::keyPressEvent(e); + if (changeModifiers) { + e->setModifiers(oldModifiers); + } auto cursor = textCursor(); if (cursor.position() == oldPosition) { bool check = false; @@ -4023,18 +4039,20 @@ PasswordInput::PasswordInput( setEchoMode(QLineEdit::Password); } -PortInput::PortInput( +NumberInput::NumberInput( QWidget *parent, const style::InputField &st, rpl::producer placeholder, - const QString &val) -: MaskedInputField(parent, st, std::move(placeholder), val) { - if (!val.toInt() || val.toInt() > 65535) { + const QString &value, + int limit) +: MaskedInputField(parent, st, std::move(placeholder), value) +, _limit(limit) { + if (!value.toInt() || (limit > 0 && value.toInt() > limit)) { setText(QString()); } } -void PortInput::correctValue( +void NumberInput::correctValue( const QString &was, int wasCursor, QString &now, @@ -4052,7 +4070,7 @@ void PortInput::correctValue( if (!newText.toInt()) { newText = QString(); newPos = 0; - } else if (newText.toInt() > 65535) { + } else if (_limit > 0 && newText.toInt() > _limit) { newText = was; newPos = wasCursor; } diff --git a/ui/widgets/input_fields.h b/ui/widgets/input_fields.h index cdfb51a..b9290ce 100644 --- a/ui/widgets/input_fields.h +++ b/ui/widgets/input_fields.h @@ -697,9 +697,14 @@ public: }; -class PortInput : public MaskedInputField { +class NumberInput : public MaskedInputField { public: - PortInput(QWidget *parent, const style::InputField &st, rpl::producer placeholder, const QString &val); + NumberInput( + QWidget *parent, + const style::InputField &st, + rpl::producer placeholder, + const QString &value, + int limit); protected: void correctValue( @@ -708,6 +713,9 @@ protected: QString &now, int &nowCursor) override; +private: + int _limit = 0; + }; class HexInput : public MaskedInputField { diff --git a/ui/widgets/labels.cpp b/ui/widgets/labels.cpp index ac5c900..c97143d 100644 --- a/ui/widgets/labels.cpp +++ b/ui/widgets/labels.cpp @@ -637,12 +637,17 @@ void FlatLabel::showContextMenu(QContextMenuEvent *e, ContextMenuReason reason) _contextMenu = new PopupMenu(this); if (fullSelection && !_contextCopyText.isEmpty()) { - _contextMenu->addAction(_contextCopyText, this, SLOT(onCopyContextText())); + _contextMenu->addAction( + _contextCopyText, + [=] { onCopyContextText(); }); } else if (uponSelection && !fullSelection) { - const auto text = Integration::Instance().phraseContextCopySelected(); - _contextMenu->addAction(text, this, SLOT(onCopySelectedText())); + _contextMenu->addAction( + Integration::Instance().phraseContextCopySelected(), + [=] { onCopySelectedText(); }); } else if (_selectable && !hasSelection && !_contextCopyText.isEmpty()) { - _contextMenu->addAction(_contextCopyText, this, SLOT(onCopyContextText())); + _contextMenu->addAction( + _contextCopyText, + [=] { onCopyContextText(); }); } if (const auto link = ClickHandler::getActive()) { diff --git a/ui/widgets/menu.cpp b/ui/widgets/menu.cpp deleted file mode 100644 index 2605a8a..0000000 --- a/ui/widgets/menu.cpp +++ /dev/null @@ -1,523 +0,0 @@ -// 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/widgets/menu.h" - -#include "ui/effects/ripple_animation.h" -#include "ui/widgets/checkbox.h" -#include "ui/text/text.h" - -#include - -namespace Ui { -namespace { - -[[nodiscard]] TextWithEntities ParseMenuItem(const QString &text) { - auto result = TextWithEntities(); - result.text.reserve(text.size()); - auto afterAmpersand = false; - for (const auto ch : text) { - if (afterAmpersand) { - afterAmpersand = false; - if (ch == '&') { - result.text.append(ch); - } else { - result.entities.append(EntityInText{ - EntityType::Underline, - result.text.size(), - 1 }); - result.text.append(ch); - } - } else if (ch == '&') { - afterAmpersand = true; - } else { - result.text.append(ch); - } - } - return result; -} - -TextParseOptions MenuTextOptions = { - TextParseLinks | TextParseRichText, // flags - 0, // maxw - 0, // maxh - Qt::LayoutDirectionAuto, // dir -}; - -} // namespace - -struct Menu::ActionData { - Text::String text; - QString shortcut; - const style::icon *icon = nullptr; - const style::icon *iconOver = nullptr; - std::unique_ptr ripple; - std::unique_ptr toggle; - int textWidth = 0; - bool hasSubmenu = false; -}; - -Menu::Menu(QWidget *parent, const style::Menu &st) -: RpWidget(parent) -, _st(st) -, _itemHeight(_st.itemPadding.top() + _st.itemStyle.font->height + _st.itemPadding.bottom()) -, _separatorHeight(_st.separatorPadding.top() + _st.separatorWidth + _st.separatorPadding.bottom()) { - init(); -} - -Menu::Menu(QWidget *parent, QMenu *menu, const style::Menu &st) -: RpWidget(parent) -, _st(st) -, _wappedMenu(menu) -, _itemHeight(_st.itemPadding.top() + _st.itemStyle.font->height + _st.itemPadding.bottom()) -, _separatorHeight(_st.separatorPadding.top() + _st.separatorWidth + _st.separatorPadding.bottom()) { - init(); - - _wappedMenu->setParent(this); - for (auto action : _wappedMenu->actions()) { - addAction(action); - } - _wappedMenu->hide(); -} - -Menu::~Menu() = default; - -void Menu::init() { - resize(_forceWidth ? _forceWidth : _st.widthMin, _st.skip * 2); - - setMouseTracking(true); - - if (_st.itemBg->c.alpha() == 255) { - setAttribute(Qt::WA_OpaquePaintEvent); - } -} - -not_null Menu::addAction(const QString &text, const QObject *receiver, const char* member, const style::icon *icon, const style::icon *iconOver) { - const auto action = addAction(new QAction(text, this), icon, iconOver); - connect(action, SIGNAL(triggered(bool)), receiver, member, Qt::QueuedConnection); - return action; -} - -not_null Menu::addAction(const QString &text, Fn callback, const style::icon *icon, const style::icon *iconOver) { - const auto action = addAction(new QAction(text, this), icon, iconOver); - connect(action, &QAction::triggered, action, std::move(callback), Qt::QueuedConnection); - return action; -} - -not_null Menu::addAction(const QString &text, std::unique_ptr submenu) { - const auto action = new QAction(text, this); - action->setMenu(submenu.release()); - return addAction(action, nullptr, nullptr); -} - -not_null Menu::addAction(not_null action, const style::icon *icon, const style::icon *iconOver) { - connect(action, &QAction::changed, this, [=] { - actionChanged(); - }); - _actions.emplace_back(action); - _actionsData.push_back([&] { - auto data = ActionData(); - data.icon = icon; - data.iconOver = iconOver ? iconOver : icon; - data.hasSubmenu = (action->menu() != nullptr); - return data; - }()); - - auto newWidth = qMax(width(), _st.widthMin); - newWidth = processAction(action, _actions.size() - 1, newWidth); - auto newHeight = height() + (action->isSeparator() ? _separatorHeight : _itemHeight); - resize(_forceWidth ? _forceWidth : newWidth, newHeight); - if (_resizedCallback) { - _resizedCallback(); - } - updateSelected(QCursor::pos()); - update(); - - return action; -} - -not_null Menu::addSeparator() { - const auto separator = new QAction(this); - separator->setSeparator(true); - return addAction(separator); -} - -void Menu::clearActions() { - setSelected(-1); - setPressed(-1); - _actionsData.clear(); - for (auto action : base::take(_actions)) { - if (action->parent() == this) { - delete action; - } - } - resize(_forceWidth ? _forceWidth : _st.widthMin, _st.skip * 2); - if (_resizedCallback) { - _resizedCallback(); - } -} - -void Menu::finishAnimating() { - for (auto &data : _actionsData) { - if (data.ripple) { - data.ripple.reset(); - } - if (data.toggle) { - data.toggle->finishAnimating(); - } - } -} - -int Menu::processAction(not_null action, int index, int width) { - auto &data = _actionsData[index]; - if (action->isSeparator() || action->text().isEmpty()) { - data.shortcut = QString(); - data.text.clear(); - } else { - auto actionTextParts = action->text().split('\t'); - auto actionText = actionTextParts.empty() ? QString() : actionTextParts[0]; - auto actionShortcut = (actionTextParts.size() > 1) ? actionTextParts[1] : QString(); - data.text.setMarkedText(_st.itemStyle, ParseMenuItem(actionText), MenuTextOptions); - const auto textw = data.text.maxWidth(); - int goodw = _st.itemPadding.left() + textw + _st.itemPadding.right(); - if (data.hasSubmenu) { - goodw += _st.itemPadding.right() + _st.arrow.width(); - } else if (!actionShortcut.isEmpty()) { - goodw += _st.itemPadding.right() + _st.itemStyle.font->width(actionShortcut); - } - if (action->isCheckable()) { - auto updateCallback = [this, index] { updateItem(index); }; - if (data.toggle) { - data.toggle->setUpdateCallback(updateCallback); - data.toggle->setChecked(action->isChecked(), anim::type::normal); - } else { - data.toggle = std::make_unique(_st.itemToggle, action->isChecked(), updateCallback); - } - goodw += _st.itemPadding.right() + data.toggle->getSize().width() - _st.itemToggleShift; - } else { - data.toggle.reset(); - } - width = std::clamp(goodw, width, _st.widthMax); - data.textWidth = width - (goodw - textw); - data.shortcut = actionShortcut; - } - return width; -} - -void Menu::setShowSource(TriggeredSource source) { - _mouseSelection = (source == TriggeredSource::Mouse); - setSelected((source == TriggeredSource::Mouse || _actions.empty()) ? -1 : 0); -} - -const std::vector> &Menu::actions() const { - return _actions; -} - -void Menu::setForceWidth(int forceWidth) { - _forceWidth = forceWidth; - resize(_forceWidth, height()); -} - -void Menu::actionChanged() { - auto newWidth = _st.widthMin; - auto index = 0; - for (const auto action : _actions) { - newWidth = processAction(action, index++, newWidth); - } - if (newWidth != width() && !_forceWidth) { - resize(newWidth, height()); - if (_resizedCallback) { - _resizedCallback(); - } - } - update(); -} - -void Menu::paintEvent(QPaintEvent *e) { - Painter p(this); - - auto clip = e->rect(); - - auto topskip = QRect(0, 0, width(), _st.skip); - auto bottomskip = QRect(0, height() - _st.skip, width(), _st.skip); - if (clip.intersects(topskip)) p.fillRect(clip.intersected(topskip), _st.itemBg); - if (clip.intersects(bottomskip)) p.fillRect(clip.intersected(bottomskip), _st.itemBg); - - int top = _st.skip; - p.translate(0, top); - p.setFont(_st.itemStyle.font); - for (int i = 0, count = int(_actions.size()); i != count; ++i) { - if (clip.top() + clip.height() <= top) break; - - const auto action = _actions[i]; - auto &data = _actionsData[i]; - auto actionHeight = action->isSeparator() ? _separatorHeight : _itemHeight; - top += actionHeight; - if (clip.top() < top) { - if (action->isSeparator()) { - p.fillRect(0, 0, width(), actionHeight, _st.itemBg); - p.fillRect(_st.separatorPadding.left(), _st.separatorPadding.top(), width() - _st.separatorPadding.left() - _st.separatorPadding.right(), _st.separatorWidth, _st.separatorFg); - } else { - auto enabled = action->isEnabled(); - auto selected = ((i == _selected || i == _pressed) && enabled); - if (selected && _st.itemBgOver->c.alpha() < 255) { - p.fillRect(0, 0, width(), actionHeight, _st.itemBg); - } - p.fillRect(0, 0, width(), actionHeight, selected ? _st.itemBgOver : _st.itemBg); - if (data.ripple) { - data.ripple->paint(p, 0, 0, width()); - if (data.ripple->empty()) { - data.ripple.reset(); - } - } - if (auto icon = (selected ? data.iconOver : data.icon)) { - icon->paint(p, _st.itemIconPosition, width()); - } - p.setPen(selected ? _st.itemFgOver : (enabled ? _st.itemFg : _st.itemFgDisabled)); - data.text.drawLeftElided(p, _st.itemPadding.left(), _st.itemPadding.top(), data.textWidth, width()); - if (data.hasSubmenu) { - const auto left = width() - _st.itemPadding.right() - _st.arrow.width(); - const auto top = (_itemHeight - _st.arrow.height()) / 2; - if (enabled) { - _st.arrow.paint(p, left, top, width()); - } else { - _st.arrow.paint( - p, - left, - top, - width(), - _st.itemFgDisabled->c); - } - } else if (!data.shortcut.isEmpty()) { - p.setPen(selected ? _st.itemFgShortcutOver : (enabled ? _st.itemFgShortcut : _st.itemFgShortcutDisabled)); - p.drawTextRight(_st.itemPadding.right(), _st.itemPadding.top(), width(), data.shortcut); - } else if (data.toggle) { - auto toggleSize = data.toggle->getSize(); - data.toggle->paint(p, width() - _st.itemPadding.right() - toggleSize.width() + _st.itemToggleShift, (_itemHeight - toggleSize.height()) / 2, width()); - } - } - } - p.translate(0, actionHeight); - } -} - -void Menu::updateSelected(QPoint globalPosition) { - if (!_mouseSelection) return; - - auto p = mapFromGlobal(globalPosition) - QPoint(0, _st.skip); - auto selected = -1, top = 0; - while (top <= p.y() && ++selected < _actions.size()) { - top += _actions[selected]->isSeparator() ? _separatorHeight : _itemHeight; - } - setSelected((selected >= 0 && selected < _actions.size() && _actions[selected]->isEnabled() && !_actions[selected]->isSeparator()) ? selected : -1); -} - -void Menu::itemPressed(TriggeredSource source) { - if (source == TriggeredSource::Mouse && !_mouseSelection) { - return; - } - if (_selected >= 0 && _selected < _actions.size() && _actions[_selected]->isEnabled()) { - setPressed(_selected); - if (source == TriggeredSource::Mouse) { - if (!_actionsData[_pressed].ripple) { - auto mask = RippleAnimation::rectMask(QSize(width(), _itemHeight)); - _actionsData[_pressed].ripple = std::make_unique(_st.ripple, std::move(mask), [this, selected = _pressed] { - updateItem(selected); - }); - } - _actionsData[_pressed].ripple->add(mapFromGlobal(QCursor::pos()) - QPoint(0, itemTop(_pressed))); - } else { - itemReleased(source); - } - } -} - -void Menu::itemReleased(TriggeredSource source) { - if (_pressed >= 0 && _pressed < _actions.size()) { - auto pressed = _pressed; - setPressed(-1); - if (source == TriggeredSource::Mouse && _actionsData[pressed].ripple) { - _actionsData[pressed].ripple->lastStop(); - } - if (pressed == _selected && _triggeredCallback) { - _triggeredCallback(_actions[_selected], itemTop(_selected), source); - } - } -} - -void Menu::keyPressEvent(QKeyEvent *e) { - auto key = e->key(); - if (!_keyPressDelegate || !_keyPressDelegate(key)) { - handleKeyPress(key); - } -} - -void Menu::handleKeyPress(int key) { - if (key == Qt::Key_Enter || key == Qt::Key_Return) { - itemPressed(TriggeredSource::Keyboard); - return; - } - if (key == (style::RightToLeft() ? Qt::Key_Left : Qt::Key_Right)) { - if (_selected >= 0 && _actionsData[_selected].hasSubmenu) { - itemPressed(TriggeredSource::Keyboard); - return; - } else if (_selected < 0 && !_actions.empty()) { - _mouseSelection = false; - setSelected(0); - } - } - if ((key != Qt::Key_Up && key != Qt::Key_Down) || _actions.empty()) { - return; - } - - auto delta = (key == Qt::Key_Down ? 1 : -1), start = _selected; - if (start < 0 || start >= _actions.size()) { - start = (delta > 0) ? (_actions.size() - 1) : 0; - } - auto newSelected = start; - do { - newSelected += delta; - if (newSelected < 0) { - newSelected += _actions.size(); - } else if (newSelected >= _actions.size()) { - newSelected -= _actions.size(); - } - } while (newSelected != start && (!_actions[newSelected]->isEnabled() || _actions[newSelected]->isSeparator())); - - if (_actions[newSelected]->isEnabled() && !_actions[newSelected]->isSeparator()) { - _mouseSelection = false; - setSelected(newSelected); - } -} - -void Menu::clearSelection() { - _mouseSelection = false; - setSelected(-1); -} - -void Menu::clearMouseSelection() { - if (_mouseSelection && !_childShown) { - clearSelection(); - } -} - -void Menu::enterEventHook(QEvent *e) { - QPoint mouse = QCursor::pos(); - if (!rect().marginsRemoved(QMargins(0, _st.skip, 0, _st.skip)).contains(mapFromGlobal(mouse))) { - clearMouseSelection(); - } - return TWidget::enterEventHook(e); -} - -void Menu::leaveEventHook(QEvent *e) { - clearMouseSelection(); - return TWidget::leaveEventHook(e); -} - -void Menu::setSelected(int selected) { - if (selected >= _actions.size()) { - selected = -1; - } - if (_selected != selected) { - updateSelectedItem(); - if (_selected >= 0 && _selected != _pressed && _actionsData[_selected].toggle) { - _actionsData[_selected].toggle->setStyle(_st.itemToggle); - } - _selected = selected; - if (_selected >= 0 && _actionsData[_selected].toggle && _actions[_selected]->isEnabled()) { - _actionsData[_selected].toggle->setStyle(_st.itemToggleOver); - } - updateSelectedItem(); - if (_activatedCallback) { - auto source = _mouseSelection ? TriggeredSource::Mouse : TriggeredSource::Keyboard; - _activatedCallback( - (_selected >= 0) ? _actions[_selected].get() : nullptr, - itemTop(_selected), - source); - } - } -} - -void Menu::setPressed(int pressed) { - if (pressed >= _actions.size()) { - pressed = -1; - } - if (_pressed != pressed) { - if (_pressed >= 0 && _pressed != _selected && _actionsData[_pressed].toggle) { - _actionsData[_pressed].toggle->setStyle(_st.itemToggle); - } - _pressed = pressed; - if (_pressed >= 0 && _actionsData[_pressed].toggle && _actions[_pressed]->isEnabled()) { - _actionsData[_pressed].toggle->setStyle(_st.itemToggleOver); - } - } -} - -int Menu::itemTop(int index) { - if (index > _actions.size()) { - index = _actions.size(); - } - int top = _st.skip; - for (int i = 0; i < index; ++i) { - top += _actions.at(i)->isSeparator() ? _separatorHeight : _itemHeight; - } - return top; -} - -void Menu::updateItem(int index) { - if (index >= 0 && index < _actions.size()) { - update(0, itemTop(index), width(), _actions[index]->isSeparator() ? _separatorHeight : _itemHeight); - } -} - -void Menu::updateSelectedItem() { - updateItem(_selected); -} - -void Menu::mouseMoveEvent(QMouseEvent *e) { - handleMouseMove(e->globalPos()); -} - -void Menu::handleMouseMove(QPoint globalPosition) { - auto inner = rect().marginsRemoved(QMargins(0, _st.skip, 0, _st.skip)); - auto localPosition = mapFromGlobal(globalPosition); - if (inner.contains(localPosition)) { - _mouseSelection = true; - updateSelected(globalPosition); - } else { - clearMouseSelection(); - if (_mouseMoveDelegate) { - _mouseMoveDelegate(globalPosition); - } - } -} - -void Menu::mousePressEvent(QMouseEvent *e) { - handleMousePress(e->globalPos()); -} - -void Menu::mouseReleaseEvent(QMouseEvent *e) { - handleMouseRelease(e->globalPos()); -} - -void Menu::handleMousePress(QPoint globalPosition) { - handleMouseMove(globalPosition); - if (rect().contains(mapFromGlobal(globalPosition))) { - itemPressed(TriggeredSource::Mouse); - } else if (_mousePressDelegate) { - _mousePressDelegate(globalPosition); - } -} - -void Menu::handleMouseRelease(QPoint globalPosition) { - handleMouseMove(globalPosition); - itemReleased(TriggeredSource::Mouse); - if (!rect().contains(mapFromGlobal(globalPosition)) && _mouseReleaseDelegate) { - _mouseReleaseDelegate(globalPosition); - } -} - -} // namespace Ui diff --git a/ui/widgets/menu/menu.cpp b/ui/widgets/menu/menu.cpp new file mode 100644 index 0000000..61ee9a4 --- /dev/null +++ b/ui/widgets/menu/menu.cpp @@ -0,0 +1,347 @@ +// 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/widgets/menu/menu.h" + +#include "ui/widgets/menu/menu_action.h" +#include "ui/widgets/menu/menu_item_base.h" +#include "ui/widgets/menu/menu_separator.h" + +#include + +namespace Ui::Menu { + +Menu::Menu(QWidget *parent, const style::Menu &st) +: RpWidget(parent) +, _st(st) { + init(); +} + +Menu::Menu(QWidget *parent, QMenu *menu, const style::Menu &st) +: RpWidget(parent) +, _st(st) +, _wappedMenu(menu) { + init(); + + _wappedMenu->setParent(this); + for (auto action : _wappedMenu->actions()) { + addAction(action); + } + _wappedMenu->hide(); +} + +Menu::~Menu() = default; + +void Menu::init() { + resize(_forceWidth ? _forceWidth : _st.widthMin, _st.skip * 2); + + setMouseTracking(true); + + if (_st.itemBg->c.alpha() == 255) { + setAttribute(Qt::WA_OpaquePaintEvent); + } + + paintRequest( + ) | rpl::start_with_next([=](const QRect &clip) { + Painter p(this); + p.fillRect(clip, _st.itemBg); + }, lifetime()); + + positionValue( + ) | rpl::start_with_next([=] { + handleMouseMove(QCursor::pos()); + }, lifetime()); +} + +not_null Menu::addAction( + const QString &text, + Fn callback, + const style::icon *icon, + const style::icon *iconOver) { + auto action = CreateAction(this, text, std::move(callback)); + return addAction(std::move(action), icon, iconOver); +} + +not_null Menu::addAction( + const QString &text, + std::unique_ptr submenu) { + const auto action = new QAction(text, this); + action->setMenu(submenu.release()); + return addAction(action, nullptr, nullptr); +} + +not_null Menu::addAction( + not_null action, + const style::icon *icon, + const style::icon *iconOver) { + if (action->isSeparator()) { + return addSeparator(); + } + auto item = base::make_unique_q( + this, + _st, + std::move(action), + icon, + iconOver ? iconOver : icon); + return addAction(std::move(item)); +} + +not_null Menu::addAction(base::unique_qptr widget) { + const auto action = widget->action(); + _actions.emplace_back(action); + + widget->setParent(this); + + const auto top = _actionWidgets.empty() + ? 0 + : _actionWidgets.back()->y() + _actionWidgets.back()->height(); + + widget->moveToLeft(0, top); + widget->show(); + + widget->setIndex(_actionWidgets.size()); + + widget->selects( + ) | rpl::start_with_next([=](const CallbackData &data) { + if (!data.selected) { + return; + } + for (auto i = 0; i < _actionWidgets.size(); i++) { + if (i != data.index) { + _actionWidgets[i]->setSelected(false); + } + } + if (_activatedCallback) { + _activatedCallback(data); + } + }, widget->lifetime()); + + widget->clicks( + ) | rpl::start_with_next([=](const CallbackData &data) { + if (_triggeredCallback) { + _triggeredCallback(data); + } + }, widget->lifetime()); + + const auto raw = widget.get(); + _actionWidgets.push_back(std::move(widget)); + + raw->minWidthValue( + ) | rpl::start_with_next([=] { + const auto newWidth = _forceWidth + ? _forceWidth + : std::clamp( + _actionWidgets.empty() + ? 0 + : (*ranges::max_element( + _actionWidgets, + std::less<>(), + &ItemBase::minWidth))->minWidth(), + _st.widthMin, + _st.widthMax); + resizeFromInner(newWidth, height()); + }, raw->lifetime()); + + const auto newHeight = ranges::accumulate( + _actionWidgets, + 0, + ranges::plus(), + &ItemBase::height); + resizeFromInner(width(), newHeight); + updateSelected(QCursor::pos()); + + return action; +} + +not_null Menu::addSeparator() { + const auto separator = new QAction(this); + separator->setSeparator(true); + auto item = base::make_unique_q(this, _st, separator); + return addAction(std::move(item)); +} + +void Menu::clearActions() { + _actionWidgets.clear(); + for (auto action : base::take(_actions)) { + if (action->parent() == this) { + delete action; + } + } + resizeFromInner(_forceWidth ? _forceWidth : _st.widthMin, _st.skip * 2); +} + +void Menu::finishAnimating() { + for (const auto &widget : _actionWidgets) { + widget->finishAnimating(); + } +} + +bool Menu::empty() const { + return _actionWidgets.empty(); +} + +void Menu::resizeFromInner(int w, int h) { + if ((w == width()) && (h == height())) { + return; + } + resize(w, h); + _resizesFromInner.fire({}); +} + +rpl::producer<> Menu::resizesFromInner() const { + return _resizesFromInner.events(); +} + +void Menu::setShowSource(TriggeredSource source) { + const auto mouseSelection = (source == TriggeredSource::Mouse); + setSelected( + (mouseSelection || _actions.empty()) ? -1 : 0, + mouseSelection); +} + +const std::vector> &Menu::actions() const { + return _actions; +} + +void Menu::setForceWidth(int forceWidth) { + _forceWidth = forceWidth; + resizeFromInner(_forceWidth, height()); +} + +void Menu::updateSelected(QPoint globalPosition) { + const auto p = mapFromGlobal(globalPosition) - QPoint(0, _st.skip); + for (const auto &widget : _actionWidgets) { + const auto widgetRect = QRect(widget->pos(), widget->size()); + if (widgetRect.contains(p)) { + widget->setSelected(true); + break; + } + } +} + +void Menu::itemPressed(TriggeredSource source) { + if (const auto action = findSelectedAction()) { + if (action->lastTriggeredSource() == source) { + action->setClicked(source); + } + } +} + +void Menu::keyPressEvent(QKeyEvent *e) { + const auto key = e->key(); + if (!_keyPressDelegate || !_keyPressDelegate(key)) { + handleKeyPress(e); + } +} + +ItemBase *Menu::findSelectedAction() const { + const auto it = ranges::find_if(_actionWidgets, &ItemBase::isSelected); + return (it == end(_actionWidgets)) ? nullptr : it->get(); +} + +void Menu::handleKeyPress(not_null e) { + const auto key = e->key(); + const auto selected = findSelectedAction(); + if ((key != Qt::Key_Up && key != Qt::Key_Down) || _actions.empty()) { + if (selected) { + selected->handleKeyPress(e); + } + return; + } + + const auto delta = (key == Qt::Key_Down ? 1 : -1); + auto start = selected ? selected->index() : -1; + if (start < 0 || start >= _actions.size()) { + start = (delta > 0) ? (_actions.size() - 1) : 0; + } + auto newSelected = start; + do { + newSelected += delta; + if (newSelected < 0) { + newSelected += _actions.size(); + } else if (newSelected >= _actions.size()) { + newSelected -= _actions.size(); + } + } while (newSelected != start && (!_actionWidgets[newSelected]->isEnabled())); + + if (_actionWidgets[newSelected]->isEnabled()) { + setSelected(newSelected, false); + } +} + +void Menu::clearSelection() { + setSelected(-1, false); +} + +void Menu::clearMouseSelection() { + const auto selected = findSelectedAction(); + const auto mouseSelection = selected + ? (selected->lastTriggeredSource() == TriggeredSource::Mouse) + : false; + if (mouseSelection && !_childShown) { + clearSelection(); + } +} + +void Menu::setSelected(int selected, bool isMouseSelection) { + if (selected >= _actionWidgets.size()) { + selected = -1; + } + const auto source = isMouseSelection + ? TriggeredSource::Mouse + : TriggeredSource::Keyboard; + if (const auto selectedItem = findSelectedAction()) { + if (selectedItem->index() == selected) { + return; + } + selectedItem->setSelected(false, source); + } + if (selected >= 0) { + _actionWidgets[selected]->setSelected(true, source); + } +} + +void Menu::mouseMoveEvent(QMouseEvent *e) { + handleMouseMove(e->globalPos()); +} + +void Menu::handleMouseMove(QPoint globalPosition) { + const auto margins = style::margins(0, _st.skip, 0, _st.skip); + const auto inner = rect().marginsRemoved(margins); + const auto localPosition = mapFromGlobal(globalPosition); + if (inner.contains(localPosition)) { + updateSelected(globalPosition); + } else { + clearMouseSelection(); + if (_mouseMoveDelegate) { + _mouseMoveDelegate(globalPosition); + } + } +} + +void Menu::mousePressEvent(QMouseEvent *e) { + handleMousePress(e->globalPos()); +} + +void Menu::mouseReleaseEvent(QMouseEvent *e) { + handleMouseRelease(e->globalPos()); +} + +void Menu::handleMousePress(QPoint globalPosition) { + handleMouseMove(globalPosition); + if (_mousePressDelegate) { + _mousePressDelegate(globalPosition); + } +} + +void Menu::handleMouseRelease(QPoint globalPosition) { + if (!rect().contains(mapFromGlobal(globalPosition)) + && _mouseReleaseDelegate) { + _mouseReleaseDelegate(globalPosition); + } +} + +} // namespace Ui::Menu diff --git a/ui/widgets/menu.h b/ui/widgets/menu/menu.h similarity index 56% rename from ui/widgets/menu.h rename to ui/widgets/menu/menu.h index 2cf7347..19b40fb 100644 --- a/ui/widgets/menu.h +++ b/ui/widgets/menu/menu.h @@ -6,14 +6,16 @@ // #pragma once +#include "base/unique_qptr.h" #include "ui/rp_widget.h" +#include "ui/widgets/menu/menu_common.h" #include "styles/style_widgets.h" #include -namespace Ui { +namespace Ui::Menu { -class ToggleView; +class ItemBase; class RippleAnimation; class Menu : public RpWidget { @@ -22,19 +24,27 @@ public: Menu(QWidget *parent, QMenu *menu, const style::Menu &st = st::defaultMenu); ~Menu(); - not_null addAction(const QString &text, const QObject *receiver, const char* member, const style::icon *icon = nullptr, const style::icon *iconOver = nullptr); - not_null addAction(const QString &text, Fn callback, const style::icon *icon = nullptr, const style::icon *iconOver = nullptr); - not_null addAction(const QString &text, std::unique_ptr submenu); + [[nodiscard]] const style::Menu &st() const { + return _st; + } + + not_null addAction(base::unique_qptr widget); + not_null addAction( + const QString &text, + Fn callback, + const style::icon *icon = nullptr, + const style::icon *iconOver = nullptr); + not_null addAction( + const QString &text, + std::unique_ptr submenu); not_null addSeparator(); void clearActions(); void finishAnimating(); + bool empty() const; + void clearSelection(); - enum class TriggeredSource { - Mouse, - Keyboard, - }; void setChildShown(bool shown) { _childShown = shown; } @@ -43,21 +53,17 @@ public: const std::vector> &actions() const; - void setResizedCallback(Fn callback) { - _resizedCallback = std::move(callback); - } - - void setActivatedCallback(Fn callback) { + void setActivatedCallback(Fn callback) { _activatedCallback = std::move(callback); } - void setTriggeredCallback(Fn callback) { + void setTriggeredCallback(Fn callback) { _triggeredCallback = std::move(callback); } void setKeyPressDelegate(Fn delegate) { _keyPressDelegate = std::move(delegate); } - void handleKeyPress(int key); + void handleKeyPress(not_null e); void setMouseMoveDelegate(Fn delegate) { _mouseMoveDelegate = std::move(delegate); @@ -74,41 +80,36 @@ public: } void handleMouseRelease(QPoint globalPosition); + rpl::producer<> resizesFromInner() const; + protected: - void paintEvent(QPaintEvent *e) override; void keyPressEvent(QKeyEvent *e) override; void mouseMoveEvent(QMouseEvent *e) override; void mousePressEvent(QMouseEvent *e) override; void mouseReleaseEvent(QMouseEvent *e) override; - void enterEventHook(QEvent *e) override; - void leaveEventHook(QEvent *e) override; private: - struct ActionData; - void updateSelected(QPoint globalPosition); - void actionChanged(); void init(); - // Returns the new width. - int processAction(not_null action, int index, int width); - not_null addAction(not_null action, const style::icon *icon = nullptr, const style::icon *iconOver = nullptr); + not_null addAction( + not_null action, + const style::icon *icon = nullptr, + const style::icon *iconOver = nullptr); - void setSelected(int selected); - void setPressed(int pressed); + void setSelected(int selected, bool isMouseSelection); void clearMouseSelection(); - int itemTop(int index); - void updateItem(int index); - void updateSelectedItem(); void itemPressed(TriggeredSource source); - void itemReleased(TriggeredSource source); + + ItemBase *findSelectedAction() const; + + void resizeFromInner(int w, int h); const style::Menu &_st; - Fn _resizedCallback; - Fn _activatedCallback; - Fn _triggeredCallback; + Fn _activatedCallback; + Fn _triggeredCallback; Fn _keyPressDelegate; Fn _mouseMoveDelegate; Fn _mousePressDelegate; @@ -116,17 +117,14 @@ private: QMenu *_wappedMenu = nullptr; std::vector> _actions; - std::vector _actionsData; + std::vector> _actionWidgets; int _forceWidth = 0; - int _itemHeight, _separatorHeight; - bool _mouseSelection = false; - - int _selected = -1; - int _pressed = -1; bool _childShown = false; + rpl::event_stream<> _resizesFromInner; + }; -} // namespace Ui +} // namespace Ui::Menu diff --git a/ui/widgets/menu/menu_action.cpp b/ui/widgets/menu/menu_action.cpp new file mode 100644 index 0000000..2f1234d --- /dev/null +++ b/ui/widgets/menu/menu_action.cpp @@ -0,0 +1,200 @@ +// 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/widgets/menu/menu_action.h" + +#include "ui/effects/ripple_animation.h" +#include "ui/painter.h" + +#include + +namespace Ui::Menu { +namespace { + +[[nodiscard]] TextWithEntities ParseMenuItem(const QString &text) { + auto result = TextWithEntities(); + result.text.reserve(text.size()); + auto afterAmpersand = false; + for (const auto ch : text) { + if (afterAmpersand) { + afterAmpersand = false; + if (ch == '&') { + result.text.append(ch); + } else { + result.entities.append(EntityInText{ + EntityType::Underline, + result.text.size(), + 1 }); + result.text.append(ch); + } + } else if (ch == '&') { + afterAmpersand = true; + } else { + result.text.append(ch); + } + } + return result; +} + +TextParseOptions MenuTextOptions = { + TextParseLinks | TextParseRichText, // flags + 0, // maxw + 0, // maxh + Qt::LayoutDirectionAuto, // dir +}; + +} // namespace + +Action::Action( + not_null parent, + const style::Menu &st, + not_null action, + const style::icon *icon, + const style::icon *iconOver) +: ItemBase(parent, st) +, _action(action) +, _st(st) +, _icon(icon) +, _iconOver(iconOver) +, _height(_st.itemPadding.top() + + _st.itemStyle.font->height + + _st.itemPadding.bottom()) { + + setAcceptBoth(true); + + initResizeHook(parent->sizeValue()); + processAction(); + + paintRequest( + ) | rpl::start_with_next([=] { + Painter p(this); + paint(p); + }, lifetime()); + + enableMouseSelecting(); + + connect(_action, &QAction::changed, [=] { processAction(); }); +} + +bool Action::hasSubmenu() const { + return _action->menu() != nullptr; +} + +void Action::paint(Painter &p) { + const auto enabled = _action->isEnabled(); + const auto selected = isSelected(); + if (selected && _st.itemBgOver->c.alpha() < 255) { + p.fillRect(0, 0, width(), _height, _st.itemBg); + } + p.fillRect(0, 0, width(), _height, selected ? _st.itemBgOver : _st.itemBg); + if (isEnabled()) { + paintRipple(p, 0, 0); + } + if (const auto icon = (selected ? _iconOver : _icon)) { + icon->paint(p, _st.itemIconPosition, width()); + } + p.setPen(selected ? _st.itemFgOver : (enabled ? _st.itemFg : _st.itemFgDisabled)); + _text.drawLeftElided(p, _st.itemPadding.left(), _st.itemPadding.top(), _textWidth, width()); + if (hasSubmenu()) { + const auto left = width() - _st.itemPadding.right() - _st.arrow.width(); + const auto top = (_height - _st.arrow.height()) / 2; + if (enabled) { + _st.arrow.paint(p, left, top, width()); + } else { + _st.arrow.paint( + p, + left, + top, + width(), + _st.itemFgDisabled->c); + } + } else if (!_shortcut.isEmpty()) { + p.setPen(selected + ? _st.itemFgShortcutOver + : (enabled ? _st.itemFgShortcut : _st.itemFgShortcutDisabled)); + p.drawTextRight( + _st.itemPadding.right(), + _st.itemPadding.top(), + width(), + _shortcut); + } +} + +void Action::processAction() { + if (_action->text().isEmpty()) { + _shortcut = QString(); + _text.clear(); + return; + } + const auto actionTextParts = _action->text().split('\t'); + const auto actionText = actionTextParts.empty() + ? QString() + : actionTextParts[0]; + const auto actionShortcut = (actionTextParts.size() > 1) + ? actionTextParts[1] + : QString(); + _text.setMarkedText( + _st.itemStyle, + ParseMenuItem(actionText), + MenuTextOptions); + const auto textWidth = _text.maxWidth(); + const auto &padding = _st.itemPadding; + + const auto additionalWidth = hasSubmenu() + ? padding.right() + _st.arrow.width() + : (!actionShortcut.isEmpty()) + ? (padding.right() + _st.itemStyle.font->width(actionShortcut)) + : 0; + const auto goodWidth = padding.left() + + textWidth + + padding.right() + + additionalWidth; + + const auto w = std::clamp(goodWidth, _st.widthMin, _st.widthMax); + _textWidth = w - (goodWidth - textWidth); + _shortcut = actionShortcut; + setMinWidth(w); + update(); +} + +bool Action::isEnabled() const { + return _action->isEnabled(); +} + +not_null Action::action() const { + return _action; +} + +QPoint Action::prepareRippleStartPosition() const { + return mapFromGlobal(QCursor::pos()); +} + +QImage Action::prepareRippleMask() const { + return Ui::RippleAnimation::rectMask(size()); +} + +int Action::contentHeight() const { + return _height; +} + +void Action::handleKeyPress(not_null e) { + if (!isSelected()) { + return; + } + const auto key = e->key(); + if (key == Qt::Key_Enter || key == Qt::Key_Return) { + setClicked(TriggeredSource::Keyboard); + return; + } + if (key == (style::RightToLeft() ? Qt::Key_Left : Qt::Key_Right)) { + if (hasSubmenu()) { + setClicked(TriggeredSource::Keyboard); + return; + } + } +} + +} // namespace Ui::Menu diff --git a/ui/widgets/menu/menu_action.h b/ui/widgets/menu/menu_action.h new file mode 100644 index 0000000..2600788 --- /dev/null +++ b/ui/widgets/menu/menu_action.h @@ -0,0 +1,55 @@ +// 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 +// +#pragma once + +#include "ui/text/text.h" +#include "ui/widgets/menu/menu_item_base.h" +#include "styles/style_widgets.h" + +class Painter; + +namespace Ui::Menu { + +class Action : public ItemBase { +public: + Action( + not_null parent, + const style::Menu &st, + not_null action, + const style::icon *icon, + const style::icon *iconOver); + + bool isEnabled() const override; + not_null action() const override; + + void handleKeyPress(not_null e) override; + +protected: + QPoint prepareRippleStartPosition() const override; + QImage prepareRippleMask() const override; + + int contentHeight() const override; + +private: + void processAction(); + void paint(Painter &p); + + bool hasSubmenu() const; + + Text::String _text; + QString _shortcut; + const not_null _action; + const style::Menu &_st; + const style::icon *_icon; + const style::icon *_iconOver; +// std::unique_ptr _toggle; + int _textWidth = 0; + const int _height; + +}; + +} // namespace Ui::Menu diff --git a/ui/widgets/menu/menu_common.cpp b/ui/widgets/menu/menu_common.cpp new file mode 100644 index 0000000..7e29038 --- /dev/null +++ b/ui/widgets/menu/menu_common.cpp @@ -0,0 +1,27 @@ +// 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/widgets/menu/menu_common.h" + +#include + +namespace Ui::Menu { + +not_null CreateAction( + QWidget *parent, + const QString &text, + Fn &&callback) { + const auto action = new QAction(text, parent); + parent->connect( + action, + &QAction::triggered, + action, + std::move(callback), + Qt::QueuedConnection); + return action; +} + +} // namespace Ui::Menu diff --git a/ui/widgets/menu/menu_common.h b/ui/widgets/menu/menu_common.h new file mode 100644 index 0000000..6d3de70 --- /dev/null +++ b/ui/widgets/menu/menu_common.h @@ -0,0 +1,29 @@ +// 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 +// +#pragma once + +namespace Ui::Menu { + +enum class TriggeredSource { + Mouse, + Keyboard, +}; + +struct CallbackData { + QAction *action; + int actionTop = 0; + TriggeredSource source; + int index = 0; + bool selected = false; +}; + +not_null CreateAction( + QWidget *parent, + const QString &text, + Fn &&callback); + +} // namespace Ui::Menu diff --git a/ui/widgets/menu/menu_item_base.cpp b/ui/widgets/menu/menu_item_base.cpp new file mode 100644 index 0000000..4bce600 --- /dev/null +++ b/ui/widgets/menu/menu_item_base.cpp @@ -0,0 +1,119 @@ +// 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/widgets/menu/menu_item_base.h" + +namespace Ui::Menu { + +ItemBase::ItemBase( + not_null parent, + const style::Menu &st) +: RippleButton(parent, st.ripple) { +} + +void ItemBase::setSelected( + bool selected, + TriggeredSource source) { + if (!isEnabled()) { + return; + } + if (_selected.current() != selected) { + setMouseTracking(!selected); + _lastTriggeredSource = source; + _selected = selected; + update(); + } +} + +bool ItemBase::isSelected() const { + return _selected.current(); +} + +rpl::producer ItemBase::selects() const { + return _selected.changes( + ) | rpl::map([=](bool selected) -> CallbackData { + return { action(), y(), _lastTriggeredSource, _index, selected }; + }); +} + +TriggeredSource ItemBase::lastTriggeredSource() const { + return _lastTriggeredSource; +} + +int ItemBase::index() const { + return _index; +} + +void ItemBase::setIndex(int index) { + _index = index; +} + +void ItemBase::setClicked(TriggeredSource source) { + if (isEnabled()) { + _lastTriggeredSource = source; + _clicks.fire({}); + } +} + +rpl::producer ItemBase::clicks() const { + return rpl::merge( + AbstractButton::clicks() | rpl::to_empty, + _clicks.events() + ) | rpl::filter([=] { + return isEnabled(); + }) | rpl::map([=]() -> CallbackData { + return { action(), y(), _lastTriggeredSource, _index, true }; + }); +} + +rpl::producer ItemBase::minWidthValue() const { + return _minWidth.value(); +} + +int ItemBase::minWidth() const { + return _minWidth.current(); +} + +void ItemBase::initResizeHook(rpl::producer &&size) { + std::move( + size + ) | rpl::start_with_next([=](QSize s) { + resize(s.width(), contentHeight()); + }, lifetime()); +} + +void ItemBase::setMinWidth(int w) { + _minWidth = w; +} + +void ItemBase::finishAnimating() { + RippleButton::finishAnimating(); +} + +void ItemBase::enableMouseSelecting() { + enableMouseSelecting(this); +} + +void ItemBase::enableMouseSelecting(not_null widget) { + widget->events( + ) | rpl::start_with_next([=](not_null e) { + const auto type = e->type(); + if (((type == QEvent::Leave) + || (type == QEvent::Enter) + || (type == QEvent::MouseMove)) && action()->isEnabled()) { + setSelected(e->type() != QEvent::Leave); + } else if ((type == QEvent::MouseButtonRelease) + && isEnabled() + && isSelected()) { + const auto point = mapFromGlobal(QCursor::pos()); + if (!rect().contains(point)) { + setSelected(false); + } + } + }, lifetime()); +} + +} // namespace Ui::Menu diff --git a/ui/widgets/menu/menu_item_base.h b/ui/widgets/menu/menu_item_base.h new file mode 100644 index 0000000..7a13bc6 --- /dev/null +++ b/ui/widgets/menu/menu_item_base.h @@ -0,0 +1,67 @@ +// 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 +// +#pragma once + +#include "ui/widgets/buttons.h" +#include "ui/widgets/menu/menu.h" +#include "ui/widgets/menu/menu_common.h" +#include "styles/style_widgets.h" + +namespace Ui::Menu { + +class ItemBase : public RippleButton { +public: + ItemBase(not_null parent, const style::Menu &st); + + TriggeredSource lastTriggeredSource() const; + + rpl::producer selects() const; + void setSelected( + bool selected, + TriggeredSource source = TriggeredSource::Mouse); + bool isSelected() const; + + int index() const; + void setIndex(int index); + + void setClicked(TriggeredSource source = TriggeredSource::Mouse); + + rpl::producer clicks() const; + + rpl::producer minWidthValue() const; + int minWidth() const; + void setMinWidth(int w); + + virtual void handleKeyPress(not_null e) { + } + + virtual not_null action() const = 0; + virtual bool isEnabled() const = 0; + + virtual void finishAnimating(); + +protected: + void initResizeHook(rpl::producer &&size); + + void enableMouseSelecting(); + void enableMouseSelecting(not_null widget); + + virtual int contentHeight() const = 0; + +private: + int _index = -1; + + rpl::variable _selected = false; + rpl::event_stream<> _clicks; + + rpl::variable _minWidth = 0; + + TriggeredSource _lastTriggeredSource = TriggeredSource::Mouse; + +}; + +} // namespace Ui::Menu diff --git a/ui/widgets/menu/menu_separator.cpp b/ui/widgets/menu/menu_separator.cpp new file mode 100644 index 0000000..a2460b7 --- /dev/null +++ b/ui/widgets/menu/menu_separator.cpp @@ -0,0 +1,52 @@ +// 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/widgets/menu/menu_separator.h" + +#include "ui/painter.h" + +namespace Ui::Menu { + +Separator::Separator( + not_null parent, + const style::Menu &st, + not_null action) +: ItemBase(parent, st) +, _lineWidth(st.separatorWidth) +, _padding(st.separatorPadding) +, _fg(st.separatorFg) +, _bg(st.itemBg) +, _height(_padding.top() + _lineWidth + _padding.bottom()) +, _action(action) { + + initResizeHook(parent->sizeValue()); + paintRequest( + ) | rpl::start_with_next([=] { + Painter p(this); + + p.fillRect(0, 0, width(), _height, _bg); + p.fillRect( + _padding.left(), + _padding.top(), + width() - _padding.left() - _padding.right(), + _lineWidth, + _fg); + }, lifetime()); +} + +not_null Separator::action() const { + return _action; +} + +bool Separator::isEnabled() const { + return false; +} + +int Separator::contentHeight() const { + return _height; +} + +} // namespace Ui::Menu diff --git a/ui/widgets/menu/menu_separator.h b/ui/widgets/menu/menu_separator.h new file mode 100644 index 0000000..1ff5f6f --- /dev/null +++ b/ui/widgets/menu/menu_separator.h @@ -0,0 +1,39 @@ +// 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 +// +#pragma once + +#include "ui/widgets/menu/menu_item_base.h" +#include "styles/style_widgets.h" + +class Painter; + +namespace Ui::Menu { + +class Separator : public ItemBase { +public: + Separator( + not_null parent, + const style::Menu &st, + not_null action); + + not_null action() const override; + bool isEnabled() const override; + +protected: + int contentHeight() const override; + +private: + const int _lineWidth; + const style::margins &_padding; + const style::color &_fg; + const style::color &_bg; + const int _height; + const not_null _action; + +}; + +} // namespace Ui::Menu diff --git a/ui/widgets/menu/menu_toggle.cpp b/ui/widgets/menu/menu_toggle.cpp new file mode 100644 index 0000000..eedc7bc --- /dev/null +++ b/ui/widgets/menu/menu_toggle.cpp @@ -0,0 +1,77 @@ +// 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/widgets/menu/menu_toggle.h" + +#include "ui/widgets/checkbox.h" + +namespace Ui::Menu { + +Toggle::Toggle( + not_null parent, + const style::Menu &st, + const QString &text, + Fn &&callback, + const style::icon *icon, + const style::icon *iconOver) +: Action( + parent, + st, + CreateAction(parent, text, std::move(callback)), + icon, + iconOver) +, _padding(st.itemPadding) +, _toggleShift(st.itemToggleShift) +, _itemToggle(st.itemToggle) +, _itemToggleOver(st.itemToggleOver) { + + const auto processAction = [=] { + if (!action()->isCheckable()) { + _toggle.reset(); + return; + } + if (_toggle) { + _toggle->setChecked(action()->isChecked(), anim::type::normal); + } else { + _toggle = std::make_unique( + st.itemToggle, + action()->isChecked(), + [=] { update(); }); + } + }; + processAction(); + connect(action(), &QAction::changed, [=] { processAction(); }); + + selects( + ) | rpl::start_with_next([=](const CallbackData &data) { + if (!_toggle) { + return; + } + _toggle->setStyle(data.selected ? _itemToggleOver : _itemToggle); + }, lifetime()); + +} + +void Toggle::paintEvent(QPaintEvent *e) { + Action::paintEvent(e); + if (_toggle) { + Painter p(this); + const auto toggleSize = _toggle->getSize(); + _toggle->paint( + p, + width() - _padding.right() - toggleSize.width() + _toggleShift, + (contentHeight() - toggleSize.height()) / 2, width()); + } +} + +void Toggle::finishAnimating() { + ItemBase::finishAnimating(); + if (_toggle) { + _toggle->finishAnimating(); + } +} + +} // namespace Ui::Menu diff --git a/ui/widgets/menu/menu_toggle.h b/ui/widgets/menu/menu_toggle.h new file mode 100644 index 0000000..7806858 --- /dev/null +++ b/ui/widgets/menu/menu_toggle.h @@ -0,0 +1,42 @@ +// 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 +// +#pragma once + +#include "ui/widgets/menu/menu_action.h" +#include "styles/style_widgets.h" + +namespace Ui { +class ToggleView; +} // namespace Ui + +namespace Ui::Menu { + +class Toggle : public Action { +public: + Toggle( + not_null parent, + const style::Menu &st, + const QString &text, + Fn &&callback, + const style::icon *icon, + const style::icon *iconOver); + + void finishAnimating() override; + +protected: + void paintEvent(QPaintEvent *e) override; + +private: + const style::margins &_padding; + const int _toggleShift; + const style::Toggle &_itemToggle; + const style::Toggle &_itemToggleOver; + std::unique_ptr _toggle; + +}; + +} // namespace Ui::Menu diff --git a/ui/widgets/popup_menu.cpp b/ui/widgets/popup_menu.cpp index de300ec..38d0acc 100644 --- a/ui/widgets/popup_menu.cpp +++ b/ui/widgets/popup_menu.cpp @@ -52,12 +52,15 @@ void PopupMenu::init() { hideMenu(true); }, lifetime()); - _menu->setResizedCallback([this] { handleMenuResize(); }); - _menu->setActivatedCallback([this](QAction *action, int actionTop, TriggeredSource source) { - handleActivated(action, actionTop, source); + _menu->resizesFromInner( + ) | rpl::start_with_next([=] { + handleMenuResize(); + }, _menu->lifetime()); + _menu->setActivatedCallback([this](const Menu::CallbackData &data) { + handleActivated(data); }); - _menu->setTriggeredCallback([this](QAction *action, int actionTop, TriggeredSource source) { - handleTriggered(action, actionTop, source); + _menu->setTriggeredCallback([this](const Menu::CallbackData &data) { + handleTriggered(data); }); _menu->setKeyPressDelegate([this](int key) { return handleKeyPress(key); }); _menu->setMouseMoveDelegate([this](QPoint globalPosition) { handleMouseMove(globalPosition); }); @@ -88,8 +91,9 @@ void PopupMenu::handleMenuResize() { _inner = rect().marginsRemoved(_padding); } -not_null PopupMenu::addAction(const QString &text, const QObject *receiver, const char* member, const style::icon *icon, const style::icon *iconOver) { - return _menu->addAction(text, receiver, member, icon, iconOver); +not_null PopupMenu::addAction( + base::unique_qptr widget) { + return _menu->addAction(std::move(widget)); } not_null PopupMenu::addAction(const QString &text, Fn callback, const style::icon *icon, const style::icon *iconOver) { @@ -119,6 +123,10 @@ const std::vector> &PopupMenu::actions() const { return _menu->actions(); } +bool PopupMenu::empty() const { + return _menu->empty(); +} + void PopupMenu::paintEvent(QPaintEvent *e) { QPainter p(this); @@ -153,9 +161,9 @@ void PopupMenu::paintBg(QPainter &p) { } } -void PopupMenu::handleActivated(QAction *action, int actionTop, TriggeredSource source) { - if (source == TriggeredSource::Mouse) { - if (!popupSubmenuFromAction(action, actionTop, source)) { +void PopupMenu::handleActivated(const Menu::CallbackData &data) { + if (data.source == TriggeredSource::Mouse) { + if (!popupSubmenuFromAction(data)) { if (auto currentSubmenu = base::take(_activeSubmenu)) { currentSubmenu->hideMenu(true); } @@ -163,11 +171,11 @@ void PopupMenu::handleActivated(QAction *action, int actionTop, TriggeredSource } } -void PopupMenu::handleTriggered(QAction *action, int actionTop, TriggeredSource source) { - if (!popupSubmenuFromAction(action, actionTop, source)) { +void PopupMenu::handleTriggered(const Menu::CallbackData &data) { + if (!popupSubmenuFromAction(data)) { _triggering = true; hideMenu(); - emit action->trigger(); + emit data.action->trigger(); _triggering = false; if (_deleteLater) { _deleteLater = false; @@ -176,12 +184,12 @@ void PopupMenu::handleTriggered(QAction *action, int actionTop, TriggeredSource } } -bool PopupMenu::popupSubmenuFromAction(QAction *action, int actionTop, TriggeredSource source) { - if (auto submenu = _submenus.value(action)) { +bool PopupMenu::popupSubmenuFromAction(const Menu::CallbackData &data) { + if (auto submenu = _submenus.value(data.action)) { if (_activeSubmenu == submenu) { submenu->hideMenu(true); } else { - popupSubmenu(submenu, actionTop, source); + popupSubmenu(submenu, data.actionTop, data.source); } return true; } @@ -203,9 +211,9 @@ void PopupMenu::popupSubmenu(SubmenuPointer submenu, int actionTop, TriggeredSou } } -void PopupMenu::forwardKeyPress(int key) { - if (!handleKeyPress(key)) { - _menu->handleKeyPress(key); +void PopupMenu::forwardKeyPress(not_null e) { + if (!handleKeyPress(e->key())) { + _menu->handleKeyPress(e); } } @@ -262,7 +270,7 @@ void PopupMenu::hideEvent(QHideEvent *e) { } void PopupMenu::keyPressEvent(QKeyEvent *e) { - forwardKeyPress(e->key()); + forwardKeyPress(e); } void PopupMenu::mouseMoveEvent(QMouseEvent *e) { diff --git a/ui/widgets/popup_menu.h b/ui/widgets/popup_menu.h index ca08f98..3b86873 100644 --- a/ui/widgets/popup_menu.h +++ b/ui/widgets/popup_menu.h @@ -7,7 +7,7 @@ #pragma once #include "styles/style_widgets.h" -#include "ui/widgets/menu.h" +#include "ui/widgets/menu/menu.h" #include "ui/effects/animations.h" #include "ui/effects/panel_animation.h" #include "ui/round_rect.h" @@ -21,13 +21,18 @@ public: PopupMenu(QWidget *parent, const style::PopupMenu &st = st::defaultPopupMenu); PopupMenu(QWidget *parent, QMenu *menu, const style::PopupMenu &st = st::defaultPopupMenu); - not_null addAction(const QString &text, const QObject *receiver, const char* member, const style::icon *icon = nullptr, const style::icon *iconOver = nullptr); + [[nodiscard]] const style::PopupMenu &st() const { + return _st; + } + + not_null addAction(base::unique_qptr widget); not_null addAction(const QString &text, Fn callback, const style::icon *icon = nullptr, const style::icon *iconOver = nullptr); not_null addAction(const QString &text, std::unique_ptr submenu); not_null addSeparator(); void clearActions(); const std::vector> &actions() const; + bool empty() const; void deleteOnHide(bool del); void popup(const QPoint &p); @@ -41,6 +46,10 @@ public: _reactivateParent = false; } + [[nodiscard]] not_null menu() const { + return _menu.data(); + } + ~PopupMenu(); protected: @@ -75,9 +84,9 @@ private: using TriggeredSource = Menu::TriggeredSource; void handleCompositingUpdate(); void handleMenuResize(); - void handleActivated(QAction *action, int actionTop, TriggeredSource source); - void handleTriggered(QAction *action, int actionTop, TriggeredSource source); - void forwardKeyPress(int key); + void handleActivated(const Menu::CallbackData &data); + void handleTriggered(const Menu::CallbackData &data); + void forwardKeyPress(not_null e); bool handleKeyPress(int key); void forwardMouseMove(QPoint globalPosition) { _menu->handleMouseMove(globalPosition); @@ -93,14 +102,14 @@ private: void handleMouseRelease(QPoint globalPosition); using SubmenuPointer = QPointer; - bool popupSubmenuFromAction(QAction *action, int actionTop, TriggeredSource source); + bool popupSubmenuFromAction(const Menu::CallbackData &data); void popupSubmenu(SubmenuPointer submenu, int actionTop, TriggeredSource source); void showMenu(const QPoint &p, PopupMenu *parent, TriggeredSource source); const style::PopupMenu &_st; RoundRect _roundRect; - object_ptr _menu; + object_ptr _menu; using Submenus = QMap; Submenus _submenus; diff --git a/ui/widgets/widgets.style b/ui/widgets/widgets.style index 737e70f..ee44c39 100644 --- a/ui/widgets/widgets.style +++ b/ui/widgets/widgets.style @@ -93,6 +93,7 @@ RoundButton { textTop: pixels; icon: icon; + iconOver: icon; iconPosition: point; font: font; @@ -135,6 +136,7 @@ Radio { toggledFg: color; diameter: pixels; thickness: pixels; + outerSkip: pixels; skip: pixels; duration: int; rippleAreaPadding: pixels; @@ -408,6 +410,18 @@ CrossLineAnimation { stroke: pixels; } +ArcsAnimation { + fg: color; + stroke: pixels; + space: pixels; + duration: int; + deltaAngle: int; + deltaHeight: pixels; + deltaWidth: pixels; + startHeight: pixels; + startWidth: pixels; +} + MultiSelectItem { padding: margins; maxWidth: pixels; @@ -741,7 +755,8 @@ defaultRadio: Radio { toggledFg: windowBgActive; diameter: 22px; thickness: 2px; - skip: 65px; // * 0.1 + outerSkip: 10px; // * 0.1 + skip: 60px; // * 0.1 duration: 120; rippleAreaPadding: 8px; } @@ -1569,3 +1584,17 @@ defaultWindowTitle: WindowTitle { windowShadow: icon {{ "window_shadow", windowShadowFg }}; windowShadowShift: 1px; + +callRadius: 6px; +callShadow: Shadow { + left: icon {{ "calls/call_shadow_left", windowShadowFg }}; + topLeft: icon {{ "calls/call_shadow_top_left", windowShadowFg }}; + top: icon {{ "calls/call_shadow_top", windowShadowFg }}; + topRight: icon {{ "calls/call_shadow_top_left-flip_horizontal", windowShadowFg }}; + right: icon {{ "calls/call_shadow_left-flip_horizontal", windowShadowFg }}; + bottomRight: icon {{ "calls/call_shadow_top_left-flip_vertical-flip_horizontal", windowShadowFg }}; + bottom: icon {{ "calls/call_shadow_top-flip_vertical", windowShadowFg }}; + bottomLeft: icon {{ "calls/call_shadow_top_left-flip_vertical", windowShadowFg }}; + extend: margins(9px, 8px, 9px, 10px); + fallback: windowShadowFgFallback; +}