mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
tray: harden async dbusMenu loading
This commit is contained in:
+297
-100
@@ -5,9 +5,11 @@
|
||||
#include "dbus/session_bus.h"
|
||||
#include "util/string_utils.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
|
||||
namespace {
|
||||
@@ -124,43 +126,147 @@ namespace {
|
||||
sdbus::Struct<std::int32_t, std::map<std::string, sdbus::Variant>, std::vector<sdbus::Variant>>;
|
||||
using DbusMenuItemProperties = sdbus::Struct<std::int32_t, std::map<std::string, sdbus::Variant>>;
|
||||
|
||||
void applyMenuEntryProperties(TrayMenuEntry& out, const std::map<std::string, sdbus::Variant>& props) {
|
||||
std::optional<std::string> stringFromVariant(const sdbus::Variant& value) {
|
||||
try {
|
||||
return value.get<std::string>();
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<bool> boolFromVariant(const sdbus::Variant& value) {
|
||||
try {
|
||||
return value.get<bool>();
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<std::int32_t> int32FromVariant(const sdbus::Variant& value) {
|
||||
try {
|
||||
return value.get<std::int32_t>();
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
try {
|
||||
return static_cast<std::int32_t>(value.get<std::uint32_t>());
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> bytesFromVariant(const sdbus::Variant& value) {
|
||||
try {
|
||||
return value.get<std::vector<std::uint8_t>>();
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void resetMenuEntryProperty(TrayMenuEntry& out, std::string_view property) {
|
||||
if (property == "label") {
|
||||
out.label.clear();
|
||||
} else if (property == "icon-name") {
|
||||
out.iconName.clear();
|
||||
} else if (property == "icon-data") {
|
||||
out.iconData.clear();
|
||||
} else if (property == "enabled") {
|
||||
out.enabled = true;
|
||||
} else if (property == "visible") {
|
||||
out.visible = true;
|
||||
} else if (property == "type") {
|
||||
out.separator = false;
|
||||
} else if (property == "children-display") {
|
||||
out.hasSubmenu = false;
|
||||
} else if (property == "toggle-type") {
|
||||
out.checkmark = false;
|
||||
out.radio = false;
|
||||
} else if (property == "toggle-state") {
|
||||
out.toggleState = -1;
|
||||
}
|
||||
}
|
||||
|
||||
bool propertyRemoved(const std::vector<std::string>& removed, std::string_view property) {
|
||||
return std::ranges::any_of(removed, [property](const std::string& value) { return value == property; });
|
||||
}
|
||||
|
||||
void applyMenuEntryProperties(TrayMenuEntry& out, const std::map<std::string, sdbus::Variant>& props,
|
||||
bool resetMissing = false, const std::vector<std::string>& removed = {}) {
|
||||
if (const auto it = props.find("label"); it != props.end()) {
|
||||
try {
|
||||
out.label = stripMnemonicUnderscores(it->second.get<std::string>());
|
||||
} catch (const sdbus::Error&) {
|
||||
if (const auto value = stringFromVariant(it->second); value.has_value()) {
|
||||
out.label = stripMnemonicUnderscores(*value);
|
||||
}
|
||||
} else if (resetMissing || propertyRemoved(removed, "label")) {
|
||||
resetMenuEntryProperty(out, "label");
|
||||
}
|
||||
|
||||
if (const auto it = props.find("icon-name"); it != props.end()) {
|
||||
if (const auto value = stringFromVariant(it->second); value.has_value()) {
|
||||
out.iconName = *value;
|
||||
}
|
||||
} else if (resetMissing || propertyRemoved(removed, "icon-name")) {
|
||||
resetMenuEntryProperty(out, "icon-name");
|
||||
}
|
||||
|
||||
if (const auto it = props.find("icon-data"); it != props.end()) {
|
||||
out.iconData = bytesFromVariant(it->second);
|
||||
} else if (resetMissing || propertyRemoved(removed, "icon-data")) {
|
||||
resetMenuEntryProperty(out, "icon-data");
|
||||
}
|
||||
|
||||
if (const auto it = props.find("enabled"); it != props.end()) {
|
||||
try {
|
||||
out.enabled = it->second.get<bool>();
|
||||
} catch (const sdbus::Error&) {
|
||||
if (const auto value = boolFromVariant(it->second); value.has_value()) {
|
||||
out.enabled = *value;
|
||||
}
|
||||
} else if (resetMissing || propertyRemoved(removed, "enabled")) {
|
||||
resetMenuEntryProperty(out, "enabled");
|
||||
}
|
||||
|
||||
if (const auto it = props.find("visible"); it != props.end()) {
|
||||
try {
|
||||
out.visible = it->second.get<bool>();
|
||||
} catch (const sdbus::Error&) {
|
||||
if (const auto value = boolFromVariant(it->second); value.has_value()) {
|
||||
out.visible = *value;
|
||||
}
|
||||
} else if (resetMissing || propertyRemoved(removed, "visible")) {
|
||||
resetMenuEntryProperty(out, "visible");
|
||||
}
|
||||
|
||||
if (const auto it = props.find("type"); it != props.end()) {
|
||||
try {
|
||||
out.separator = (it->second.get<std::string>() == "separator");
|
||||
} catch (const sdbus::Error&) {
|
||||
if (const auto value = stringFromVariant(it->second); value.has_value()) {
|
||||
out.separator = (*value == "separator");
|
||||
}
|
||||
} else if (resetMissing || propertyRemoved(removed, "type")) {
|
||||
resetMenuEntryProperty(out, "type");
|
||||
}
|
||||
|
||||
if (const auto it = props.find("children-display"); it != props.end()) {
|
||||
try {
|
||||
out.hasSubmenu = (it->second.get<std::string>() == "submenu");
|
||||
} catch (const sdbus::Error&) {
|
||||
if (const auto value = stringFromVariant(it->second); value.has_value()) {
|
||||
out.hasSubmenu = (*value == "submenu");
|
||||
}
|
||||
} else if (resetMissing || propertyRemoved(removed, "children-display")) {
|
||||
resetMenuEntryProperty(out, "children-display");
|
||||
}
|
||||
|
||||
if (const auto it = props.find("toggle-type"); it != props.end()) {
|
||||
if (const auto value = stringFromVariant(it->second); value.has_value()) {
|
||||
out.checkmark = (*value == "checkmark");
|
||||
out.radio = (*value == "radio");
|
||||
}
|
||||
} else if (resetMissing || propertyRemoved(removed, "toggle-type")) {
|
||||
resetMenuEntryProperty(out, "toggle-type");
|
||||
}
|
||||
|
||||
if (const auto it = props.find("toggle-state"); it != props.end()) {
|
||||
if (const auto value = int32FromVariant(it->second); value.has_value()) {
|
||||
out.toggleState = *value;
|
||||
}
|
||||
} else if (resetMissing || propertyRemoved(removed, "toggle-state")) {
|
||||
resetMenuEntryProperty(out, "toggle-state");
|
||||
}
|
||||
}
|
||||
|
||||
TrayMenuEntry decodeMenuEntry(const DbusMenuLayout& entryLayout) {
|
||||
TrayMenuEntry out;
|
||||
out.id = std::get<0>(entryLayout);
|
||||
applyMenuEntryProperties(out, std::get<1>(entryLayout));
|
||||
applyMenuEntryProperties(out, std::get<1>(entryLayout), true);
|
||||
|
||||
return out;
|
||||
}
|
||||
@@ -169,7 +275,8 @@ namespace {
|
||||
if (entry.id <= 0 || !entry.visible) {
|
||||
return false;
|
||||
}
|
||||
if (entry.label.empty() && !entry.separator) {
|
||||
if (entry.label.empty() && !entry.separator && !entry.hasSubmenu && entry.iconName.empty() &&
|
||||
entry.iconData.empty()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -425,30 +532,51 @@ std::vector<TrayItemInfo> TrayService::items() const {
|
||||
|
||||
namespace {
|
||||
|
||||
// Recursively decode a DbusMenuLayout into the cache. Each layout node contributes
|
||||
// a `std::vector<TrayMenuEntry>` keyed by its id into entriesByParent. Invisible
|
||||
// entries are skipped from display but we still recurse so their own children
|
||||
// (if any) are reachable from the cache.
|
||||
void ingestLayoutNode(const DbusMenuLayout& node,
|
||||
std::unordered_map<std::int32_t, std::vector<TrayMenuEntry>>& entriesByParent) {
|
||||
// Recursively decode a DbusMenuLayout into retained item + child-id maps.
|
||||
// Visibility is applied when entries are read for display, not while storing,
|
||||
// so later ItemsPropertiesUpdated patches can reveal previously hidden rows.
|
||||
void ingestLayoutNode(const DbusMenuLayout& node, std::unordered_map<std::int32_t, TrayMenuEntry>& entriesById,
|
||||
std::unordered_map<std::int32_t, std::vector<std::int32_t>>& childrenByParent) {
|
||||
const auto nodeId = std::get<0>(node);
|
||||
const auto& children = std::get<2>(node);
|
||||
|
||||
std::vector<TrayMenuEntry> entries;
|
||||
entries.reserve(children.size());
|
||||
std::vector<std::int32_t> childIds;
|
||||
childIds.reserve(children.size());
|
||||
for (const auto& childValue : children) {
|
||||
try {
|
||||
const auto child = childValue.get<DbusMenuLayout>();
|
||||
auto entry = decodeMenuEntry(child);
|
||||
ingestLayoutNode(child, entriesByParent);
|
||||
if (!displayableMenuEntry(entry)) {
|
||||
continue;
|
||||
const auto entryId = entry.id;
|
||||
if (entryId > 0) {
|
||||
entriesById[entryId] = std::move(entry);
|
||||
childIds.push_back(entryId);
|
||||
}
|
||||
entries.push_back(std::move(entry));
|
||||
ingestLayoutNode(child, entriesById, childrenByParent);
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
}
|
||||
entriesByParent[nodeId] = std::move(entries);
|
||||
childrenByParent[nodeId] = std::move(childIds);
|
||||
}
|
||||
|
||||
std::vector<TrayMenuEntry>
|
||||
entriesForParent(const std::unordered_map<std::int32_t, TrayMenuEntry>& entriesById,
|
||||
const std::unordered_map<std::int32_t, std::vector<std::int32_t>>& childrenByParent,
|
||||
std::int32_t parentId) {
|
||||
std::vector<TrayMenuEntry> out;
|
||||
const auto childrenIt = childrenByParent.find(parentId);
|
||||
if (childrenIt == childrenByParent.end()) {
|
||||
return out;
|
||||
}
|
||||
|
||||
out.reserve(childrenIt->second.size());
|
||||
for (const auto childId : childrenIt->second) {
|
||||
const auto entryIt = entriesById.find(childId);
|
||||
if (entryIt == entriesById.end() || !displayableMenuEntry(entryIt->second)) {
|
||||
continue;
|
||||
}
|
||||
out.push_back(entryIt->second);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -463,10 +591,11 @@ bool TrayService::fetchMenuProperties(const std::string& itemId, const std::vect
|
||||
if (cacheIt == m_menuCache.end() || cacheIt->second.proxy == nullptr) {
|
||||
return false;
|
||||
}
|
||||
auto& cache = cacheIt->second;
|
||||
|
||||
try {
|
||||
std::vector<DbusMenuItemProperties> properties;
|
||||
cacheIt->second.proxy->callMethod("GetGroupProperties")
|
||||
cache.proxy->callMethod("GetGroupProperties")
|
||||
.onInterface(k_menu_interface)
|
||||
.withTimeout(std::chrono::milliseconds(1000))
|
||||
.withArguments(entryIds, std::vector<std::string>{})
|
||||
@@ -484,8 +613,9 @@ bool TrayService::fetchMenuProperties(const std::string& itemId, const std::vect
|
||||
TrayMenuEntry entry;
|
||||
entry.id = entryId;
|
||||
if (const auto propsIt = propertiesById.find(entryId); propsIt != propertiesById.end()) {
|
||||
applyMenuEntryProperties(entry, propsIt->second);
|
||||
applyMenuEntryProperties(entry, propsIt->second, true);
|
||||
}
|
||||
cache.entriesById[entryId] = entry;
|
||||
if (displayableMenuEntry(entry)) {
|
||||
outEntries.push_back(std::move(entry));
|
||||
}
|
||||
@@ -497,62 +627,104 @@ bool TrayService::fetchMenuProperties(const std::string& itemId, const std::vect
|
||||
}
|
||||
}
|
||||
|
||||
bool TrayService::fetchMenuSubtree(const std::string& itemId, std::int32_t parentId) {
|
||||
void TrayService::requestMenuSubtree(const std::string& itemId, std::int32_t parentId, bool force) {
|
||||
auto cacheIt = m_menuCache.find(itemId);
|
||||
if (cacheIt == m_menuCache.end() || cacheIt->second.proxy == nullptr) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
auto& cache = cacheIt->second;
|
||||
|
||||
// AboutToShow lets the server populate or refresh this subtree. Failures are
|
||||
// non-fatal — not every app implements it, and some Electron versions throw
|
||||
// on it even when GetLayout would succeed.
|
||||
if (!force && cache.loadedParents.contains(parentId)) {
|
||||
return;
|
||||
}
|
||||
if (cache.loadingParents.contains(parentId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
cache.loadingParents.insert(parentId);
|
||||
const auto generation = cache.generation;
|
||||
|
||||
try {
|
||||
bool needsUpdate = false;
|
||||
cache.proxy->callMethod("AboutToShow")
|
||||
cache.proxy->callMethodAsync("AboutToShow")
|
||||
.onInterface(k_menu_interface)
|
||||
.withTimeout(std::chrono::milliseconds(500))
|
||||
.withArguments(parentId)
|
||||
.storeResultsTo(needsUpdate);
|
||||
(void)needsUpdate;
|
||||
.uponReplyInvoke([this, itemId, parentId, generation](std::optional<sdbus::Error> error, bool /*needsUpdate*/) {
|
||||
if (error.has_value()) {
|
||||
kLog.debug("AboutToShow async failed id={} parentId={} err={}", itemId, parentId, error->what());
|
||||
}
|
||||
requestMenuLayoutAfterAboutToShow(itemId, parentId, generation);
|
||||
});
|
||||
} catch (const sdbus::Error& e) {
|
||||
kLog.debug("AboutToShow failed id={} parentId={} err={}", itemId, parentId, e.what());
|
||||
cache.loadingParents.erase(parentId);
|
||||
kLog.debug("AboutToShow async setup failed id={} parentId={} err={}", itemId, parentId, e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void TrayService::requestMenuLayoutAfterAboutToShow(const std::string& itemId, std::int32_t parentId,
|
||||
std::uint64_t generation) {
|
||||
auto cacheIt = m_menuCache.find(itemId);
|
||||
if (cacheIt == m_menuCache.end() || cacheIt->second.proxy == nullptr) {
|
||||
return;
|
||||
}
|
||||
auto& cache = cacheIt->second;
|
||||
if (cache.generation != generation) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
std::uint32_t revision = 0;
|
||||
DbusMenuLayout layout{};
|
||||
// depth=-1 asks for the full subtree in one call so we don't round-trip
|
||||
// per submenu, dbusmenu spec allows it.
|
||||
cache.proxy->callMethod("GetLayout")
|
||||
cache.proxy->callMethodAsync("GetLayout")
|
||||
.onInterface(k_menu_interface)
|
||||
.withTimeout(std::chrono::milliseconds(2000))
|
||||
.withArguments(parentId, static_cast<std::int32_t>(-1), std::vector<std::string>{})
|
||||
.storeResultsTo(revision, layout);
|
||||
.uponReplyInvoke([this, itemId, parentId, generation](std::optional<sdbus::Error> error, std::uint32_t revision,
|
||||
DbusMenuLayout layout) {
|
||||
auto replyCacheIt = m_menuCache.find(itemId);
|
||||
if (replyCacheIt == m_menuCache.end() || replyCacheIt->second.proxy == nullptr) {
|
||||
return;
|
||||
}
|
||||
auto& replyCache = replyCacheIt->second;
|
||||
if (replyCache.generation != generation) {
|
||||
return;
|
||||
}
|
||||
|
||||
cache.revision = revision;
|
||||
ingestLayoutNode(layout, cache.entriesByParent);
|
||||
const auto before = entriesForParent(replyCache.entriesById, replyCache.childrenByParent, parentId);
|
||||
replyCache.loadingParents.erase(parentId);
|
||||
|
||||
auto entriesIt = cache.entriesByParent.find(parentId);
|
||||
if (entriesIt == cache.entriesByParent.end() || entriesIt->second.empty()) {
|
||||
const auto childIds = childIdsFromLayoutProperties(layout);
|
||||
if (!childIds.empty()) {
|
||||
std::vector<TrayMenuEntry> propertyEntries;
|
||||
if (fetchMenuProperties(itemId, childIds, propertyEntries)) {
|
||||
kLog.debug("dbusmenu children-property fallback id={} parentId={} children={} entries={}", itemId, parentId,
|
||||
childIds.size(), propertyEntries.size());
|
||||
cache.entriesByParent[parentId] = std::move(propertyEntries);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (error.has_value()) {
|
||||
kLog.debug("GetLayout async failed id={} parentId={} err={}", itemId, parentId, error->what());
|
||||
return;
|
||||
}
|
||||
|
||||
if (parentId == 0) {
|
||||
cache.rootLoaded = true;
|
||||
}
|
||||
return true;
|
||||
replyCache.revision = revision;
|
||||
ingestLayoutNode(layout, replyCache.entriesById, replyCache.childrenByParent);
|
||||
|
||||
auto after = entriesForParent(replyCache.entriesById, replyCache.childrenByParent, parentId);
|
||||
if (after.empty()) {
|
||||
const auto childIds = childIdsFromLayoutProperties(layout);
|
||||
if (!childIds.empty()) {
|
||||
replyCache.childrenByParent[parentId] = childIds;
|
||||
std::vector<TrayMenuEntry> propertyEntries;
|
||||
if (fetchMenuProperties(itemId, childIds, propertyEntries)) {
|
||||
kLog.debug("dbusmenu children-property fallback id={} parentId={} children={} entries={}", itemId,
|
||||
parentId, childIds.size(), propertyEntries.size());
|
||||
after = entriesForParent(replyCache.entriesById, replyCache.childrenByParent, parentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
replyCache.loadedParents.insert(parentId);
|
||||
if (parentId == 0) {
|
||||
replyCache.rootLoaded = true;
|
||||
}
|
||||
|
||||
if (before != after || !after.empty()) {
|
||||
emitChanged();
|
||||
}
|
||||
});
|
||||
} catch (const sdbus::Error& e) {
|
||||
kLog.debug("GetLayout failed id={} parentId={} err={}", itemId, parentId, e.what());
|
||||
return false;
|
||||
cache.loadingParents.erase(parentId);
|
||||
kLog.debug("GetLayout async setup failed id={} parentId={} err={}", itemId, parentId, e.what());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -579,22 +751,14 @@ std::vector<TrayMenuEntry> TrayService::menuEntries(const std::string& itemId) {
|
||||
}
|
||||
|
||||
if (!cacheIt->second.rootLoaded) {
|
||||
if (!fetchMenuSubtree(itemId, 0)) {
|
||||
return {};
|
||||
}
|
||||
requestMenuSubtree(itemId, 0);
|
||||
}
|
||||
|
||||
auto rootIt = cacheIt->second.entriesByParent.find(0);
|
||||
if (rootIt == cacheIt->second.entriesByParent.end() || rootIt->second.empty()) {
|
||||
if (!fetchMenuSubtree(itemId, 0)) {
|
||||
return {};
|
||||
}
|
||||
rootIt = cacheIt->second.entriesByParent.find(0);
|
||||
if (rootIt == cacheIt->second.entriesByParent.end()) {
|
||||
return {};
|
||||
}
|
||||
auto entries = entriesForParent(cacheIt->second.entriesById, cacheIt->second.childrenByParent, 0);
|
||||
if (entries.empty() && !cacheIt->second.loadingParents.contains(0)) {
|
||||
requestMenuSubtree(itemId, 0, true);
|
||||
}
|
||||
return rootIt->second;
|
||||
return entries;
|
||||
}
|
||||
|
||||
std::vector<TrayMenuEntry> TrayService::menuEntriesForParent(const std::string& itemId, std::int32_t parentId) {
|
||||
@@ -610,19 +774,18 @@ std::vector<TrayMenuEntry> TrayService::menuEntriesForParent(const std::string&
|
||||
}
|
||||
|
||||
auto& cache = cacheIt->second;
|
||||
if (const auto it = cache.entriesByParent.find(parentId); it != cache.entriesByParent.end()) {
|
||||
return it->second;
|
||||
auto entries = entriesForParent(cache.entriesById, cache.childrenByParent, parentId);
|
||||
if (!entries.empty()) {
|
||||
return entries;
|
||||
}
|
||||
|
||||
// Parent's children weren't populated by the recursive root fetch (some apps
|
||||
// populate submenus lazily on AboutToShow). Fetch the subtree now.
|
||||
if (!fetchMenuSubtree(itemId, parentId)) {
|
||||
return {};
|
||||
// populate submenus lazily on AboutToShow). Request the subtree and let the
|
||||
// tray menu refresh when the async reply arrives.
|
||||
if (!cache.loadingParents.contains(parentId)) {
|
||||
requestMenuSubtree(itemId, parentId, true);
|
||||
}
|
||||
if (const auto it = cache.entriesByParent.find(parentId); it != cache.entriesByParent.end()) {
|
||||
return it->second;
|
||||
}
|
||||
return {};
|
||||
return entries;
|
||||
}
|
||||
|
||||
void TrayService::ensureMenuCache(const std::string& itemId, const std::string& busName, const std::string& menuPath) {
|
||||
@@ -644,32 +807,66 @@ void TrayService::ensureMenuCache(const std::string& itemId, const std::string&
|
||||
.onInterface(k_menu_interface)
|
||||
.call([this, itemId](std::uint32_t revision, std::int32_t parent) {
|
||||
if (auto it = m_menuCache.find(itemId); it != m_menuCache.end()) {
|
||||
it->second.entriesByParent.clear();
|
||||
it->second.entriesById.clear();
|
||||
it->second.childrenByParent.clear();
|
||||
it->second.loadedParents.clear();
|
||||
it->second.loadingParents.clear();
|
||||
it->second.rootLoaded = false;
|
||||
it->second.revision = revision;
|
||||
++it->second.generation;
|
||||
}
|
||||
kLog.debug("LayoutUpdated id={} rev={} parent={}", itemId, revision, parent);
|
||||
emitChanged();
|
||||
});
|
||||
|
||||
// ItemsPropertiesUpdated(updated, removed): fine-grained property changes.
|
||||
// We invalidate wholesale rather than trying to patch individual entries —
|
||||
// the cost is one extra GetLayout on next open, and it keeps the code path
|
||||
// simple and correct. Signature matches the dbusmenu spec (a(ia{sv}) + a(ias)).
|
||||
// Patch retained rows in place. This keeps checked/radio/visible state in
|
||||
// sync without forcing another GetLayout round-trip for every state change.
|
||||
// Signature matches the dbusmenu spec (a(ia{sv}) + a(ias)).
|
||||
using PropertiesUpdate = std::vector<sdbus::Struct<std::int32_t, std::map<std::string, sdbus::Variant>>>;
|
||||
using PropertiesRemoved = std::vector<sdbus::Struct<std::int32_t, std::vector<std::string>>>;
|
||||
proxy->uponSignal("ItemsPropertiesUpdated")
|
||||
.onInterface(k_menu_interface)
|
||||
.call([this, itemId](const PropertiesUpdate& /*updated*/, const PropertiesRemoved& /*removed*/) {
|
||||
if (auto it = m_menuCache.find(itemId); it != m_menuCache.end()) {
|
||||
it->second.entriesByParent.clear();
|
||||
it->second.rootLoaded = false;
|
||||
.call([this, itemId](const PropertiesUpdate& updated, const PropertiesRemoved& removed) {
|
||||
auto it = m_menuCache.find(itemId);
|
||||
if (it == m_menuCache.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
for (const auto& itemProperties : updated) {
|
||||
const auto entryId = std::get<0>(itemProperties);
|
||||
if (entryId <= 0) {
|
||||
continue;
|
||||
}
|
||||
auto& entry = it->second.entriesById[entryId];
|
||||
if (entry.id == 0) {
|
||||
entry.id = entryId;
|
||||
}
|
||||
const auto before = entry;
|
||||
applyMenuEntryProperties(entry, std::get<1>(itemProperties), false);
|
||||
changed = changed || before != entry;
|
||||
}
|
||||
|
||||
for (const auto& removedProperties : removed) {
|
||||
const auto entryId = std::get<0>(removedProperties);
|
||||
auto entryIt = it->second.entriesById.find(entryId);
|
||||
if (entryIt == it->second.entriesById.end()) {
|
||||
continue;
|
||||
}
|
||||
const auto before = entryIt->second;
|
||||
applyMenuEntryProperties(entryIt->second, {}, false, std::get<1>(removedProperties));
|
||||
changed = changed || before != entryIt->second;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
emitChanged();
|
||||
}
|
||||
emitChanged();
|
||||
});
|
||||
|
||||
MenuCache cache;
|
||||
cache.proxy = std::move(proxy);
|
||||
cache.generation = 1;
|
||||
m_menuCache[itemId] = std::move(cache);
|
||||
kLog.debug("menuCache: persistent proxy + signals for id={}", itemId);
|
||||
} catch (const sdbus::Error& e) {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <sdbus-c++/sdbus-c++.h>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
class SessionBus;
|
||||
@@ -39,10 +40,15 @@ struct TrayItemInfo {
|
||||
struct TrayMenuEntry {
|
||||
std::int32_t id = 0;
|
||||
std::string label;
|
||||
std::string iconName;
|
||||
std::vector<std::uint8_t> iconData;
|
||||
bool enabled = true;
|
||||
bool visible = true;
|
||||
bool separator = false;
|
||||
bool hasSubmenu = false;
|
||||
bool checkmark = false;
|
||||
bool radio = false;
|
||||
std::int32_t toggleState = -1;
|
||||
|
||||
bool operator==(const TrayMenuEntry&) const = default;
|
||||
};
|
||||
@@ -80,9 +86,13 @@ public:
|
||||
private:
|
||||
struct MenuCache {
|
||||
std::unique_ptr<sdbus::IProxy> proxy;
|
||||
// Decoded children per parent-id. parentId=0 is the root menu.
|
||||
std::unordered_map<std::int32_t, std::vector<TrayMenuEntry>> entriesByParent;
|
||||
std::unordered_map<std::int32_t, TrayMenuEntry> entriesById;
|
||||
// Decoded child ids per parent-id. parentId=0 is the root menu.
|
||||
std::unordered_map<std::int32_t, std::vector<std::int32_t>> childrenByParent;
|
||||
std::unordered_set<std::int32_t> loadedParents;
|
||||
std::unordered_set<std::int32_t> loadingParents;
|
||||
std::uint32_t revision = 0;
|
||||
std::uint64_t generation = 0;
|
||||
bool rootLoaded = false;
|
||||
};
|
||||
|
||||
@@ -99,7 +109,8 @@ private:
|
||||
void dropMenuCache(const std::string& itemId);
|
||||
bool fetchMenuProperties(const std::string& itemId, const std::vector<std::int32_t>& entryIds,
|
||||
std::vector<TrayMenuEntry>& outEntries);
|
||||
bool fetchMenuSubtree(const std::string& itemId, std::int32_t parentId);
|
||||
void requestMenuSubtree(const std::string& itemId, std::int32_t parentId, bool force = false);
|
||||
void requestMenuLayoutAfterAboutToShow(const std::string& itemId, std::int32_t parentId, std::uint64_t generation);
|
||||
void sendMenuEvent(const std::string& itemId, std::int32_t entryId, const std::string& eventName);
|
||||
[[nodiscard]] bool ensureItemProxy(const std::string& itemId);
|
||||
[[nodiscard]] bool hasServiceOwner(const std::string& serviceName) const;
|
||||
|
||||
@@ -288,7 +288,16 @@ void TrayMenu::onTrayChanged() {
|
||||
close();
|
||||
return;
|
||||
}
|
||||
resizeMainSurfaceToEntries();
|
||||
rebuildScenes();
|
||||
|
||||
if (m_pendingSubmenuParentEntryId != 0 && m_submenuInstance == nullptr) {
|
||||
const auto parentId = m_pendingSubmenuParentEntryId;
|
||||
const auto rowCenterY = m_pendingSubmenuRowCenterY;
|
||||
m_pendingSubmenuParentEntryId = 0;
|
||||
m_pendingSubmenuRowCenterY = 0.0f;
|
||||
openSubmenu(parentId, rowCenterY);
|
||||
}
|
||||
}
|
||||
|
||||
void TrayMenu::toggleForItem(const std::string& itemId) {
|
||||
@@ -515,6 +524,8 @@ void TrayMenu::refreshEntries() {
|
||||
m_entries.insert(m_entries.begin(), TrayMenuEntry{
|
||||
.id = kPinToggleEntryId,
|
||||
.label = i18n::tr(pinned ? "tray.menu.unpin" : "tray.menu.pin"),
|
||||
.iconName = {},
|
||||
.iconData = {},
|
||||
.enabled = true,
|
||||
.visible = true,
|
||||
.separator = false,
|
||||
@@ -525,6 +536,8 @@ void TrayMenu::refreshEntries() {
|
||||
m_entries.push_back(TrayMenuEntry{
|
||||
.id = -1,
|
||||
.label = i18n::tr("tray.menu.empty"),
|
||||
.iconName = {},
|
||||
.iconData = {},
|
||||
.enabled = false,
|
||||
.visible = true,
|
||||
.separator = false,
|
||||
@@ -561,6 +574,20 @@ void TrayMenu::scheduleEntryRetry(int attempt) {
|
||||
}
|
||||
kLog.debug("tray menu recovered (attempt {}) for id={}", attempt + 1, capturedItemId);
|
||||
m_entries = std::move(fresh);
|
||||
if (!m_entries.empty() && trayDrawerEnabled(m_config)) {
|
||||
const bool pinned = activeItemPinned();
|
||||
m_entries.insert(m_entries.begin(), TrayMenuEntry{
|
||||
.id = kPinToggleEntryId,
|
||||
.label = i18n::tr(pinned ? "tray.menu.unpin" : "tray.menu.pin"),
|
||||
.iconName = {},
|
||||
.iconData = {},
|
||||
.enabled = true,
|
||||
.visible = true,
|
||||
.separator = false,
|
||||
.hasSubmenu = false,
|
||||
});
|
||||
}
|
||||
resizeMainSurfaceToEntries();
|
||||
rebuildScenes();
|
||||
});
|
||||
}
|
||||
@@ -575,6 +602,9 @@ uint32_t TrayMenu::submenuHeightPx() const {
|
||||
.enabled = entry.enabled,
|
||||
.separator = entry.separator,
|
||||
.hasSubmenu = entry.hasSubmenu,
|
||||
.checkmark = entry.checkmark,
|
||||
.radio = entry.radio,
|
||||
.toggleState = entry.toggleState,
|
||||
});
|
||||
}
|
||||
return static_cast<uint32_t>(ContextMenuControl::preferredHeight(entries, visibleEntryLimit(entries.size())));
|
||||
@@ -590,6 +620,9 @@ uint32_t TrayMenu::surfaceHeightPx() const {
|
||||
.enabled = entry.enabled,
|
||||
.separator = entry.separator,
|
||||
.hasSubmenu = entry.hasSubmenu,
|
||||
.checkmark = entry.checkmark,
|
||||
.radio = entry.radio,
|
||||
.toggleState = entry.toggleState,
|
||||
});
|
||||
}
|
||||
return static_cast<uint32_t>(ContextMenuControl::preferredHeight(entries, visibleEntryLimit(entries.size())));
|
||||
@@ -709,6 +742,26 @@ void TrayMenu::ensureSurface() {
|
||||
});
|
||||
}
|
||||
|
||||
void TrayMenu::resizeMainSurfaceToEntries() {
|
||||
if (m_instance == nullptr || m_instance->surface == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto desiredWidth = static_cast<std::uint32_t>(kSurfaceWidth);
|
||||
const auto desiredHeight = surfaceHeightPx();
|
||||
if (desiredHeight == 0) {
|
||||
return;
|
||||
}
|
||||
if (m_instance->surface->width() == desiredWidth && m_instance->surface->height() == desiredHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
closeSubmenu();
|
||||
if (!m_instance->surface->resize(desiredWidth, desiredHeight)) {
|
||||
m_instance->surface->requestLayout();
|
||||
}
|
||||
}
|
||||
|
||||
void TrayMenu::destroySurface() {
|
||||
if (m_instance != nullptr) {
|
||||
m_instance->inputDispatcher.setSceneRoot(nullptr);
|
||||
@@ -769,6 +822,9 @@ void TrayMenu::buildScene(MenuInstance& inst, uint32_t width, uint32_t height) {
|
||||
.enabled = entry.enabled,
|
||||
.separator = entry.separator,
|
||||
.hasSubmenu = entry.hasSubmenu,
|
||||
.checkmark = entry.checkmark,
|
||||
.radio = entry.radio,
|
||||
.toggleState = entry.toggleState,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -910,6 +966,8 @@ void TrayMenu::closeSubmenu() {
|
||||
m_submenuInstance.reset();
|
||||
m_submenuEntries.clear();
|
||||
m_submenuParentEntryId = 0;
|
||||
m_pendingSubmenuParentEntryId = 0;
|
||||
m_pendingSubmenuRowCenterY = 0.0f;
|
||||
}
|
||||
|
||||
void TrayMenu::openSubmenu(std::int32_t parentEntryId, float rowCenterY) {
|
||||
@@ -921,8 +979,12 @@ void TrayMenu::openSubmenu(std::int32_t parentEntryId, float rowCenterY) {
|
||||
|
||||
m_submenuEntries = m_tray->menuEntriesForParent(m_activeItemId, parentEntryId);
|
||||
if (m_submenuEntries.empty()) {
|
||||
m_pendingSubmenuParentEntryId = parentEntryId;
|
||||
m_pendingSubmenuRowCenterY = rowCenterY;
|
||||
return;
|
||||
}
|
||||
m_pendingSubmenuParentEntryId = 0;
|
||||
m_pendingSubmenuRowCenterY = 0.0f;
|
||||
m_submenuParentEntryId = parentEntryId;
|
||||
// Signal the server that this submenu is being opened. Matches the opened/closed
|
||||
// pairing we do for the root menu.
|
||||
@@ -1028,6 +1090,9 @@ void TrayMenu::buildSubmenuScene(MenuInstance& inst, uint32_t width, uint32_t he
|
||||
.enabled = entry.enabled,
|
||||
.separator = entry.separator,
|
||||
.hasSubmenu = entry.hasSubmenu,
|
||||
.checkmark = entry.checkmark,
|
||||
.radio = entry.radio,
|
||||
.toggleState = entry.toggleState,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ private:
|
||||
[[nodiscard]] uint32_t submenuHeightPx() const;
|
||||
[[nodiscard]] bool ownsSurface(wl_surface* surface) const;
|
||||
void ensureSurface();
|
||||
void resizeMainSurfaceToEntries();
|
||||
void destroySurface();
|
||||
void rebuildScenes();
|
||||
void prepareMainMenuFrame(MenuInstance& inst, bool needsUpdate, bool needsLayout);
|
||||
@@ -79,6 +80,8 @@ private:
|
||||
|
||||
std::vector<TrayMenuEntry> m_submenuEntries;
|
||||
std::int32_t m_submenuParentEntryId = 0;
|
||||
std::int32_t m_pendingSubmenuParentEntryId = 0;
|
||||
float m_pendingSubmenuRowCenterY = 0.0f;
|
||||
std::unique_ptr<MenuInstance> m_submenuInstance;
|
||||
|
||||
// Hyprland-only: keeps the popup surfaces in the focus whitelist so motion
|
||||
|
||||
@@ -23,6 +23,18 @@ namespace {
|
||||
|
||||
ColorSpec disabledItemColor() { return colorSpecFromRole(ColorRole::OnSurface, 0.55f); }
|
||||
|
||||
bool hasToggle(const ContextMenuControlEntry& entry) { return entry.checkmark || entry.radio; }
|
||||
|
||||
std::string toggleGlyphName(const ContextMenuControlEntry& entry) {
|
||||
if (entry.toggleState == 2) {
|
||||
return "minus";
|
||||
}
|
||||
if (entry.radio) {
|
||||
return entry.toggleState == 1 ? "circle-dot" : "circle";
|
||||
}
|
||||
return entry.toggleState == 1 ? "check" : "";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ContextMenuControl::ContextMenuControl() : Node(NodeType::Base) {}
|
||||
@@ -123,6 +135,7 @@ void ContextMenuControl::rebuildRows(Renderer& renderer) {
|
||||
|
||||
Box* rowBgPtr = nullptr;
|
||||
Label* labelPtr = nullptr;
|
||||
Glyph* togglePtr = nullptr;
|
||||
Glyph* chevronPtr = nullptr;
|
||||
|
||||
const float rowCenterY = currentY + rowHeight * 0.5f;
|
||||
@@ -148,14 +161,27 @@ void ContextMenuControl::rebuildRows(Renderer& renderer) {
|
||||
rowBg->setFrameSize(rowWidth, rowHeight);
|
||||
rowBgPtr = static_cast<Box*>(row->addChild(std::move(rowBg)));
|
||||
|
||||
const bool toggleVisible = hasToggle(entry);
|
||||
const float toggleSlot = toggleVisible ? 22.0f : 0.0f;
|
||||
const std::string toggleGlyph = toggleGlyphName(entry);
|
||||
if (!toggleGlyph.empty()) {
|
||||
auto glyph = std::make_unique<Glyph>();
|
||||
glyph->setGlyph(toggleGlyph);
|
||||
glyph->setGlyphSize(Style::fontSizeBody - 1.0f);
|
||||
glyph->setColor(entry.enabled ? enabledItemColor() : disabledItemColor());
|
||||
glyph->measure(renderer);
|
||||
glyph->setPosition(8.0f, (rowHeight - glyph->height()) * 0.5f);
|
||||
togglePtr = static_cast<Glyph*>(row->addChild(std::move(glyph)));
|
||||
}
|
||||
|
||||
std::string labelText = entry.label;
|
||||
auto label = std::make_unique<Label>();
|
||||
label->setText(labelText);
|
||||
label->setFontSize(Style::fontSizeBody);
|
||||
label->setColor(entry.enabled ? enabledItemColor() : disabledItemColor());
|
||||
label->setMaxWidth(entry.hasSubmenu ? (rowWidth - 30.0f) : (rowWidth - 16.0f));
|
||||
label->setMaxWidth(entry.hasSubmenu ? (rowWidth - 30.0f - toggleSlot) : (rowWidth - 16.0f - toggleSlot));
|
||||
label->measure(renderer);
|
||||
label->setPosition(8.0f, (rowHeight - label->height()) * 0.5f);
|
||||
label->setPosition(8.0f + toggleSlot, (rowHeight - label->height()) * 0.5f);
|
||||
labelPtr = static_cast<Label*>(row->addChild(std::move(label)));
|
||||
|
||||
if (entry.hasSubmenu) {
|
||||
@@ -188,7 +214,7 @@ void ContextMenuControl::rebuildRows(Renderer& renderer) {
|
||||
}
|
||||
|
||||
if (rowBgPtr != nullptr && labelPtr != nullptr) {
|
||||
const auto applyRowState = [rowBgPtr, labelPtr, chevronPtr, interactive, separator](bool highlighted) {
|
||||
const auto applyRowState = [rowBgPtr, labelPtr, togglePtr, chevronPtr, interactive, separator](bool highlighted) {
|
||||
rowBgPtr->setFill(highlighted ? colorSpecFromRole(ColorRole::Hover) : clearColorSpec());
|
||||
if (separator) {
|
||||
labelPtr->setColor(colorSpecFromRole(ColorRole::OnSurfaceVariant));
|
||||
@@ -196,6 +222,10 @@ void ContextMenuControl::rebuildRows(Renderer& renderer) {
|
||||
labelPtr->setColor(highlighted ? colorSpecFromRole(ColorRole::OnHover)
|
||||
: (interactive ? enabledItemColor() : disabledItemColor()));
|
||||
}
|
||||
if (togglePtr != nullptr) {
|
||||
togglePtr->setColor(highlighted ? colorSpecFromRole(ColorRole::OnHover)
|
||||
: (interactive ? enabledItemColor() : disabledItemColor()));
|
||||
}
|
||||
if (chevronPtr != nullptr) {
|
||||
chevronPtr->setColor(highlighted ? colorSpecFromRole(ColorRole::OnHover)
|
||||
: (interactive ? enabledItemColor() : disabledItemColor()));
|
||||
|
||||
@@ -20,6 +20,9 @@ struct ContextMenuControlEntry {
|
||||
bool enabled = true;
|
||||
bool separator = false;
|
||||
bool hasSubmenu = false;
|
||||
bool checkmark = false;
|
||||
bool radio = false;
|
||||
std::int32_t toggleState = -1;
|
||||
};
|
||||
|
||||
class ContextMenuControl : public Node {
|
||||
|
||||
@@ -25,6 +25,26 @@ namespace {
|
||||
.repositioned = &PopupSurface::handlePopupRepositioned,
|
||||
};
|
||||
|
||||
xdg_positioner* createPositioner(xdg_wm_base* wmBase, const PopupSurfaceConfig& config) {
|
||||
if (wmBase == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
xdg_positioner* positioner = xdg_wm_base_create_positioner(wmBase);
|
||||
if (positioner == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
xdg_positioner_set_size(positioner, static_cast<std::int32_t>(config.width),
|
||||
static_cast<std::int32_t>(config.height));
|
||||
xdg_positioner_set_anchor_rect(positioner, config.anchorX, config.anchorY, config.anchorWidth, config.anchorHeight);
|
||||
xdg_positioner_set_anchor(positioner, config.anchor);
|
||||
xdg_positioner_set_gravity(positioner, config.gravity);
|
||||
xdg_positioner_set_constraint_adjustment(positioner, config.constraintAdjustment);
|
||||
xdg_positioner_set_offset(positioner, config.offsetX, config.offsetY);
|
||||
return positioner;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PopupSurface::PopupSurface(WaylandConnection& connection) : Surface(connection) {}
|
||||
@@ -99,22 +119,13 @@ bool PopupSurface::initialize(zwlr_layer_surface_v1* parentLayerSurface, wl_outp
|
||||
}
|
||||
xdg_surface_add_listener(m_xdgSurface, &kXdgSurfaceListener, this);
|
||||
|
||||
xdg_positioner* positioner = xdg_wm_base_create_positioner(m_connection.xdgWmBase());
|
||||
xdg_positioner* positioner = createPositioner(m_connection.xdgWmBase(), m_config);
|
||||
if (positioner == nullptr) {
|
||||
destroyRoleObjects();
|
||||
destroySurface();
|
||||
return false;
|
||||
}
|
||||
|
||||
xdg_positioner_set_size(positioner, static_cast<std::int32_t>(m_config.width),
|
||||
static_cast<std::int32_t>(m_config.height));
|
||||
xdg_positioner_set_anchor_rect(positioner, m_config.anchorX, m_config.anchorY, m_config.anchorWidth,
|
||||
m_config.anchorHeight);
|
||||
xdg_positioner_set_anchor(positioner, m_config.anchor);
|
||||
xdg_positioner_set_gravity(positioner, m_config.gravity);
|
||||
xdg_positioner_set_constraint_adjustment(positioner, m_config.constraintAdjustment);
|
||||
xdg_positioner_set_offset(positioner, m_config.offsetX, m_config.offsetY);
|
||||
|
||||
m_popup = xdg_surface_get_popup(m_xdgSurface, nullptr, positioner);
|
||||
xdg_positioner_destroy(positioner);
|
||||
if (m_popup == nullptr) {
|
||||
@@ -146,6 +157,41 @@ bool PopupSurface::initialize(zwlr_layer_surface_v1* parentLayerSurface, wl_outp
|
||||
|
||||
void PopupSurface::setDismissedCallback(std::function<void()> callback) { m_dismissedCallback = std::move(callback); }
|
||||
|
||||
bool PopupSurface::resize(std::uint32_t width, std::uint32_t height) {
|
||||
if (m_surface == nullptr || m_popup == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
width = std::max(width, 1u);
|
||||
height = std::max(height, 1u);
|
||||
if (m_config.width == width && m_config.height == height && Surface::width() == width &&
|
||||
Surface::height() == height) {
|
||||
return true;
|
||||
}
|
||||
|
||||
m_config.width = width;
|
||||
m_config.height = height;
|
||||
m_pendingWidth = width;
|
||||
m_pendingHeight = height;
|
||||
|
||||
// Update our render target immediately so async menu hydration does not leave
|
||||
// the next frame clipped to the placeholder popup size while waiting for the
|
||||
// compositor's reposition configure.
|
||||
Surface::onConfigure(width, height);
|
||||
|
||||
if (xdg_popup_get_version(m_popup) >= XDG_POPUP_REPOSITION_SINCE_VERSION) {
|
||||
xdg_positioner* positioner = createPositioner(m_connection.xdgWmBase(), m_config);
|
||||
if (positioner != nullptr) {
|
||||
xdg_popup_reposition(m_popup, positioner, ++m_repositionToken);
|
||||
xdg_positioner_destroy(positioner);
|
||||
}
|
||||
}
|
||||
|
||||
wl_surface_commit(m_surface);
|
||||
wl_display_flush(m_connection.display());
|
||||
return true;
|
||||
}
|
||||
|
||||
void PopupSurface::handleXdgSurfaceConfigure(void* data, xdg_surface* surface, std::uint32_t serial) {
|
||||
auto* self = static_cast<PopupSurface*>(data);
|
||||
xdg_surface_ack_configure(surface, serial);
|
||||
@@ -211,22 +257,13 @@ bool PopupSurface::initializeAsChild(xdg_surface* parentXdgSurface, wl_output* o
|
||||
}
|
||||
xdg_surface_add_listener(m_xdgSurface, &kXdgSurfaceListener, this);
|
||||
|
||||
xdg_positioner* positioner = xdg_wm_base_create_positioner(m_connection.xdgWmBase());
|
||||
xdg_positioner* positioner = createPositioner(m_connection.xdgWmBase(), m_config);
|
||||
if (positioner == nullptr) {
|
||||
destroyRoleObjects();
|
||||
destroySurface();
|
||||
return false;
|
||||
}
|
||||
|
||||
xdg_positioner_set_size(positioner, static_cast<std::int32_t>(m_config.width),
|
||||
static_cast<std::int32_t>(m_config.height));
|
||||
xdg_positioner_set_anchor_rect(positioner, m_config.anchorX, m_config.anchorY, m_config.anchorWidth,
|
||||
m_config.anchorHeight);
|
||||
xdg_positioner_set_anchor(positioner, m_config.anchor);
|
||||
xdg_positioner_set_gravity(positioner, m_config.gravity);
|
||||
xdg_positioner_set_constraint_adjustment(positioner, m_config.constraintAdjustment);
|
||||
xdg_positioner_set_offset(positioner, m_config.offsetX, m_config.offsetY);
|
||||
|
||||
// popup-of-popup: pass the parent xdg_surface directly
|
||||
m_popup = xdg_surface_get_popup(m_xdgSurface, parentXdgSurface, positioner);
|
||||
xdg_positioner_destroy(positioner);
|
||||
|
||||
@@ -35,6 +35,7 @@ public:
|
||||
bool initialize() override { return false; }
|
||||
bool initialize(zwlr_layer_surface_v1* parentLayerSurface, wl_output* output, PopupSurfaceConfig config);
|
||||
bool initializeAsChild(xdg_surface* parentXdgSurface, wl_output* output, PopupSurfaceConfig config);
|
||||
bool resize(std::uint32_t width, std::uint32_t height);
|
||||
|
||||
void setDismissedCallback(std::function<void()> callback);
|
||||
|
||||
@@ -61,6 +62,7 @@ private:
|
||||
std::function<void()> m_dismissedCallback;
|
||||
std::uint32_t m_pendingWidth = 0;
|
||||
std::uint32_t m_pendingHeight = 0;
|
||||
std::uint32_t m_repositionToken = 0;
|
||||
std::int32_t m_configuredX = 0;
|
||||
std::int32_t m_configuredY = 0;
|
||||
bool m_enrolledInGrabHost = false;
|
||||
|
||||
Reference in New Issue
Block a user