Updated lib_ui sources to TDesktop version 2.6

This commit is contained in:
Eric Kotato 2021-02-24 08:07:24 +03:00
commit f04ec94710
64 changed files with 2982 additions and 915 deletions

View file

@ -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}

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 559 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 927 B

View file

@ -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

View file

@ -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

View file

@ -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;
};

233
ui/paint/arcs.cpp Normal file
View file

@ -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<float> 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<float> 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<QColor> 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

85
ui/paint/arcs.h Normal file
View file

@ -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<float> thresholds,
float64 startValue,
Direction direction);
void paint(
Painter &p,
std::optional<QColor> 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<float> 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<Arc> _arcs;
};
} // namespace Ui::Paint

View file

@ -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) {

View file

@ -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 {

View file

@ -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<BlobData> 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 {

View file

@ -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<BlobData> 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);
}
}

View file

@ -0,0 +1,46 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "ui/platform/linux/ui_linux_wayland_integration.h"
#include "base/platform/base_platform_info.h"
#include <QtGui/QWindow>
#include <private/qwaylanddisplay_p.h>
#include <private/qwaylandwindow_p.h>
#include <private/qwaylandshellsurface_p.h>
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<QWaylandWindow*>(
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

View file

@ -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

View file

@ -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

View file

@ -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 <QtCore/QPoint>
#include <QtGui/QScreen>
#include <QtGui/QWindow>
#include <QtWidgets/QApplication>
#include <qpa/qplatformnativeinterface.h>
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>{
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<const char*>(&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<QWidget*> 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<QMargins>(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<TitleControls::Layout> {
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<TitleControls::Control> controlsLeft;
ranges::transform(
decorationLayout[0].split(','),
ranges::back_inserter(controlsLeft),
GtkKeywordToTitleControl);
std::vector<TitleControls::Control> 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

View file

@ -6,6 +6,8 @@
//
#pragma once
#include "ui/platform/ui_platform_utility.h"
class QPainter;
class QPaintEvent;

View file

@ -6,6 +6,7 @@
//
#pragma once
#include "ui/platform/ui_platform_utility.h"
#include "base/platform/base_platform_info.h"
#include <QtCore/QPoint>
@ -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

View file

@ -99,5 +99,15 @@ void DrainMainQueue() {
void IgnoreAllActivation(not_null<QWidget*> widget) {
}
TitleControls::Layout TitleControlsLayout() {
return TitleControls::Layout{
.left = {
TitleControls::Control::Close,
TitleControls::Control::Minimize,
TitleControls::Control::Maximize,
}
};
}
} // namespace Platform
} // namespace Ui

View file

@ -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

View file

@ -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<QWidget*> 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

View file

@ -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 <QtCore/QCoreApplication>
#include <QtGui/QWindow>
#include <QtGui/QtEvents>
namespace Ui {
namespace Platform {
namespace {
[[nodiscard]] const style::Shadow &Shadow() {
return st::callShadow;
}
} // namespace
BasicWindowHelper::BasicWindowHelper(not_null<RpWidget*> 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<RpWidget*> BasicWindowHelper::body() {
@ -100,8 +110,6 @@ void BasicWindowHelper::setupBodyTitleAreaEvents() {
&& (static_cast<QMouseEvent*>(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<QMouseEvent*>(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<RpWidget*> window)
: BasicWindowHelper(window)
, _title(Ui::CreateChild<DefaultTitleWidget>(window.get()))
, _body(Ui::CreateChild<RpWidget>(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<QEvent*> e) {
if (e->type() == QEvent::MouseButtonPress) {
const auto mouseEvent = static_cast<QMouseEvent*>(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<RpWidget*> 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<QWidget*>(window()) == static_cast<QWidget*>(obj)) {
const auto mouseEvent = static_cast<QMouseEvent*>(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

View file

@ -20,6 +20,8 @@ using WindowTitleHitTestFlags = base::flags<WindowTitleHitTestFlag>;
namespace Platform {
class DefaultTitleWidget;
class BasicWindowHelper {
public:
explicit BasicWindowHelper(not_null<RpWidget*> window);
@ -57,6 +59,35 @@ private:
};
class DefaultWindowHelper final : public QObject, public BasicWindowHelper {
public:
explicit DefaultWindowHelper(not_null<RpWidget*> window);
not_null<RpWidget*> 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<DefaultTitleWidget*> _title;
const not_null<RpWidget*> _body;
bool _extentsSet = false;
};
[[nodiscard]] std::unique_ptr<BasicWindowHelper> CreateSpecialWindowHelper(
not_null<RpWidget*> window);
@ -65,7 +96,7 @@ private:
if (auto special = CreateSpecialWindowHelper(window)) {
return special;
}
return std::make_unique<BasicWindowHelper>(window);
return std::make_unique<DefaultWindowHelper>(window);
}
} // namespace Platform

View file

@ -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 <QtGui/QPainter>
#include <QtGui/QtEvents>
#include <QtGui/QWindow>
namespace Ui {
namespace Platform {
namespace {
template <typename T>
void RemoveDuplicates(std::vector<T> &v) {
auto end = v.end();
for (auto it = v.begin(); it != end; ++it) {
end = std::remove(it + 1, end, *it);
}
v.erase(end, v.end());
}
} // namespace
TitleControls::TitleControls(
not_null<RpWidget*> parent,
const style::WindowTitle &st,
Fn<void(bool maximized)> maximize)
: _st(&st)
, _minimize(parent, _st->minimize)
, _maximizeRestore(parent, _st->maximize)
, _close(parent, _st->close)
, _maximizedState(parent->windowState()
& (Qt::WindowMaximized | Qt::WindowFullScreen))
, _activeState(parent->isActiveWindow()) {
init(std::move(maximize));
_close->paintRequest(
) | rpl::start_with_next([=] {
const auto active = window()->isActiveWindow();
if (_activeState != active) {
_activeState = active;
updateButtonsState();
}
}, _close->lifetime());
}
void TitleControls::setStyle(const style::WindowTitle &st) {
_st = &st;
updateButtonsState();
}
not_null<const style::WindowTitle*> TitleControls::st() const {
return _st;
}
QRect TitleControls::geometry() const {
auto result = QRect();
const auto add = [&](auto &&control) {
if (!control->isHidden()) {
result = result.united(control->geometry());
}
};
add(_minimize);
add(_maximizeRestore);
add(_close);
return result;
}
not_null<RpWidget*> TitleControls::parent() const {
return static_cast<RpWidget*>(_close->parentWidget());
}
not_null<QWidget*> TitleControls::window() const {
return _close->window();
}
void TitleControls::init(Fn<void(bool maximized)> maximize) {
_minimize->setClickedCallback([=] {
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<QObject*>(nullptr);
*winIdEventFilter = base::install_event_filter(
window(),
[=](not_null<QEvent*> 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<Control> &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<RpWidget*> parent)
: RpWidget(parent)
, _controls(this, st::defaultWindowTitle)
, _shadow(this, st::titleShadow) {
setAttribute(Qt::WA_OpaquePaintEvent);
}
not_null<const style::WindowTitle*> DefaultTitleWidget::st() const {
return _controls.st();
}
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

View file

@ -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 <QtCore/QRect>
#include <QtCore/QPoint>
namespace style {
struct WindowTitle;
} // namespace style
namespace Ui {
class IconButton;
class PlainShadow;
namespace Platform {
class TitleControls final {
public:
TitleControls(
not_null<RpWidget*> parent,
const style::WindowTitle &st,
Fn<void(bool maximized)> maximize = nullptr);
void setStyle(const style::WindowTitle &st);
[[nodiscard]] not_null<const style::WindowTitle*> st() const;
[[nodiscard]] QRect geometry() const;
void setResizeEnabled(bool enabled);
void raise();
enum class Control {
Unknown,
Minimize,
Maximize,
Close,
};
struct Layout {
std::vector<Control> left;
std::vector<Control> right;
};
private:
[[nodiscard]] not_null<RpWidget*> parent() const;
[[nodiscard]] not_null<QWidget*> window() const;
[[nodiscard]] Ui::IconButton *controlWidget(Control control) const;
void init(Fn<void(bool maximized)> maximize);
void updateButtonsState();
void updateControlsPosition();
void updateControlsPositionBySide(
const std::vector<Control> &controls,
bool right);
void handleWindowStateChanged(Qt::WindowState state = Qt::WindowNoState);
not_null<const style::WindowTitle*> _st;
object_ptr<Ui::IconButton> _minimize;
object_ptr<Ui::IconButton> _maximizeRestore;
object_ptr<Ui::IconButton> _close;
bool _maximizedState = false;
bool _activeState = false;
bool _resizeEnabled = true;
};
class DefaultTitleWidget : public RpWidget {
public:
explicit DefaultTitleWidget(not_null<RpWidget*> parent);
[[nodiscard]] not_null<const style::WindowTitle*> 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<Ui::PlainShadow> _shadow;
bool _mousePressed = false;
};
} // namespace Platform
} // namespace Ui

View file

@ -6,10 +6,11 @@
//
#include "ui/platform/win/ui_utility_win.h"
#include <QtWidgets/QApplication>
#include "base/platform/win/base_windows_h.h"
#include <QtWidgets/QApplication>
#include <QtGui/QWindow>
namespace Ui {
namespace Platform {
@ -44,5 +45,27 @@ void IgnoreAllActivation(not_null<QWidget*> 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

View file

@ -6,6 +6,8 @@
//
#pragma once
#include "ui/platform/ui_platform_utility.h"
#include <QtCore/QPoint>
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

View file

@ -19,170 +19,6 @@
namespace Ui {
namespace Platform {
TitleControls::TitleControls(
not_null<RpWidget*> parent,
const style::WindowTitle &st,
Fn<void(bool maximized)> maximize)
: _st(&st)
, _minimize(parent, _st->minimize)
, _maximizeRestore(parent, _st->maximize)
, _close(parent, _st->close)
, _maximizedState(parent->windowState()
& (Qt::WindowMaximized | Qt::WindowFullScreen))
, _activeState(parent->isActiveWindow()) {
init(std::move(maximize));
_close->paintRequest(
) | rpl::start_with_next([=] {
const auto active = window()->isActiveWindow();
if (_activeState != active) {
_activeState = active;
updateButtonsState();
}
}, _close->lifetime());
}
void TitleControls::setStyle(const style::WindowTitle &st) {
_st = &st;
updateButtonsState();
}
not_null<const style::WindowTitle*> TitleControls::st() const {
return _st;
}
QRect TitleControls::geometry() const {
auto result = QRect();
const auto add = [&](auto &&control) {
if (!control->isHidden()) {
result = result.united(control->geometry());
}
};
add(_minimize);
add(_maximizeRestore);
add(_close);
return result;
}
not_null<RpWidget*> TitleControls::parent() const {
return static_cast<RpWidget*>(_close->parentWidget());
}
not_null<QWidget*> TitleControls::window() const {
return _close->window();
}
void TitleControls::init(Fn<void(bool maximized)> maximize) {
_minimize->setClickedCallback([=] {
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<RpWidget*> parent)
: RpWidget(parent)
, _controls(this, st::defaultWindowTitle)

View file

@ -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<RpWidget*> parent,
const style::WindowTitle &st,
Fn<void(bool maximized)> maximize = nullptr);
void setStyle(const style::WindowTitle &st);
[[nodiscard]] not_null<const style::WindowTitle*> st() const;
[[nodiscard]] QRect geometry() const;
void setResizeEnabled(bool enabled);
void raise();
private:
[[nodiscard]] not_null<RpWidget*> parent() const;
[[nodiscard]] not_null<QWidget*> window() const;
void init(Fn<void(bool maximized)> maximize);
void updateControlsVisibility();
void updateButtonsState();
void updateControlsPosition();
void handleWindowStateChanged(Qt::WindowState state = Qt::WindowNoState);
not_null<const style::WindowTitle*> _st;
object_ptr<Ui::IconButton> _minimize;
object_ptr<Ui::IconButton> _maximizeRestore;
object_ptr<Ui::IconButton> _close;
bool _maximizedState = false;
bool _activeState = false;
bool _resizeEnabled = true;
};
class TitleWidget : public RpWidget {
public:
explicit TitleWidget(not_null<RpWidget*> parent);

View file

@ -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) {

View file

@ -243,7 +243,7 @@ using RpWidgetParent = std::conditional_t<
TWidget,
TWidgetHelper<Widget>>;
template <typename Widget>
template <typename Widget, typename Traits>
class RpWidgetWrap;
class RpWidgetMethods {
@ -281,7 +281,7 @@ protected:
virtual bool eventHook(QEvent *event) = 0;
private:
template <typename Widget>
template <typename Widget, typename Traits>
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 <typename Widget>
struct RpWidgetDefaultTraits {
static constexpr bool kSetZeroGeometry = true;
};
template <typename Widget, typename Traits = RpWidgetDefaultTraits>
class RpWidgetWrap
: public RpWidgetParent<Widget>
, public RpWidgetMethods {
using Self = RpWidgetWrap<Widget>;
using Self = RpWidgetWrap<Widget, Traits>;
using Parent = RpWidgetParent<Widget>;
public:
@ -362,7 +366,7 @@ private:
return this->isHidden();
}
Initer _initer = { this };
Initer _initer = { this, Traits::kSetZeroGeometry };
};

View file

@ -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<QColor> 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 &current = ((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()) {

View file

@ -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<QColor> color);
void setTextMargins(QMargins margins);
int32 textWidth() const;
@ -97,6 +108,7 @@ private:
QString _text;
QMargins _textMargins;
int _width = 0;
std::optional<QColor> _colorOverride;
const style::FlatButton &_st;

View file

@ -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) {

View file

@ -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<InfiniteRadialAnimation>(
[=] { _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)

View file

@ -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);

View file

@ -12,7 +12,7 @@ namespace Ui {
DropdownMenu::DropdownMenu(QWidget *parent, const style::DropdownMenu &st) : InnerDropdown(parent, st.wrap)
, _st(st) {
_menu = setOwnedWidget(object_ptr<Menu>(this, _st.menu));
_menu = setOwnedWidget(object_ptr<Menu::Menu>(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<QAction*> 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<QAction*> DropdownMenu::addAction(
base::unique_qptr<Menu::ItemBase> widget) {
return _menu->addAction(std::move(widget));
}
not_null<QAction*> DropdownMenu::addAction(const QString &text, Fn<void()> callback, const style::icon *icon, const style::icon *iconOver) {
@ -73,9 +77,13 @@ const std::vector<not_null<QAction*>> &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<QKeyEvent*> 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) {

View file

@ -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<QAction*> addAction(const QString &text, const QObject *receiver, const char* member, const style::icon *icon = nullptr, const style::icon *iconOver = nullptr);
not_null<QAction*> addAction(base::unique_qptr<Menu::ItemBase> widget);
not_null<QAction*> addAction(const QString &text, Fn<void()> callback, const style::icon *icon = nullptr, const style::icon *iconOver = nullptr);
not_null<QAction*> addSeparator();
void clearActions();
@ -28,6 +28,7 @@ public:
}
const std::vector<not_null<QAction*>> &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<QKeyEvent*> e);
bool handleKeyPress(int key);
void forwardMouseMove(QPoint globalPosition) {
_menu->handleMouseMove(globalPosition);
@ -74,14 +75,14 @@ private:
void handleMouseRelease(QPoint globalPosition);
using SubmenuPointer = QPointer<DropdownMenu>;
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<void()> _hiddenCallback;
QPointer<Menu> _menu;
QPointer<Menu::Menu> _menu;
// Not ready with submenus yet.
//using Submenus = QMap<QAction*, SubmenuPointer>;

View file

@ -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<QString> 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;
}

View file

@ -697,9 +697,14 @@ public:
};
class PortInput : public MaskedInputField {
class NumberInput : public MaskedInputField {
public:
PortInput(QWidget *parent, const style::InputField &st, rpl::producer<QString> placeholder, const QString &val);
NumberInput(
QWidget *parent,
const style::InputField &st,
rpl::producer<QString> 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 {

View file

@ -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()) {

View file

@ -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 <QtGui/QtEvents>
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<RippleAnimation> ripple;
std::unique_ptr<ToggleView> 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<QAction*> 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<QAction*> Menu::addAction(const QString &text, Fn<void()> 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<QAction*> Menu::addAction(const QString &text, std::unique_ptr<QMenu> submenu) {
const auto action = new QAction(text, this);
action->setMenu(submenu.release());
return addAction(action, nullptr, nullptr);
}
not_null<QAction*> Menu::addAction(not_null<QAction*> 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<QAction*> 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<QAction*> 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<ToggleView>(_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<not_null<QAction*>> &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<RippleAnimation>(_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

347
ui/widgets/menu/menu.cpp Normal file
View file

@ -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 <QtGui/QtEvents>
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<QAction*> Menu::addAction(
const QString &text,
Fn<void()> 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<QAction*> Menu::addAction(
const QString &text,
std::unique_ptr<QMenu> submenu) {
const auto action = new QAction(text, this);
action->setMenu(submenu.release());
return addAction(action, nullptr, nullptr);
}
not_null<QAction*> Menu::addAction(
not_null<QAction*> action,
const style::icon *icon,
const style::icon *iconOver) {
if (action->isSeparator()) {
return addSeparator();
}
auto item = base::make_unique_q<Action>(
this,
_st,
std::move(action),
icon,
iconOver ? iconOver : icon);
return addAction(std::move(item));
}
not_null<QAction*> Menu::addAction(base::unique_qptr<ItemBase> 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<QAction*> Menu::addSeparator() {
const auto separator = new QAction(this);
separator->setSeparator(true);
auto item = base::make_unique_q<Separator>(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<not_null<QAction*>> &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<QKeyEvent*> 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

View file

@ -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 <QtWidgets/QMenu>
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<QAction*> addAction(const QString &text, const QObject *receiver, const char* member, const style::icon *icon = nullptr, const style::icon *iconOver = nullptr);
not_null<QAction*> addAction(const QString &text, Fn<void()> callback, const style::icon *icon = nullptr, const style::icon *iconOver = nullptr);
not_null<QAction*> addAction(const QString &text, std::unique_ptr<QMenu> submenu);
[[nodiscard]] const style::Menu &st() const {
return _st;
}
not_null<QAction*> addAction(base::unique_qptr<ItemBase> widget);
not_null<QAction*> addAction(
const QString &text,
Fn<void()> callback,
const style::icon *icon = nullptr,
const style::icon *iconOver = nullptr);
not_null<QAction*> addAction(
const QString &text,
std::unique_ptr<QMenu> submenu);
not_null<QAction*> 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<not_null<QAction*>> &actions() const;
void setResizedCallback(Fn<void()> callback) {
_resizedCallback = std::move(callback);
}
void setActivatedCallback(Fn<void(QAction *action, int actionTop, TriggeredSource source)> callback) {
void setActivatedCallback(Fn<void(const CallbackData &data)> callback) {
_activatedCallback = std::move(callback);
}
void setTriggeredCallback(Fn<void(QAction *action, int actionTop, TriggeredSource source)> callback) {
void setTriggeredCallback(Fn<void(const CallbackData &data)> callback) {
_triggeredCallback = std::move(callback);
}
void setKeyPressDelegate(Fn<bool(int key)> delegate) {
_keyPressDelegate = std::move(delegate);
}
void handleKeyPress(int key);
void handleKeyPress(not_null<QKeyEvent*> e);
void setMouseMoveDelegate(Fn<void(QPoint globalPosition)> 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<QAction*> action, int index, int width);
not_null<QAction*> addAction(not_null<QAction*> action, const style::icon *icon = nullptr, const style::icon *iconOver = nullptr);
not_null<QAction*> addAction(
not_null<QAction*> 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<void()> _resizedCallback;
Fn<void(QAction *action, int actionTop, TriggeredSource source)> _activatedCallback;
Fn<void(QAction *action, int actionTop, TriggeredSource source)> _triggeredCallback;
Fn<void(const CallbackData &data)> _activatedCallback;
Fn<void(const CallbackData &data)> _triggeredCallback;
Fn<bool(int key)> _keyPressDelegate;
Fn<void(QPoint globalPosition)> _mouseMoveDelegate;
Fn<void(QPoint globalPosition)> _mousePressDelegate;
@ -116,17 +117,14 @@ private:
QMenu *_wappedMenu = nullptr;
std::vector<not_null<QAction*>> _actions;
std::vector<ActionData> _actionsData;
std::vector<base::unique_qptr<ItemBase>> _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

View file

@ -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 <QtGui/QtEvents>
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<RpWidget*> parent,
const style::Menu &st,
not_null<QAction*> 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<QAction*> 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<QKeyEvent*> 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

View file

@ -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<RpWidget*> parent,
const style::Menu &st,
not_null<QAction*> action,
const style::icon *icon,
const style::icon *iconOver);
bool isEnabled() const override;
not_null<QAction*> action() const override;
void handleKeyPress(not_null<QKeyEvent*> 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<QAction*> _action;
const style::Menu &_st;
const style::icon *_icon;
const style::icon *_iconOver;
// std::unique_ptr<ToggleView> _toggle;
int _textWidth = 0;
const int _height;
};
} // namespace Ui::Menu

View file

@ -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 <QtWidgets/QAction>
namespace Ui::Menu {
not_null<QAction*> CreateAction(
QWidget *parent,
const QString &text,
Fn<void()> &&callback) {
const auto action = new QAction(text, parent);
parent->connect(
action,
&QAction::triggered,
action,
std::move(callback),
Qt::QueuedConnection);
return action;
}
} // namespace Ui::Menu

View file

@ -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<QAction*> CreateAction(
QWidget *parent,
const QString &text,
Fn<void()> &&callback);
} // namespace Ui::Menu

View file

@ -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<RpWidget*> 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<CallbackData> 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<CallbackData> 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<int> ItemBase::minWidthValue() const {
return _minWidth.value();
}
int ItemBase::minWidth() const {
return _minWidth.current();
}
void ItemBase::initResizeHook(rpl::producer<QSize> &&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<RpWidget*> widget) {
widget->events(
) | rpl::start_with_next([=](not_null<QEvent*> 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

View file

@ -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<RpWidget*> parent, const style::Menu &st);
TriggeredSource lastTriggeredSource() const;
rpl::producer<CallbackData> 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<CallbackData> clicks() const;
rpl::producer<int> minWidthValue() const;
int minWidth() const;
void setMinWidth(int w);
virtual void handleKeyPress(not_null<QKeyEvent*> e) {
}
virtual not_null<QAction*> action() const = 0;
virtual bool isEnabled() const = 0;
virtual void finishAnimating();
protected:
void initResizeHook(rpl::producer<QSize> &&size);
void enableMouseSelecting();
void enableMouseSelecting(not_null<RpWidget*> widget);
virtual int contentHeight() const = 0;
private:
int _index = -1;
rpl::variable<bool> _selected = false;
rpl::event_stream<> _clicks;
rpl::variable<int> _minWidth = 0;
TriggeredSource _lastTriggeredSource = TriggeredSource::Mouse;
};
} // namespace Ui::Menu

View file

@ -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<RpWidget*> parent,
const style::Menu &st,
not_null<QAction*> 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<QAction*> Separator::action() const {
return _action;
}
bool Separator::isEnabled() const {
return false;
}
int Separator::contentHeight() const {
return _height;
}
} // namespace Ui::Menu

View file

@ -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<RpWidget*> parent,
const style::Menu &st,
not_null<QAction*> action);
not_null<QAction*> 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<QAction*> _action;
};
} // namespace Ui::Menu

View file

@ -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<RpWidget*> parent,
const style::Menu &st,
const QString &text,
Fn<void()> &&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<ToggleView>(
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

View file

@ -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<RpWidget*> parent,
const style::Menu &st,
const QString &text,
Fn<void()> &&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<ToggleView> _toggle;
};
} // namespace Ui::Menu

View file

@ -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<QAction*> 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<QAction*> PopupMenu::addAction(
base::unique_qptr<Menu::ItemBase> widget) {
return _menu->addAction(std::move(widget));
}
not_null<QAction*> PopupMenu::addAction(const QString &text, Fn<void()> callback, const style::icon *icon, const style::icon *iconOver) {
@ -119,6 +123,10 @@ const std::vector<not_null<QAction*>> &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<QKeyEvent*> 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) {

View file

@ -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<QAction*> 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<QAction*> addAction(base::unique_qptr<Menu::ItemBase> widget);
not_null<QAction*> addAction(const QString &text, Fn<void()> callback, const style::icon *icon = nullptr, const style::icon *iconOver = nullptr);
not_null<QAction*> addAction(const QString &text, std::unique_ptr<PopupMenu> submenu);
not_null<QAction*> addSeparator();
void clearActions();
const std::vector<not_null<QAction*>> &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::Menu*> 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<QKeyEvent*> e);
bool handleKeyPress(int key);
void forwardMouseMove(QPoint globalPosition) {
_menu->handleMouseMove(globalPosition);
@ -93,14 +102,14 @@ private:
void handleMouseRelease(QPoint globalPosition);
using SubmenuPointer = QPointer<PopupMenu>;
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> _menu;
object_ptr<Menu::Menu> _menu;
using Submenus = QMap<QAction*, SubmenuPointer>;
Submenus _submenus;

View file

@ -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;
}