forked from quickshell/quickshell
service/tray!: redesign menus / dbusmenu and add native menu support
Reworks dbusmenu menus to be displayable with a system context menu. Breaks the entire DBusMenu api.
This commit is contained in:
parent
c31bbea837
commit
ec362637b8
18 changed files with 898 additions and 191 deletions
|
@ -30,6 +30,8 @@ qt_add_library(quickshell-core STATIC
|
|||
elapsedtimer.cpp
|
||||
desktopentry.cpp
|
||||
objectrepeater.cpp
|
||||
platformmenu.cpp
|
||||
qsmenu.cpp
|
||||
)
|
||||
|
||||
set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}")
|
||||
|
|
|
@ -8,12 +8,17 @@
|
|||
#include <qfileinfo.h>
|
||||
#include <qfilesystemwatcher.h>
|
||||
#include <qhash.h>
|
||||
#include <qicon.h>
|
||||
#include <qiconengine.h>
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qobject.h>
|
||||
#include <qpixmap.h>
|
||||
#include <qqmlcontext.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qqmlincubator.h>
|
||||
#include <qquickimageprovider.h>
|
||||
#include <qsize.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "iconimageprovider.hpp"
|
||||
|
@ -308,6 +313,90 @@ EngineGeneration* EngineGeneration::currentGeneration() {
|
|||
} else return nullptr;
|
||||
}
|
||||
|
||||
// QMenu re-calls pixmap() every time the mouse moves so its important to cache it.
|
||||
class PixmapCacheIconEngine: public QIconEngine {
|
||||
void paint(
|
||||
QPainter* /*unused*/,
|
||||
const QRect& /*unused*/,
|
||||
QIcon::Mode /*unused*/,
|
||||
QIcon::State /*unused*/
|
||||
) override {
|
||||
qFatal(
|
||||
) << "Unexpected icon paint request bypassed pixmap method. Please report this as a bug.";
|
||||
}
|
||||
|
||||
QPixmap pixmap(const QSize& size, QIcon::Mode /*unused*/, QIcon::State /*unused*/) override {
|
||||
if (this->lastPixmap.isNull() || size != this->lastSize) {
|
||||
this->lastPixmap = this->createPixmap(size);
|
||||
this->lastSize = size;
|
||||
}
|
||||
|
||||
return this->lastPixmap;
|
||||
}
|
||||
|
||||
virtual QPixmap createPixmap(const QSize& size) = 0;
|
||||
|
||||
private:
|
||||
QSize lastSize;
|
||||
QPixmap lastPixmap;
|
||||
};
|
||||
|
||||
class ImageProviderIconEngine: public PixmapCacheIconEngine {
|
||||
public:
|
||||
explicit ImageProviderIconEngine(QQuickImageProvider* provider, QString id)
|
||||
: provider(provider)
|
||||
, id(std::move(id)) {}
|
||||
|
||||
QPixmap createPixmap(const QSize& size) override {
|
||||
if (this->provider->imageType() == QQmlImageProviderBase::Pixmap) {
|
||||
return this->provider->requestPixmap(this->id, nullptr, size);
|
||||
} else if (this->provider->imageType() == QQmlImageProviderBase::Image) {
|
||||
auto image = this->provider->requestImage(this->id, nullptr, size);
|
||||
return QPixmap::fromImage(image);
|
||||
} else {
|
||||
qFatal() << "Unexpected ImageProviderIconEngine image type" << this->provider->imageType();
|
||||
return QPixmap(); // never reached, satisfies lint
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] QIconEngine* clone() const override {
|
||||
return new ImageProviderIconEngine(this->provider, this->id);
|
||||
}
|
||||
|
||||
private:
|
||||
QQuickImageProvider* provider;
|
||||
QString id;
|
||||
};
|
||||
|
||||
QIcon EngineGeneration::iconByUrl(const QUrl& url) const {
|
||||
if (url.isEmpty()) return QIcon();
|
||||
|
||||
auto scheme = url.scheme();
|
||||
if (scheme == "image") {
|
||||
auto providerName = url.authority();
|
||||
auto path = url.path();
|
||||
if (!path.isEmpty()) path = path.sliced(1);
|
||||
|
||||
auto* provider = qobject_cast<QQuickImageProvider*>(this->engine->imageProvider(providerName));
|
||||
|
||||
if (provider == nullptr) {
|
||||
qWarning() << "iconByUrl failed: no provider found for" << url;
|
||||
return QIcon();
|
||||
}
|
||||
|
||||
if (provider->imageType() == QQmlImageProviderBase::Pixmap
|
||||
|| provider->imageType() == QQmlImageProviderBase::Image)
|
||||
{
|
||||
return QIcon(new ImageProviderIconEngine(provider, path));
|
||||
}
|
||||
|
||||
} else {
|
||||
qWarning() << "iconByUrl failed: unsupported scheme" << scheme << "in path" << url;
|
||||
}
|
||||
|
||||
return QIcon();
|
||||
}
|
||||
|
||||
EngineGeneration* EngineGeneration::findEngineGeneration(QQmlEngine* engine) {
|
||||
return g_generations.value(engine);
|
||||
}
|
||||
|
|
|
@ -3,11 +3,13 @@
|
|||
#include <qcontainerfwd.h>
|
||||
#include <qdir.h>
|
||||
#include <qfilesystemwatcher.h>
|
||||
#include <qicon.h>
|
||||
#include <qobject.h>
|
||||
#include <qpair.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qqmlincubator.h>
|
||||
#include <qtclasshelpermacros.h>
|
||||
#include <qurl.h>
|
||||
|
||||
#include "incubator.hpp"
|
||||
#include "qsintercept.hpp"
|
||||
|
@ -40,6 +42,8 @@ public:
|
|||
// otherwise null.
|
||||
static EngineGeneration* currentGeneration();
|
||||
|
||||
[[nodiscard]] QIcon iconByUrl(const QUrl& url) const;
|
||||
|
||||
RootWrapper* wrapper = nullptr;
|
||||
QDir rootPath;
|
||||
QmlScanner scanner;
|
||||
|
|
|
@ -22,5 +22,6 @@ headers = [
|
|||
"elapsedtimer.hpp",
|
||||
"desktopentry.hpp",
|
||||
"objectrepeater.hpp",
|
||||
"qsmenu.hpp"
|
||||
]
|
||||
-----
|
||||
|
|
276
src/core/platformmenu.cpp
Normal file
276
src/core/platformmenu.cpp
Normal file
|
@ -0,0 +1,276 @@
|
|||
#include "platformmenu.hpp"
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
|
||||
#include <qaction.h>
|
||||
#include <qactiongroup.h>
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qicon.h>
|
||||
#include <qlogging.h>
|
||||
#include <qmenu.h>
|
||||
#include <qnamespace.h>
|
||||
#include <qobject.h>
|
||||
#include <qpoint.h>
|
||||
#include <qqmllist.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qwindow.h>
|
||||
|
||||
#include "generation.hpp"
|
||||
#include "proxywindow.hpp"
|
||||
#include "qsmenu.hpp"
|
||||
#include "windowinterface.hpp"
|
||||
|
||||
namespace qs::menu::platform {
|
||||
|
||||
namespace {
|
||||
QVector<std::function<void(PlatformMenuQMenu*)>> CREATION_HOOKS; // NOLINT
|
||||
PlatformMenuQMenu* ACTIVE_MENU = nullptr; // NOLINT
|
||||
} // namespace
|
||||
|
||||
PlatformMenuQMenu::~PlatformMenuQMenu() {
|
||||
if (this == ACTIVE_MENU) {
|
||||
ACTIVE_MENU = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void PlatformMenuQMenu::setVisible(bool visible) {
|
||||
if (visible) {
|
||||
for (auto& hook: CREATION_HOOKS) {
|
||||
hook(this);
|
||||
}
|
||||
} else {
|
||||
if (this == ACTIVE_MENU) {
|
||||
ACTIVE_MENU = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
this->QMenu::setVisible(visible);
|
||||
}
|
||||
|
||||
PlatformMenuEntry::PlatformMenuEntry(QsMenuEntry* menu): QObject(menu), menu(menu) {
|
||||
this->relayout();
|
||||
|
||||
// clang-format off
|
||||
QObject::connect(menu, &QsMenuEntry::enabledChanged, this, &PlatformMenuEntry::onEnabledChanged);
|
||||
QObject::connect(menu, &QsMenuEntry::textChanged, this, &PlatformMenuEntry::onTextChanged);
|
||||
QObject::connect(menu, &QsMenuEntry::iconChanged, this, &PlatformMenuEntry::onIconChanged);
|
||||
QObject::connect(menu, &QsMenuEntry::buttonTypeChanged, this, &PlatformMenuEntry::onButtonTypeChanged);
|
||||
QObject::connect(menu, &QsMenuEntry::checkStateChanged, this, &PlatformMenuEntry::onCheckStateChanged);
|
||||
QObject::connect(menu, &QsMenuEntry::hasChildrenChanged, this, &PlatformMenuEntry::relayoutParent);
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
PlatformMenuEntry::~PlatformMenuEntry() {
|
||||
this->clearChildren();
|
||||
delete this->qaction;
|
||||
delete this->qmenu;
|
||||
}
|
||||
|
||||
void PlatformMenuEntry::registerCreationHook(std::function<void(PlatformMenuQMenu*)> hook) {
|
||||
CREATION_HOOKS.push_back(std::move(hook));
|
||||
}
|
||||
|
||||
bool PlatformMenuEntry::display(QObject* parentWindow, int relativeX, int relativeY) {
|
||||
QWindow* window = nullptr;
|
||||
|
||||
if (this->qmenu == nullptr) {
|
||||
qCritical() << "Cannot display PlatformMenuEntry as it is not a menu.";
|
||||
return false;
|
||||
} else if (parentWindow == nullptr) {
|
||||
qCritical() << "Cannot display PlatformMenuEntry with null parent window.";
|
||||
return false;
|
||||
} else if (auto* proxy = qobject_cast<ProxyWindowBase*>(parentWindow)) {
|
||||
window = proxy->backingWindow();
|
||||
} else if (auto* interface = qobject_cast<WindowInterface*>(parentWindow)) {
|
||||
window = interface->proxyWindow()->backingWindow();
|
||||
} else {
|
||||
qCritical() << "PlatformMenuEntry.display() must be called with a window.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (window == nullptr) {
|
||||
qCritical() << "Cannot display PlatformMenuEntry from a parent window that is not visible.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ACTIVE_MENU && this->qmenu != ACTIVE_MENU) {
|
||||
ACTIVE_MENU->close();
|
||||
}
|
||||
|
||||
ACTIVE_MENU = this->qmenu;
|
||||
|
||||
auto point = window->mapToGlobal(QPoint(relativeX, relativeY));
|
||||
|
||||
this->qmenu->createWinId();
|
||||
this->qmenu->windowHandle()->setTransientParent(window);
|
||||
|
||||
this->qmenu->popup(point);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlatformMenuEntry::relayout() {
|
||||
if (this->menu->hasChildren()) {
|
||||
delete this->qaction;
|
||||
this->qaction = nullptr;
|
||||
|
||||
if (this->qmenu == nullptr) {
|
||||
this->qmenu = new PlatformMenuQMenu();
|
||||
QObject::connect(this->qmenu, &QMenu::aboutToShow, this, &PlatformMenuEntry::onAboutToShow);
|
||||
QObject::connect(this->qmenu, &QMenu::aboutToHide, this, &PlatformMenuEntry::onAboutToHide);
|
||||
} else {
|
||||
this->clearChildren();
|
||||
}
|
||||
|
||||
this->qmenu->setTitle(this->menu->text());
|
||||
|
||||
auto icon = this->menu->icon();
|
||||
if (!icon.isEmpty()) {
|
||||
auto* generation = EngineGeneration::currentGeneration();
|
||||
this->qmenu->setIcon(generation->iconByUrl(this->menu->icon()));
|
||||
}
|
||||
|
||||
auto children = this->menu->children();
|
||||
auto len = children.count(&children);
|
||||
for (auto i = 0; i < len; i++) {
|
||||
auto* child = children.at(&children, i);
|
||||
|
||||
auto* instance = new PlatformMenuEntry(child);
|
||||
QObject::connect(instance, &QObject::destroyed, this, &PlatformMenuEntry::onChildDestroyed);
|
||||
|
||||
QObject::connect(
|
||||
instance,
|
||||
&PlatformMenuEntry::relayoutParent,
|
||||
this,
|
||||
&PlatformMenuEntry::relayout
|
||||
);
|
||||
|
||||
this->childEntries.push_back(instance);
|
||||
instance->addToQMenu(this->qmenu);
|
||||
}
|
||||
} else if (!this->menu->isSeparator()) {
|
||||
this->clearChildren();
|
||||
delete this->qmenu;
|
||||
this->qmenu = nullptr;
|
||||
|
||||
if (this->qaction == nullptr) {
|
||||
this->qaction = new QAction(this);
|
||||
|
||||
QObject::connect(
|
||||
this->qaction,
|
||||
&QAction::triggered,
|
||||
this,
|
||||
&PlatformMenuEntry::onActionTriggered
|
||||
);
|
||||
}
|
||||
|
||||
this->qaction->setText(this->menu->text());
|
||||
|
||||
auto icon = this->menu->icon();
|
||||
if (!icon.isEmpty()) {
|
||||
auto* generation = EngineGeneration::currentGeneration();
|
||||
this->qaction->setIcon(generation->iconByUrl(this->menu->icon()));
|
||||
}
|
||||
|
||||
this->qaction->setEnabled(this->menu->enabled());
|
||||
this->qaction->setCheckable(this->menu->buttonType() != QsMenuButtonType::None);
|
||||
|
||||
if (this->menu->buttonType() == QsMenuButtonType::RadioButton) {
|
||||
if (!this->qactiongroup) this->qactiongroup = new QActionGroup(this);
|
||||
this->qaction->setActionGroup(this->qactiongroup);
|
||||
}
|
||||
|
||||
this->qaction->setChecked(this->menu->checkState() != Qt::Unchecked);
|
||||
} else {
|
||||
delete this->qmenu;
|
||||
delete this->qaction;
|
||||
this->qmenu = nullptr;
|
||||
this->qaction = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void PlatformMenuEntry::onAboutToShow() { this->menu->ref(); }
|
||||
|
||||
void PlatformMenuEntry::onAboutToHide() {
|
||||
this->menu->unref();
|
||||
emit this->closed();
|
||||
}
|
||||
|
||||
void PlatformMenuEntry::onActionTriggered() {
|
||||
auto* action = qobject_cast<PlatformMenuEntry*>(this->sender()->parent());
|
||||
emit action->menu->triggered();
|
||||
}
|
||||
|
||||
void PlatformMenuEntry::onChildDestroyed() { this->childEntries.removeOne(this->sender()); }
|
||||
|
||||
void PlatformMenuEntry::onEnabledChanged() {
|
||||
if (this->qaction != nullptr) {
|
||||
this->qaction->setEnabled(this->menu->enabled());
|
||||
}
|
||||
}
|
||||
|
||||
void PlatformMenuEntry::onTextChanged() {
|
||||
if (this->qmenu != nullptr) {
|
||||
this->qmenu->setTitle(this->menu->text());
|
||||
} else if (this->qaction != nullptr) {
|
||||
this->qaction->setText(this->menu->text());
|
||||
}
|
||||
}
|
||||
|
||||
void PlatformMenuEntry::onIconChanged() {
|
||||
if (this->qmenu == nullptr && this->qaction == nullptr) return;
|
||||
|
||||
auto iconName = this->menu->icon();
|
||||
QIcon icon;
|
||||
|
||||
if (!iconName.isEmpty()) {
|
||||
auto* generation = EngineGeneration::currentGeneration();
|
||||
icon = generation->iconByUrl(iconName);
|
||||
}
|
||||
|
||||
if (this->qmenu != nullptr) {
|
||||
this->qmenu->setIcon(icon);
|
||||
} else if (this->qaction != nullptr) {
|
||||
this->qaction->setIcon(icon);
|
||||
}
|
||||
}
|
||||
|
||||
void PlatformMenuEntry::onButtonTypeChanged() {
|
||||
if (this->qaction != nullptr) {
|
||||
QActionGroup* group = nullptr;
|
||||
|
||||
if (this->menu->buttonType() == QsMenuButtonType::RadioButton) {
|
||||
if (!this->qactiongroup) this->qactiongroup = new QActionGroup(this);
|
||||
group = this->qactiongroup;
|
||||
}
|
||||
|
||||
this->qaction->setActionGroup(group);
|
||||
}
|
||||
}
|
||||
|
||||
void PlatformMenuEntry::onCheckStateChanged() {
|
||||
if (this->qaction != nullptr) {
|
||||
this->qaction->setChecked(this->menu->checkState() != Qt::Unchecked);
|
||||
}
|
||||
}
|
||||
|
||||
void PlatformMenuEntry::clearChildren() {
|
||||
for (auto* child: this->childEntries) {
|
||||
delete child;
|
||||
}
|
||||
|
||||
this->childEntries.clear();
|
||||
}
|
||||
|
||||
void PlatformMenuEntry::addToQMenu(PlatformMenuQMenu* menu) {
|
||||
if (this->qmenu != nullptr) {
|
||||
menu->addMenu(this->qmenu);
|
||||
this->qmenu->containingMenu = menu;
|
||||
} else if (this->qaction != nullptr) {
|
||||
menu->addAction(this->qaction);
|
||||
} else {
|
||||
menu->addSeparator();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace qs::menu::platform
|
72
src/core/platformmenu.hpp
Normal file
72
src/core/platformmenu.hpp
Normal file
|
@ -0,0 +1,72 @@
|
|||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include <qaction.h>
|
||||
#include <qactiongroup.h>
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qmenu.h>
|
||||
#include <qobject.h>
|
||||
#include <qpoint.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qqmllist.h>
|
||||
#include <qtclasshelpermacros.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "qsmenu.hpp"
|
||||
|
||||
namespace qs::menu::platform {
|
||||
|
||||
class PlatformMenuQMenu: public QMenu {
|
||||
public:
|
||||
explicit PlatformMenuQMenu() = default;
|
||||
~PlatformMenuQMenu() override;
|
||||
Q_DISABLE_COPY_MOVE(PlatformMenuQMenu);
|
||||
|
||||
void setVisible(bool visible) override;
|
||||
|
||||
PlatformMenuQMenu* containingMenu = nullptr;
|
||||
};
|
||||
|
||||
class PlatformMenuEntry: public QObject {
|
||||
Q_OBJECT;
|
||||
|
||||
public:
|
||||
explicit PlatformMenuEntry(QsMenuEntry* menu);
|
||||
~PlatformMenuEntry() override;
|
||||
Q_DISABLE_COPY_MOVE(PlatformMenuEntry);
|
||||
|
||||
bool display(QObject* parentWindow, int relativeX, int relativeY);
|
||||
|
||||
static void registerCreationHook(std::function<void(PlatformMenuQMenu*)> hook);
|
||||
|
||||
signals:
|
||||
void closed();
|
||||
void relayoutParent();
|
||||
|
||||
public slots:
|
||||
void relayout();
|
||||
|
||||
private slots:
|
||||
void onAboutToShow();
|
||||
void onAboutToHide();
|
||||
void onActionTriggered();
|
||||
void onChildDestroyed();
|
||||
void onEnabledChanged();
|
||||
void onTextChanged();
|
||||
void onIconChanged();
|
||||
void onButtonTypeChanged();
|
||||
void onCheckStateChanged();
|
||||
|
||||
private:
|
||||
void clearChildren();
|
||||
void addToQMenu(PlatformMenuQMenu* menu);
|
||||
|
||||
QsMenuEntry* menu;
|
||||
PlatformMenuQMenu* qmenu = nullptr;
|
||||
QAction* qaction = nullptr;
|
||||
QActionGroup* qactiongroup = nullptr;
|
||||
QVector<PlatformMenuEntry*> childEntries;
|
||||
};
|
||||
|
||||
} // namespace qs::menu::platform
|
95
src/core/qsmenu.cpp
Normal file
95
src/core/qsmenu.cpp
Normal file
|
@ -0,0 +1,95 @@
|
|||
#include "qsmenu.hpp"
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmllist.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "platformmenu.hpp"
|
||||
|
||||
using namespace qs::menu::platform;
|
||||
|
||||
namespace qs::menu {
|
||||
|
||||
QString QsMenuButtonType::toString(QsMenuButtonType::Enum value) {
|
||||
switch (value) {
|
||||
case QsMenuButtonType::None: return "None";
|
||||
case QsMenuButtonType::CheckBox: return "CheckBox";
|
||||
case QsMenuButtonType::RadioButton: return "RadioButton";
|
||||
default: return "Invalid button type";
|
||||
}
|
||||
}
|
||||
|
||||
void QsMenuEntry::display(QObject* parentWindow, int relativeX, int relativeY) {
|
||||
auto* platform = new PlatformMenuEntry(this);
|
||||
|
||||
QObject::connect(platform, &PlatformMenuEntry::closed, platform, [=]() {
|
||||
platform->deleteLater();
|
||||
});
|
||||
|
||||
auto success = platform->display(parentWindow, relativeX, relativeY);
|
||||
if (!success) delete platform;
|
||||
}
|
||||
|
||||
QQmlListProperty<QsMenuEntry> QsMenuEntry::emptyChildren(QObject* parent) {
|
||||
return QQmlListProperty<QsMenuEntry>(
|
||||
parent,
|
||||
nullptr,
|
||||
&QsMenuEntry::childCount,
|
||||
&QsMenuEntry::childAt
|
||||
);
|
||||
}
|
||||
|
||||
void QsMenuEntry::ref() {
|
||||
this->refcount++;
|
||||
if (this->refcount == 1) emit this->opened();
|
||||
}
|
||||
|
||||
void QsMenuEntry::unref() {
|
||||
this->refcount--;
|
||||
if (this->refcount == 0) emit this->closed();
|
||||
}
|
||||
|
||||
QQmlListProperty<QsMenuEntry> QsMenuEntry::children() { return QsMenuEntry::emptyChildren(this); }
|
||||
|
||||
QsMenuEntry* QsMenuOpener::menu() const { return this->mMenu; }
|
||||
|
||||
void QsMenuOpener::setMenu(QsMenuEntry* menu) {
|
||||
if (menu == this->mMenu) return;
|
||||
|
||||
if (this->mMenu != nullptr) {
|
||||
this->mMenu->unref();
|
||||
QObject::disconnect(this->mMenu, nullptr, this, nullptr);
|
||||
}
|
||||
|
||||
this->mMenu = menu;
|
||||
|
||||
if (menu != nullptr) {
|
||||
QObject::connect(menu, &QObject::destroyed, this, &QsMenuOpener::onMenuDestroyed);
|
||||
QObject::connect(menu, &QsMenuEntry::childrenChanged, this, &QsMenuOpener::childrenChanged);
|
||||
menu->ref();
|
||||
}
|
||||
|
||||
emit this->menuChanged();
|
||||
emit this->childrenChanged();
|
||||
}
|
||||
|
||||
void QsMenuOpener::onMenuDestroyed() {
|
||||
this->mMenu = nullptr;
|
||||
emit this->menuChanged();
|
||||
emit this->childrenChanged();
|
||||
}
|
||||
|
||||
QQmlListProperty<QsMenuEntry> QsMenuOpener::children() {
|
||||
return this->mMenu ? this->mMenu->children() : QsMenuEntry::emptyChildren(this);
|
||||
}
|
||||
|
||||
qsizetype QsMenuEntry::childCount(QQmlListProperty<QsMenuEntry>* /*property*/) { return 0; }
|
||||
|
||||
QsMenuEntry*
|
||||
QsMenuEntry::childAt(QQmlListProperty<QsMenuEntry>* /*property*/, qsizetype /*index*/) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace qs::menu
|
140
src/core/qsmenu.hpp
Normal file
140
src/core/qsmenu.hpp
Normal file
|
@ -0,0 +1,140 @@
|
|||
#pragma once
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qnamespace.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qqmllist.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "doc.hpp"
|
||||
|
||||
namespace qs::menu {
|
||||
|
||||
class QsMenuButtonType: public QObject {
|
||||
Q_OBJECT;
|
||||
QML_ELEMENT;
|
||||
QML_SINGLETON;
|
||||
|
||||
public:
|
||||
enum Enum {
|
||||
/// This menu item does not have a checkbox or a radiobutton associated with it.
|
||||
None = 0,
|
||||
/// This menu item should draw a checkbox.
|
||||
CheckBox = 1,
|
||||
/// This menu item should draw a radiobutton.
|
||||
RadioButton = 2,
|
||||
};
|
||||
Q_ENUM(Enum);
|
||||
|
||||
Q_INVOKABLE static QString toString(QsMenuButtonType::Enum value);
|
||||
};
|
||||
|
||||
class QsMenuEntry: public QObject {
|
||||
Q_OBJECT;
|
||||
/// If this menu item should be rendered as a separator between other items.
|
||||
///
|
||||
/// No other properties have a meaningful value when `isSeparator` is true.
|
||||
Q_PROPERTY(bool isSeparator READ isSeparator NOTIFY isSeparatorChanged);
|
||||
Q_PROPERTY(bool enabled READ enabled NOTIFY enabledChanged);
|
||||
/// Text of the menu item.
|
||||
Q_PROPERTY(QString text READ text NOTIFY textChanged);
|
||||
/// Url of the menu item's icon or `""` if it doesn't have one.
|
||||
///
|
||||
/// This can be passed to [Image.source](https://doc.qt.io/qt-6/qml-qtquick-image.html#source-prop)
|
||||
/// as shown below.
|
||||
///
|
||||
/// ```qml
|
||||
/// Image {
|
||||
/// source: menuItem.icon
|
||||
/// // To get the best image quality, set the image source size to the same size
|
||||
/// // as the rendered image.
|
||||
/// sourceSize.width: width
|
||||
/// sourceSize.height: height
|
||||
/// }
|
||||
/// ```
|
||||
Q_PROPERTY(QString icon READ icon NOTIFY iconChanged);
|
||||
/// If this menu item has an associated checkbox or radiobutton.
|
||||
Q_PROPERTY(QsMenuButtonType::Enum buttonType READ buttonType NOTIFY buttonTypeChanged);
|
||||
/// The check state of the checkbox or radiobutton if applicable, as a
|
||||
/// [Qt.CheckState](https://doc.qt.io/qt-6/qt.html#CheckState-enum).
|
||||
Q_PROPERTY(Qt::CheckState checkState READ checkState NOTIFY checkStateChanged);
|
||||
/// If this menu item has children that can be accessed through a [QsMenuOpener](../qsmenuopener).
|
||||
Q_PROPERTY(bool hasChildren READ hasChildren NOTIFY hasChildrenChanged);
|
||||
QML_ELEMENT;
|
||||
QML_UNCREATABLE("QsMenuEntry cannot be directly created");
|
||||
|
||||
public:
|
||||
explicit QsMenuEntry(QObject* parent = nullptr): QObject(parent) {}
|
||||
|
||||
/// Display a platform menu at the given location relative to the parent window.
|
||||
Q_INVOKABLE void display(QObject* parentWindow, qint32 relativeX, qint32 relativeY);
|
||||
|
||||
[[nodiscard]] virtual bool isSeparator() const { return false; }
|
||||
[[nodiscard]] virtual bool enabled() const { return true; }
|
||||
[[nodiscard]] virtual QString text() const { return ""; }
|
||||
[[nodiscard]] virtual QString icon() const { return ""; }
|
||||
[[nodiscard]] virtual QsMenuButtonType::Enum buttonType() const { return QsMenuButtonType::None; }
|
||||
[[nodiscard]] virtual Qt::CheckState checkState() const { return Qt::Unchecked; }
|
||||
[[nodiscard]] virtual bool hasChildren() const { return false; }
|
||||
|
||||
void ref();
|
||||
void unref();
|
||||
|
||||
[[nodiscard]] virtual QQmlListProperty<QsMenuEntry> children();
|
||||
|
||||
static QQmlListProperty<QsMenuEntry> emptyChildren(QObject* parent);
|
||||
|
||||
signals:
|
||||
/// Send a trigger/click signal to the menu entry.
|
||||
void triggered();
|
||||
|
||||
QSDOC_HIDE void opened();
|
||||
QSDOC_HIDE void closed();
|
||||
|
||||
void isSeparatorChanged();
|
||||
void enabledChanged();
|
||||
void textChanged();
|
||||
void iconChanged();
|
||||
void buttonTypeChanged();
|
||||
void checkStateChanged();
|
||||
void hasChildrenChanged();
|
||||
QSDOC_HIDE void childrenChanged();
|
||||
|
||||
private:
|
||||
static qsizetype childCount(QQmlListProperty<QsMenuEntry>* property);
|
||||
static QsMenuEntry* childAt(QQmlListProperty<QsMenuEntry>* property, qsizetype index);
|
||||
|
||||
qsizetype refcount = 0;
|
||||
};
|
||||
|
||||
///! Provides access to children of a QsMenuEntry
|
||||
class QsMenuOpener: public QObject {
|
||||
Q_OBJECT;
|
||||
/// The menu to retrieve children from.
|
||||
Q_PROPERTY(QsMenuEntry* menu READ menu WRITE setMenu NOTIFY menuChanged);
|
||||
/// The children of the given menu.
|
||||
Q_PROPERTY(QQmlListProperty<QsMenuEntry> children READ children NOTIFY childrenChanged);
|
||||
QML_ELEMENT;
|
||||
|
||||
public:
|
||||
explicit QsMenuOpener(QObject* parent = nullptr): QObject(parent) {}
|
||||
|
||||
[[nodiscard]] QsMenuEntry* menu() const;
|
||||
void setMenu(QsMenuEntry* menu);
|
||||
|
||||
[[nodiscard]] QQmlListProperty<QsMenuEntry> children();
|
||||
|
||||
signals:
|
||||
void menuChanged();
|
||||
void childrenChanged();
|
||||
|
||||
private slots:
|
||||
void onMenuDestroyed();
|
||||
|
||||
private:
|
||||
QsMenuEntry* mMenu = nullptr;
|
||||
};
|
||||
|
||||
} // namespace qs::menu
|
Loading…
Add table
Add a link
Reference in a new issue