mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
Merge branch 'noctalia-dev:v5' into v5
This commit is contained in:
@@ -43,7 +43,8 @@ jobs:
|
||||
cairo pango \
|
||||
libxkbcommon \
|
||||
sdbus-cpp libpipewire \
|
||||
pam curl libwebp
|
||||
pam curl libwebp \
|
||||
librsvg polkit
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ sudo dnf install meson gcc-c++ just \
|
||||
cairo-devel pango-devel \
|
||||
libxkbcommon-devel glib2-devel \
|
||||
sdbus-cpp-devel pipewire-devel \
|
||||
pam-devel polkit-devel libcurl-devel libwebp-devel
|
||||
pam-devel polkit-devel libcurl-devel libwebp-devel librsvg2-devel
|
||||
```
|
||||
|
||||
### Arch
|
||||
@@ -29,7 +29,7 @@ sudo pacman -S meson gcc just \
|
||||
cairo pango \
|
||||
libxkbcommon glib2 \
|
||||
sdbus-cpp libpipewire polkit \
|
||||
pam curl libwebp
|
||||
pam curl libwebp librsvg
|
||||
```
|
||||
|
||||
### Debian / Ubuntu
|
||||
@@ -43,7 +43,7 @@ sudo apt install meson g++ just \
|
||||
libxkbcommon-dev libglib2.0-dev \
|
||||
libsdbus-c++-dev libpipewire-0.3-dev \
|
||||
libpam0g-dev libpolkit-agent-1-dev libpolkit-gobject-1-dev \
|
||||
libcurl4-openssl-dev libwebp-dev
|
||||
libcurl4-openssl-dev libwebp-dev librsvg2-dev
|
||||
```
|
||||
|
||||
Vendored dependencies, with no system package needed: `Wuffs`, `nanosvg`, `tomlplusplus`, `tinyexpr`,
|
||||
|
||||
@@ -675,6 +675,8 @@
|
||||
"picker": {
|
||||
"placeholder": "Search widgets...",
|
||||
"empty": "No widgets found",
|
||||
"instance-toggle": "New Instance",
|
||||
"instance-description": "New instance for type \"{type}\"",
|
||||
"create-label": "New {label} Instance",
|
||||
"create-description": "Choose an id for type \"{type}\""
|
||||
},
|
||||
@@ -691,6 +693,7 @@
|
||||
"inspector": {
|
||||
"edit-title": "Editing widget",
|
||||
"add-title": "Add widget to {lane}",
|
||||
"add-instance-title": "Add {widget} widget to {lane}",
|
||||
"move-to-lane": "Move to {lane}"
|
||||
},
|
||||
"detail": {
|
||||
|
||||
@@ -551,7 +551,6 @@ _noctalia_sources = files(
|
||||
'src/ui/controls/label.cpp',
|
||||
'src/ui/controls/list_editor.cpp',
|
||||
'src/ui/controls/progress_bar.cpp',
|
||||
'src/ui/controls/popup_window.cpp',
|
||||
'src/ui/controls/radio_button.cpp',
|
||||
'src/ui/controls/scroll_view.cpp',
|
||||
'src/ui/controls/search_picker.cpp',
|
||||
|
||||
+26
-9
@@ -30,7 +30,6 @@
|
||||
#include "system/distro_info.h"
|
||||
#include "time/time_format.h"
|
||||
#include "ui/controls/input.h"
|
||||
#include "ui/controls/popup_window.h"
|
||||
#include "ui/dialogs/color_picker_dialog.h"
|
||||
#include "ui/dialogs/file_dialog.h"
|
||||
#include "ui/dialogs/glyph_picker_dialog.h"
|
||||
@@ -403,6 +402,8 @@ void Application::initServices() {
|
||||
m_configService.addReloadCallback([this]() { m_idleManager.reload(m_configService.config().idle); });
|
||||
|
||||
m_hookManager.setCommandRunner([this](const std::string& command) { return runUserCommand(command); });
|
||||
m_hookManager.setBlockingCommandRunner(
|
||||
[this](const std::string& command) { return runUserCommandBlocking(command); });
|
||||
m_hookManager.reload(m_configService.config().hooks);
|
||||
m_configService.addReloadCallback([this]() { m_hookManager.reload(m_configService.config().hooks); });
|
||||
m_nightLightManager.reload(m_configService.config().nightlight);
|
||||
@@ -757,9 +758,9 @@ void Application::initUi() {
|
||||
m_lockScreen.setSessionHooks([this]() { m_hookManager.fire(HookKind::SessionLocked); },
|
||||
[this]() { m_hookManager.fire(HookKind::SessionUnlocked); });
|
||||
|
||||
m_sessionActionHooks.onLogout = [this]() { m_hookManager.fire(HookKind::LoggingOut); };
|
||||
m_sessionActionHooks.onReboot = [this]() { m_hookManager.fire(HookKind::Rebooting); };
|
||||
m_sessionActionHooks.onShutdown = [this]() { m_hookManager.fire(HookKind::ShuttingDown); };
|
||||
m_sessionActionHooks.onLogout = [this]() { return m_hookManager.fireBlocking(HookKind::LoggingOut); };
|
||||
m_sessionActionHooks.onReboot = [this]() { return m_hookManager.fireBlocking(HookKind::Rebooting); };
|
||||
m_sessionActionHooks.onShutdown = [this]() { return m_hookManager.fireBlocking(HookKind::ShuttingDown); };
|
||||
|
||||
m_wayland.setPointerEventCallback([this](const PointerEvent& event) {
|
||||
if (m_lockScreen.isActive()) {
|
||||
@@ -809,11 +810,7 @@ void Application::initUi() {
|
||||
m_fileDialogPopup.onKeyboardEvent(event);
|
||||
return;
|
||||
}
|
||||
if (PopupWindow::dispatchKeyboardEvent(m_wayland.lastKeyboardSurface(), event)) {
|
||||
return;
|
||||
}
|
||||
if (m_settingsWindow.isOpen() && m_settingsWindow.wlSurface() != nullptr &&
|
||||
m_wayland.lastKeyboardSurface() == m_settingsWindow.wlSurface()) {
|
||||
if (m_settingsWindow.ownsKeyboardSurface(m_wayland.lastKeyboardSurface())) {
|
||||
m_settingsWindow.onKeyboardEvent(event);
|
||||
return;
|
||||
}
|
||||
@@ -1165,6 +1162,26 @@ bool Application::runUserCommand(const std::string& command) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Application::runUserCommandBlocking(const std::string& command) {
|
||||
constexpr std::string_view prefix = "noctalia:";
|
||||
|
||||
if (command.rfind(prefix, 0) == 0) {
|
||||
const std::string response = m_ipcService.execute(command.substr(prefix.size()));
|
||||
if (response.rfind("error:", 0) == 0) {
|
||||
kLog.warn("IPC command '{}' failed: {}", command, response.substr(0, response.find('\n')));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto result = process::runSync(command);
|
||||
if (!result) {
|
||||
kLog.warn("command failed: {} exit_code={} stderr={}", command, result.exitCode, result.err);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Application::runIdleCommand(const std::string& command) { return runUserCommand(command); }
|
||||
|
||||
void Application::onIconThemeChanged() {
|
||||
|
||||
@@ -110,6 +110,7 @@ private:
|
||||
void syncNotificationDaemon();
|
||||
void syncPolkitAgent();
|
||||
bool runUserCommand(const std::string& command);
|
||||
bool runUserCommandBlocking(const std::string& command);
|
||||
bool runIdleCommand(const std::string& command);
|
||||
void onIconThemeChanged();
|
||||
void onUpowerStateChangedForHooks();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "config/config_service.h"
|
||||
|
||||
#include "core/build_info.h"
|
||||
#include "core/deferred_call.h"
|
||||
#include "core/log.h"
|
||||
#include "ipc/ipc_service.h"
|
||||
#include "notification/notification_manager.h"
|
||||
@@ -314,9 +315,19 @@ void ConfigService::addReloadCallback(ReloadCallback callback) { m_reloadCallbac
|
||||
void ConfigService::setNotificationManager(NotificationManager* manager) {
|
||||
m_notificationManager = manager;
|
||||
if (m_notificationManager != nullptr && !m_pendingError.empty()) {
|
||||
m_configErrorNotificationId =
|
||||
m_notificationManager->addInternal("Noctalia", "Config parse error", m_pendingError, Urgency::Critical, 0);
|
||||
const std::string pendingError = std::move(m_pendingError);
|
||||
m_pendingError.clear();
|
||||
DeferredCall::callLater([this, pendingError]() {
|
||||
if (m_notificationManager == nullptr) {
|
||||
m_pendingError = pendingError;
|
||||
return;
|
||||
}
|
||||
if (m_configErrorNotificationId != 0) {
|
||||
m_notificationManager->close(m_configErrorNotificationId);
|
||||
}
|
||||
m_configErrorNotificationId =
|
||||
m_notificationManager->addInternal("Noctalia", "Config parse error", pendingError, Urgency::Critical, 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -677,6 +688,7 @@ void ConfigService::loadOverridesFromFile() {
|
||||
m_defaultWallpaperPath.clear();
|
||||
m_monitorWallpaperPaths.clear();
|
||||
m_setupWizardCompleted = false;
|
||||
m_overridesParseError.clear();
|
||||
|
||||
if (m_overridesPath.empty() || !std::filesystem::exists(m_overridesPath)) {
|
||||
return;
|
||||
@@ -686,7 +698,12 @@ void ConfigService::loadOverridesFromFile() {
|
||||
try {
|
||||
m_overridesTable = toml::parse_file(m_overridesPath);
|
||||
} catch (const toml::parse_error& e) {
|
||||
kLog.warn("parse error in {}: {}", m_overridesPath, e.what());
|
||||
const auto& src = e.source();
|
||||
kLog.warn("parse error in {} at line {}, column {}: {}", m_overridesPath, src.begin.line, src.begin.column,
|
||||
e.description());
|
||||
m_overridesParseError =
|
||||
std::format("{} line {}, column {}: {}", std::filesystem::path(m_overridesPath).filename().string(),
|
||||
src.begin.line, src.begin.column, e.description());
|
||||
m_overridesTable = toml::table{};
|
||||
return;
|
||||
}
|
||||
@@ -695,6 +712,28 @@ void ConfigService::loadOverridesFromFile() {
|
||||
extractWallpaperFromOverrides();
|
||||
}
|
||||
|
||||
void ConfigService::setConfigParseError(std::string parseError) {
|
||||
if (parseError.empty()) {
|
||||
// Dismiss any previous config-error notification.
|
||||
if (m_notificationManager != nullptr && m_configErrorNotificationId != 0) {
|
||||
m_notificationManager->close(m_configErrorNotificationId);
|
||||
m_configErrorNotificationId = 0;
|
||||
}
|
||||
m_pendingError.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_notificationManager != nullptr) {
|
||||
if (m_configErrorNotificationId != 0) {
|
||||
m_notificationManager->close(m_configErrorNotificationId);
|
||||
}
|
||||
m_configErrorNotificationId =
|
||||
m_notificationManager->addInternal("Noctalia", "Config parse error", parseError, Urgency::Critical, 0);
|
||||
} else {
|
||||
m_pendingError = std::move(parseError);
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigService::deepMerge(toml::table& base, const toml::table& overlay) {
|
||||
for (const auto& [k, v] : overlay) {
|
||||
if (const auto* overlayTbl = v.as_table()) {
|
||||
@@ -846,6 +885,7 @@ void ConfigService::loadAll() {
|
||||
});
|
||||
m_config.bars.push_back(BarConfig{});
|
||||
m_config.controlCenter.shortcuts = defaultControlCenterShortcuts();
|
||||
setConfigParseError(m_overridesParseError);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -857,25 +897,10 @@ void ConfigService::loadAll() {
|
||||
kLog.warn("config parse error: {}", semanticError);
|
||||
}
|
||||
|
||||
const std::string parseError = !firstError.empty() ? firstError : semanticError;
|
||||
if (parseError.empty()) {
|
||||
// Dismiss any previous config-error notification.
|
||||
if (m_notificationManager != nullptr && m_configErrorNotificationId != 0) {
|
||||
m_notificationManager->close(m_configErrorNotificationId);
|
||||
m_configErrorNotificationId = 0;
|
||||
}
|
||||
m_pendingError.clear();
|
||||
} else {
|
||||
if (m_notificationManager != nullptr) {
|
||||
if (m_configErrorNotificationId != 0) {
|
||||
m_notificationManager->close(m_configErrorNotificationId);
|
||||
}
|
||||
m_configErrorNotificationId =
|
||||
m_notificationManager->addInternal("Noctalia", "Config parse error", parseError, Urgency::Critical, 0);
|
||||
} else {
|
||||
m_pendingError = parseError;
|
||||
}
|
||||
}
|
||||
const std::string parseError = !firstError.empty() ? firstError
|
||||
: !m_overridesParseError.empty() ? m_overridesParseError
|
||||
: semanticError;
|
||||
setConfigParseError(parseError);
|
||||
}
|
||||
|
||||
void ConfigService::parseTable(const toml::table& tbl) { parseTableInto(tbl, m_config, true); }
|
||||
|
||||
@@ -97,6 +97,7 @@ private:
|
||||
void setupWatch();
|
||||
void fireReloadCallbacks();
|
||||
void loadOverridesFromFile();
|
||||
void setConfigParseError(std::string parseError);
|
||||
bool writeOverridesToFile();
|
||||
void extractWallpaperFromOverrides();
|
||||
|
||||
@@ -116,6 +117,7 @@ private:
|
||||
bool m_setupWizardCompleted = false;
|
||||
mutable std::unordered_map<std::string, bool> m_effectiveOverrideCache;
|
||||
|
||||
std::string m_overridesParseError;
|
||||
std::string m_pendingError; // parse error from initial load, sent as notification once manager is wired up
|
||||
uint32_t m_configErrorNotificationId = 0; // ID of the active config-error notification, 0 if none
|
||||
NotificationManager* m_notificationManager = nullptr;
|
||||
|
||||
@@ -12,23 +12,34 @@ namespace {
|
||||
|
||||
void HookManager::setCommandRunner(CommandRunner runner) { m_runner = std::move(runner); }
|
||||
|
||||
void HookManager::setBlockingCommandRunner(CommandRunner runner) { m_blockingRunner = std::move(runner); }
|
||||
|
||||
void HookManager::reload(const HooksConfig& config) { m_config = config; }
|
||||
|
||||
void HookManager::fire(HookKind kind) const {
|
||||
if (kind == HookKind::Count || !m_runner) {
|
||||
return;
|
||||
void HookManager::fire(HookKind kind) const { (void)fireWithRunner(kind, m_runner); }
|
||||
|
||||
bool HookManager::fireBlocking(HookKind kind) const {
|
||||
return fireWithRunner(kind, m_blockingRunner ? m_blockingRunner : m_runner);
|
||||
}
|
||||
|
||||
bool HookManager::fireWithRunner(HookKind kind, const CommandRunner& runner) const {
|
||||
if (kind == HookKind::Count || !runner) {
|
||||
return false;
|
||||
}
|
||||
const auto& cmds = m_config.commands[static_cast<std::size_t>(kind)];
|
||||
if (cmds.empty()) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
const std::string_view name = hookKindKey(kind);
|
||||
kLog.debug("hook '{}' running {} command(s)", name, cmds.size());
|
||||
bool ok = true;
|
||||
for (const auto& cmd : cmds) {
|
||||
if (!m_runner(cmd)) {
|
||||
if (!runner(cmd)) {
|
||||
kLog.warn("hook '{}' command failed: {}", name, cmd);
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
void HookManager::fire(HookKind kind, std::initializer_list<EnvVar> env) const {
|
||||
|
||||
@@ -13,13 +13,18 @@ public:
|
||||
using EnvVar = std::pair<const char*, std::string>;
|
||||
|
||||
void setCommandRunner(CommandRunner runner);
|
||||
void setBlockingCommandRunner(CommandRunner runner);
|
||||
void reload(const HooksConfig& config);
|
||||
void fire(HookKind kind) const;
|
||||
[[nodiscard]] bool fireBlocking(HookKind kind) const;
|
||||
void fire(HookKind kind, std::initializer_list<EnvVar> env) const;
|
||||
|
||||
[[nodiscard]] const HooksConfig& config() const noexcept { return m_config; }
|
||||
|
||||
private:
|
||||
[[nodiscard]] bool fireWithRunner(HookKind kind, const CommandRunner& runner) const;
|
||||
|
||||
HooksConfig m_config;
|
||||
CommandRunner m_runner;
|
||||
CommandRunner m_blockingRunner;
|
||||
};
|
||||
|
||||
@@ -146,6 +146,9 @@ void ClockWidget::doLayout(Renderer& renderer, float containerWidth, float conta
|
||||
renderer.measureText(m_lastPrimaryText, primaryFontSize, true, 0.0f, 1, TextAlign::Start);
|
||||
const auto secondaryMetrics =
|
||||
renderer.measureText(m_lastSecondaryText, secondaryFontSize, false, 0.0f, 1, TextAlign::Start);
|
||||
const float primaryInkWidth = std::max(0.0f, primaryMetrics.inkRight - primaryMetrics.inkLeft);
|
||||
const float secondaryInkWidth = std::max(0.0f, secondaryMetrics.inkRight - secondaryMetrics.inkLeft);
|
||||
width = std::max({width, primaryInkWidth, secondaryInkWidth});
|
||||
const float centerX = width * 0.5f;
|
||||
const float primaryInkCenterX = (primaryMetrics.inkLeft + primaryMetrics.inkRight) * 0.5f;
|
||||
const float secondaryInkCenterX = (secondaryMetrics.inkLeft + secondaryMetrics.inkRight) * 0.5f;
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <xkbcommon/xkbcommon-keysyms.h>
|
||||
|
||||
@@ -116,6 +118,18 @@ namespace {
|
||||
return true;
|
||||
}
|
||||
|
||||
void runPowerAction(std::function<bool()> hook, bool (*action)(), std::string_view actionName) {
|
||||
std::thread([hook = std::move(hook), action, actionName = std::string(actionName)]() mutable {
|
||||
if (hook && !hook()) {
|
||||
kLog.warn("{} cancelled because a configured hook failed", actionName);
|
||||
return;
|
||||
}
|
||||
if (!action()) {
|
||||
kLog.warn("{} failed after hooks completed", actionName);
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void SessionPanel::create() {
|
||||
@@ -227,28 +241,13 @@ void SessionPanel::activateSelected() {
|
||||
void SessionPanel::invokeAction(ActionId id) {
|
||||
switch (id) {
|
||||
case ActionId::Logout:
|
||||
if (m_actionHooks.onLogout) {
|
||||
m_actionHooks.onLogout();
|
||||
}
|
||||
if (!doLogout()) {
|
||||
notify::error("Noctalia", i18n::tr("session.errors.logout-title"), i18n::tr("session.errors.logout-body"));
|
||||
}
|
||||
runPowerAction(m_actionHooks.onLogout, doLogout, "logout");
|
||||
break;
|
||||
case ActionId::Reboot:
|
||||
if (m_actionHooks.onReboot) {
|
||||
m_actionHooks.onReboot();
|
||||
}
|
||||
if (!doReboot()) {
|
||||
notify::error("Noctalia", i18n::tr("session.errors.reboot-title"), i18n::tr("session.errors.reboot-body"));
|
||||
}
|
||||
runPowerAction(m_actionHooks.onReboot, doReboot, "reboot");
|
||||
break;
|
||||
case ActionId::Shutdown:
|
||||
if (m_actionHooks.onShutdown) {
|
||||
m_actionHooks.onShutdown();
|
||||
}
|
||||
if (!doShutdown()) {
|
||||
notify::error("Noctalia", i18n::tr("session.errors.shutdown-title"), i18n::tr("session.errors.shutdown-body"));
|
||||
}
|
||||
runPowerAction(m_actionHooks.onShutdown, doShutdown, "shutdown");
|
||||
break;
|
||||
case ActionId::Lock:
|
||||
if (!doLock()) {
|
||||
|
||||
@@ -15,9 +15,9 @@ class Renderer;
|
||||
class ConfigService;
|
||||
|
||||
struct SessionActionHooks {
|
||||
std::function<void()> onLogout;
|
||||
std::function<void()> onReboot;
|
||||
std::function<void()> onShutdown;
|
||||
std::function<bool()> onLogout;
|
||||
std::function<bool()> onReboot;
|
||||
std::function<bool()> onShutdown;
|
||||
};
|
||||
|
||||
class SessionPanel : public Panel {
|
||||
|
||||
@@ -49,7 +49,6 @@ namespace {
|
||||
constexpr Logger kLog("settings");
|
||||
constexpr std::int32_t kActionSupportReport = 1;
|
||||
constexpr std::int32_t kActionFlattenedConfig = 2;
|
||||
constexpr std::string_view kCreateInstancePrefix = "create-instance:";
|
||||
|
||||
constexpr float kWindowWidth = 1080.0f;
|
||||
constexpr float kWindowHeight = 600.0f;
|
||||
@@ -112,27 +111,6 @@ namespace {
|
||||
return key;
|
||||
}
|
||||
|
||||
bool isCreateInstanceValue(std::string_view value) { return value.starts_with(kCreateInstancePrefix); }
|
||||
|
||||
std::string createInstanceTypeFromValue(std::string_view value) {
|
||||
if (!isCreateInstanceValue(value)) {
|
||||
return {};
|
||||
}
|
||||
value.remove_prefix(kCreateInstancePrefix.size());
|
||||
return std::string(value);
|
||||
}
|
||||
|
||||
std::string pathKey(const std::vector<std::string>& path) {
|
||||
std::string out;
|
||||
for (const auto& part : path) {
|
||||
if (!out.empty()) {
|
||||
out.push_back('.');
|
||||
}
|
||||
out += part;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool isBarWidgetListPath(const std::vector<std::string>& path) {
|
||||
if (path.size() < 3 || path.front() != "bar") {
|
||||
return false;
|
||||
@@ -199,6 +177,16 @@ float SettingsWindow::uiScale() const {
|
||||
return std::max(0.1f, m_config->config().shell.uiScale);
|
||||
}
|
||||
|
||||
bool SettingsWindow::ownsKeyboardSurface(wl_surface* surface) const noexcept {
|
||||
if (!isOpen() || surface == nullptr || m_surface == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (surface == m_surface->wlSurface()) {
|
||||
return true;
|
||||
}
|
||||
return m_widgetAddPopup != nullptr && m_widgetAddPopup->wlSurface() == surface;
|
||||
}
|
||||
|
||||
void SettingsWindow::open() {
|
||||
if (m_wayland == nullptr || m_renderContext == nullptr || !m_wayland->hasXdgShell()) {
|
||||
return;
|
||||
@@ -483,8 +471,10 @@ void SettingsWindow::openBarWidgetAddPopup(const std::vector<std::string>& laneP
|
||||
}
|
||||
|
||||
if (m_widgetAddPopup == nullptr) {
|
||||
m_widgetAddPopup = std::make_unique<settings::WidgetAddPopup>(*m_wayland, *m_renderContext);
|
||||
m_widgetAddPopup->setOnSelect([this](const std::vector<std::string>& selectedLanePath, const std::string& value) {
|
||||
m_widgetAddPopup = std::make_unique<settings::WidgetAddPopup>();
|
||||
m_widgetAddPopup->initialize(*m_wayland, *m_config, *m_renderContext);
|
||||
m_widgetAddPopup->setOnSelect([this](const std::vector<std::string>& selectedLanePath, const std::string& value,
|
||||
const std::string& newInstanceType, const std::string& newInstanceId) {
|
||||
if (value.empty() || m_config == nullptr) {
|
||||
return;
|
||||
}
|
||||
@@ -497,10 +487,11 @@ void SettingsWindow::openBarWidgetAddPopup(const std::vector<std::string>& laneP
|
||||
m_renamingWidgetName.clear();
|
||||
m_editingWidgetName.clear();
|
||||
|
||||
if (const auto type = createInstanceTypeFromValue(value); !type.empty()) {
|
||||
m_creatingWidgetType = type;
|
||||
m_openWidgetPickerPath = pathKey(selectedLanePath);
|
||||
requestSceneRebuild();
|
||||
if (!newInstanceType.empty() && !newInstanceId.empty()) {
|
||||
laneItems.push_back(newInstanceId);
|
||||
m_creatingWidgetType.clear();
|
||||
m_openWidgetPickerPath.clear();
|
||||
setSettingOverrides({{{"widget", newInstanceId, "type"}, newInstanceType}, {selectedLanePath, laneItems}});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -516,8 +507,8 @@ void SettingsWindow::openBarWidgetAddPopup(const std::vector<std::string>& laneP
|
||||
output = m_output;
|
||||
}
|
||||
|
||||
m_widgetAddPopup->open(m_surface->xdgSurface(), output, m_wayland->lastInputSerial(), anchorButton, lanePath,
|
||||
m_config->config(), uiScale(), PopupWindow::AnchorMode::CenterOnAnchor);
|
||||
m_widgetAddPopup->open(m_surface->xdgSurface(), output, m_wayland->lastInputSerial(), anchorButton,
|
||||
m_surface->wlSurface(), lanePath, m_config->config(), uiScale());
|
||||
}
|
||||
|
||||
void SettingsWindow::saveSupportReport() {
|
||||
|
||||
@@ -41,6 +41,7 @@ public:
|
||||
[[nodiscard]] wl_surface* wlSurface() const noexcept {
|
||||
return m_surface != nullptr ? m_surface->wlSurface() : nullptr;
|
||||
}
|
||||
[[nodiscard]] bool ownsKeyboardSurface(wl_surface* surface) const noexcept;
|
||||
|
||||
[[nodiscard]] bool onPointerEvent(const PointerEvent& event);
|
||||
void onKeyboardEvent(const KeyboardEvent& event);
|
||||
|
||||
@@ -1,27 +1,34 @@
|
||||
#include "shell/settings/widget_add_popup.h"
|
||||
|
||||
#include "config/config_service.h"
|
||||
#include "core/deferred_call.h"
|
||||
#include "i18n/i18n.h"
|
||||
#include "render/render_context.h"
|
||||
#include "render/scene/input_area.h"
|
||||
#include "render/scene/node.h"
|
||||
#include "shell/settings/widget_settings_registry.h"
|
||||
#include "ui/controls/button.h"
|
||||
#include "ui/controls/flex.h"
|
||||
#include "ui/controls/input.h"
|
||||
#include "ui/controls/label.h"
|
||||
#include "ui/controls/toggle.h"
|
||||
#include "ui/palette.h"
|
||||
#include "ui/style.h"
|
||||
#include "wayland/wayland_connection.h"
|
||||
#include "wayland/wayland_seat.h"
|
||||
#include "xdg-shell-client-protocol.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
|
||||
namespace settings {
|
||||
namespace {
|
||||
|
||||
constexpr std::string_view kCreateInstancePrefix = "create-instance:";
|
||||
|
||||
std::string laneLabel(std::string_view lane) {
|
||||
if (lane == "start") {
|
||||
return i18n::tr("settings.entities.widget.lanes.start");
|
||||
@@ -44,43 +51,172 @@ namespace settings {
|
||||
return label;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
WidgetAddPopup::WidgetAddPopup(WaylandConnection& wayland, RenderContext& renderContext)
|
||||
: m_wayland(wayland), m_renderContext(renderContext),
|
||||
m_popup(std::make_unique<PopupWindow>(wayland, renderContext)) {
|
||||
m_popup->setOnDismissed([this]() {
|
||||
m_lanePath.clear();
|
||||
m_searchPicker = nullptr;
|
||||
m_focusSearchOnOpen = false;
|
||||
if (m_onDismissed) {
|
||||
m_onDismissed();
|
||||
std::string toLowerAscii(std::string text) {
|
||||
std::transform(text.begin(), text.end(), text.begin(),
|
||||
[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
|
||||
return text;
|
||||
}
|
||||
|
||||
void sortSearchOptions(std::vector<SearchPickerOption>& options) {
|
||||
std::sort(options.begin(), options.end(), [](const SearchPickerOption& a, const SearchPickerOption& b) {
|
||||
const std::string aLabel = toLowerAscii(a.label);
|
||||
const std::string bLabel = toLowerAscii(b.label);
|
||||
if (aLabel == bLabel) {
|
||||
return a.value < b.value;
|
||||
}
|
||||
return aLabel < bLabel;
|
||||
});
|
||||
}
|
||||
|
||||
WidgetAddPopup::~WidgetAddPopup() = default;
|
||||
void collectWidgetReferenceNames(const std::vector<std::string>& widgets, std::unordered_set<std::string>& seen) {
|
||||
for (const auto& widget : widgets) {
|
||||
seen.insert(widget);
|
||||
}
|
||||
}
|
||||
|
||||
bool widgetReferenceNameExists(const Config& cfg, std::string_view name) {
|
||||
const std::string key(name);
|
||||
if (isBuiltInWidgetType(name) || cfg.widgets.contains(key)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unordered_set<std::string> seen;
|
||||
for (const auto& bar : cfg.bars) {
|
||||
collectWidgetReferenceNames(bar.startWidgets, seen);
|
||||
collectWidgetReferenceNames(bar.centerWidgets, seen);
|
||||
collectWidgetReferenceNames(bar.endWidgets, seen);
|
||||
for (const auto& ovr : bar.monitorOverrides) {
|
||||
if (ovr.startWidgets.has_value()) {
|
||||
collectWidgetReferenceNames(*ovr.startWidgets, seen);
|
||||
}
|
||||
if (ovr.centerWidgets.has_value()) {
|
||||
collectWidgetReferenceNames(*ovr.centerWidgets, seen);
|
||||
}
|
||||
if (ovr.endWidgets.has_value()) {
|
||||
collectWidgetReferenceNames(*ovr.endWidgets, seen);
|
||||
}
|
||||
}
|
||||
}
|
||||
return seen.contains(key);
|
||||
}
|
||||
|
||||
bool isValidWidgetInstanceId(std::string_view id) {
|
||||
if (id.empty()) {
|
||||
return false;
|
||||
}
|
||||
for (const unsigned char c : id) {
|
||||
if (!std::isalnum(c) && c != '_' && c != '-') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string normalizedWidgetInstanceBase(std::string_view type) {
|
||||
std::string out;
|
||||
out.reserve(type.size());
|
||||
bool lastUnderscore = false;
|
||||
for (const unsigned char c : type) {
|
||||
if (std::isalnum(c)) {
|
||||
out.push_back(static_cast<char>(std::tolower(c)));
|
||||
lastUnderscore = false;
|
||||
} else if (!lastUnderscore && !out.empty()) {
|
||||
out.push_back('_');
|
||||
lastUnderscore = true;
|
||||
}
|
||||
}
|
||||
while (!out.empty() && out.back() == '_') {
|
||||
out.pop_back();
|
||||
}
|
||||
return out.empty() ? std::string("widget") : out;
|
||||
}
|
||||
|
||||
std::string nextWidgetInstanceId(const Config& cfg, std::string_view type) {
|
||||
const std::string base = normalizedWidgetInstanceBase(type);
|
||||
for (std::size_t index = 2; index < 10000; ++index) {
|
||||
const std::string candidate = base + "_" + std::to_string(index);
|
||||
if (!widgetReferenceNameExists(cfg, candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return base + "_custom";
|
||||
}
|
||||
|
||||
std::string trimmedText(std::string_view text) {
|
||||
std::size_t start = 0;
|
||||
while (start < text.size() && std::isspace(static_cast<unsigned char>(text[start]))) {
|
||||
++start;
|
||||
}
|
||||
|
||||
std::size_t end = text.size();
|
||||
while (end > start && std::isspace(static_cast<unsigned char>(text[end - 1]))) {
|
||||
--end;
|
||||
}
|
||||
|
||||
return std::string(text.substr(start, end - start));
|
||||
}
|
||||
|
||||
PopupSurfaceConfig makePopupConfig(std::int32_t anchorX, std::int32_t anchorY, std::int32_t anchorWidth,
|
||||
std::int32_t anchorHeight, std::uint32_t width, std::uint32_t height,
|
||||
std::uint32_t serial, std::int32_t offsetX = 0, std::int32_t offsetY = 0,
|
||||
bool grab = true) {
|
||||
PopupSurfaceConfig cfg{
|
||||
.anchorX = anchorX,
|
||||
.anchorY = anchorY,
|
||||
.anchorWidth = std::max(1, anchorWidth),
|
||||
.anchorHeight = std::max(1, anchorHeight),
|
||||
.width = std::max<std::uint32_t>(1, width),
|
||||
.height = std::max<std::uint32_t>(1, height),
|
||||
.anchor = XDG_POSITIONER_ANCHOR_BOTTOM,
|
||||
.gravity = XDG_POSITIONER_GRAVITY_BOTTOM,
|
||||
.constraintAdjustment =
|
||||
XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y |
|
||||
XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_Y,
|
||||
.offsetX = offsetX,
|
||||
.offsetY = offsetY,
|
||||
.serial = serial,
|
||||
.grab = grab,
|
||||
};
|
||||
|
||||
cfg.anchorX += cfg.anchorWidth / 2;
|
||||
cfg.anchorY += cfg.anchorHeight / 2;
|
||||
cfg.anchorWidth = 1;
|
||||
cfg.anchorHeight = 1;
|
||||
return cfg;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
WidgetAddPopup::~WidgetAddPopup() { destroyPopup(); }
|
||||
|
||||
void WidgetAddPopup::initialize(WaylandConnection& wayland, ConfigService& config, RenderContext& renderContext) {
|
||||
initializeBase(wayland, config, renderContext);
|
||||
}
|
||||
|
||||
void WidgetAddPopup::setOnSelect(SelectCallback callback) { m_onSelect = std::move(callback); }
|
||||
|
||||
void WidgetAddPopup::setOnDismissed(std::function<void()> callback) { m_onDismissed = std::move(callback); }
|
||||
|
||||
void WidgetAddPopup::open(xdg_surface* parentXdgSurface, wl_output* output, std::uint32_t serial,
|
||||
Button* anchorButton, const std::vector<std::string>& lanePath, const Config& config,
|
||||
float scale, PopupWindow::AnchorMode anchorMode) {
|
||||
if (m_popup == nullptr || parentXdgSurface == nullptr || anchorButton == nullptr) {
|
||||
Button* anchorButton, wl_surface* parentWlSurface, const std::vector<std::string>& lanePath,
|
||||
const Config& config, float scale) {
|
||||
if (parentXdgSurface == nullptr || parentWlSurface == nullptr || anchorButton == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto pickerEntries = widgetPickerEntries(config);
|
||||
std::vector<SearchPickerOption> options;
|
||||
options.reserve(pickerEntries.size() * 2);
|
||||
std::vector<SearchPickerOption> normalOptions;
|
||||
std::vector<SearchPickerOption> instanceOptions;
|
||||
normalOptions.reserve(pickerEntries.size());
|
||||
instanceOptions.reserve(pickerEntries.size());
|
||||
|
||||
for (const auto& entry : pickerEntries) {
|
||||
options.push_back(SearchPickerOption{.value = entry.value,
|
||||
normalOptions.push_back(SearchPickerOption{.value = entry.value,
|
||||
.label = entry.label,
|
||||
.description = entry.description,
|
||||
.category = entry.category,
|
||||
.enabled = true});
|
||||
|
||||
if (entry.kind != WidgetReferenceKind::BuiltIn) {
|
||||
continue;
|
||||
}
|
||||
@@ -88,148 +224,390 @@ namespace settings {
|
||||
if (spec.type != entry.value || !spec.supportsMultipleInstances) {
|
||||
continue;
|
||||
}
|
||||
options.push_back(SearchPickerOption{
|
||||
.value = std::string(kCreateInstancePrefix) + entry.value,
|
||||
.label = i18n::tr("settings.entities.widget.picker.create-label", "label", entry.label),
|
||||
.description = i18n::tr("settings.entities.widget.picker.create-description", "type", entry.value),
|
||||
.category = i18n::tr("settings.entities.widget.kinds.new-instance"),
|
||||
instanceOptions.push_back(SearchPickerOption{
|
||||
.value = entry.value,
|
||||
.label = entry.label,
|
||||
.description = i18n::tr("settings.entities.widget.picker.instance-description", "type", entry.value),
|
||||
.category = {},
|
||||
.enabled = true,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.empty()) {
|
||||
if (normalOptions.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
sortSearchOptions(normalOptions);
|
||||
sortSearchOptions(instanceOptions);
|
||||
|
||||
if (isOpen()) {
|
||||
close();
|
||||
}
|
||||
|
||||
m_scale = std::max(0.1f, scale);
|
||||
m_config = &config;
|
||||
m_normalOptions = std::move(normalOptions);
|
||||
m_instanceOptions = std::move(instanceOptions);
|
||||
m_lanePath = lanePath;
|
||||
m_root = nullptr;
|
||||
m_headerRow = nullptr;
|
||||
m_body = nullptr;
|
||||
m_createActions = nullptr;
|
||||
m_searchPicker = nullptr;
|
||||
m_createTitle = nullptr;
|
||||
m_instanceInput = nullptr;
|
||||
m_instanceModeEnabled = false;
|
||||
m_createFormVisible = false;
|
||||
m_createType.clear();
|
||||
m_createLabel.clear();
|
||||
|
||||
const float panelPadding = Style::spaceSm * scale;
|
||||
const float panelGap = Style::spaceSm * scale;
|
||||
const float panelWidth = 520.0f * scale;
|
||||
const float panelHeight = 420.0f * scale;
|
||||
m_parentXdgSurface = parentXdgSurface;
|
||||
m_parentWlSurface = parentWlSurface;
|
||||
m_output = output;
|
||||
m_serial = serial;
|
||||
|
||||
Node::absolutePosition(anchorButton, m_anchorAbsX, m_anchorAbsY);
|
||||
m_anchorWidth = std::max(1, static_cast<std::int32_t>(anchorButton->width()));
|
||||
m_anchorHeight = std::max(1, static_cast<std::int32_t>(anchorButton->height()));
|
||||
|
||||
reopenForCurrentMode();
|
||||
}
|
||||
|
||||
void WidgetAddPopup::close() { destroyPopup(); }
|
||||
|
||||
bool WidgetAddPopup::isOpen() const noexcept { return DialogPopupHost::isOpen(); }
|
||||
|
||||
bool WidgetAddPopup::onPointerEvent(const PointerEvent& event) { return DialogPopupHost::onPointerEvent(event); }
|
||||
|
||||
void WidgetAddPopup::onKeyboardEvent(const KeyboardEvent& event) { DialogPopupHost::onKeyboardEvent(event); }
|
||||
|
||||
wl_surface* WidgetAddPopup::wlSurface() const noexcept { return DialogPopupHost::wlSurface(); }
|
||||
|
||||
void WidgetAddPopup::requestLayout() { DialogPopupHost::requestLayout(); }
|
||||
|
||||
void WidgetAddPopup::requestRedraw() { DialogPopupHost::requestRedraw(); }
|
||||
|
||||
void WidgetAddPopup::refreshPickerOptions() {
|
||||
if (m_searchPicker == nullptr) {
|
||||
return;
|
||||
}
|
||||
m_searchPicker->setOptions(m_instanceModeEnabled ? m_instanceOptions : m_normalOptions);
|
||||
}
|
||||
|
||||
void WidgetAddPopup::refreshBodyState() {
|
||||
if (m_searchPicker != nullptr) {
|
||||
m_searchPicker->setVisible(!m_createFormVisible);
|
||||
m_searchPicker->setParticipatesInLayout(!m_createFormVisible);
|
||||
if (!m_createFormVisible) {
|
||||
refreshPickerOptions();
|
||||
}
|
||||
}
|
||||
if (m_createTitle != nullptr) {
|
||||
m_createTitle->setVisible(m_createFormVisible);
|
||||
m_createTitle->setParticipatesInLayout(m_createFormVisible);
|
||||
}
|
||||
if (m_instanceInput != nullptr) {
|
||||
m_instanceInput->setVisible(m_createFormVisible);
|
||||
m_instanceInput->setParticipatesInLayout(m_createFormVisible);
|
||||
}
|
||||
if (m_createActions != nullptr) {
|
||||
m_createActions->setVisible(m_createFormVisible);
|
||||
m_createActions->setParticipatesInLayout(m_createFormVisible);
|
||||
}
|
||||
|
||||
if (!m_createFormVisible && m_searchPicker != nullptr) {
|
||||
if (auto* filter = m_searchPicker->filterInputArea(); filter != nullptr) {
|
||||
inputDispatcher().setFocus(filter);
|
||||
}
|
||||
}
|
||||
if (m_createFormVisible && m_instanceInput != nullptr && m_instanceInput->inputArea() != nullptr) {
|
||||
inputDispatcher().setFocus(m_instanceInput->inputArea());
|
||||
}
|
||||
}
|
||||
|
||||
std::string WidgetAddPopup::suggestedInstanceId(std::string_view type) const {
|
||||
if (m_config == nullptr) {
|
||||
return std::string(type);
|
||||
}
|
||||
return nextWidgetInstanceId(*m_config, type);
|
||||
}
|
||||
|
||||
bool WidgetAddPopup::canCreateInstanceId(std::string_view id) const {
|
||||
if (m_config == nullptr) {
|
||||
return false;
|
||||
}
|
||||
return isValidWidgetInstanceId(id) && !widgetReferenceNameExists(*m_config, id);
|
||||
}
|
||||
|
||||
void WidgetAddPopup::beginCreateFlow(const SearchPickerOption& option) {
|
||||
m_createType = option.value;
|
||||
m_createLabel = option.label;
|
||||
m_createFormVisible = true;
|
||||
if (m_createTitle != nullptr) {
|
||||
m_createTitle->setText(i18n::tr("settings.entities.widget.instance.create-title", "type", m_createType));
|
||||
}
|
||||
if (m_instanceInput != nullptr) {
|
||||
m_instanceInput->setValue(suggestedInstanceId(m_createType));
|
||||
m_instanceInput->setInvalid(false);
|
||||
}
|
||||
reopenForCurrentMode();
|
||||
}
|
||||
|
||||
void WidgetAddPopup::finishCreateFlow() {
|
||||
if (m_instanceInput == nullptr) {
|
||||
return;
|
||||
}
|
||||
const std::string id = trimmedText(m_instanceInput->value());
|
||||
if (!canCreateInstanceId(id)) {
|
||||
m_instanceInput->setInvalid(true);
|
||||
return;
|
||||
}
|
||||
m_instanceInput->setInvalid(false);
|
||||
if (m_onSelect) {
|
||||
m_onSelect(m_lanePath, id, m_createType, id);
|
||||
}
|
||||
DeferredCall::callLater([this]() { close(); });
|
||||
}
|
||||
|
||||
void WidgetAddPopup::populateContent(Node* contentParent, std::uint32_t /*width*/, std::uint32_t /*height*/) {
|
||||
const float panelPadding = Style::spaceSm * m_scale;
|
||||
const float panelGap = Style::spaceSm * m_scale;
|
||||
|
||||
m_popup->setContentBuilder([this, options, panelPadding, panelGap, scale](float width, float height) {
|
||||
auto root = std::make_unique<Flex>();
|
||||
root->setDirection(FlexDirection::Vertical);
|
||||
root->setAlign(FlexAlign::Stretch);
|
||||
root->setGap(panelGap);
|
||||
root->setPadding(panelPadding);
|
||||
root->setFill(colorSpecFromRole(ColorRole::Surface));
|
||||
root->setBorder(colorSpecFromRole(ColorRole::Outline, 0.35f), Style::borderWidth);
|
||||
root->setRadius(Style::radiusLg);
|
||||
root->setSize(width, height);
|
||||
m_root = root.get();
|
||||
|
||||
auto header = std::make_unique<Flex>();
|
||||
header->setDirection(FlexDirection::Horizontal);
|
||||
header->setAlign(FlexAlign::Center);
|
||||
header->setGap(Style::spaceSm * scale);
|
||||
header->addChild(makeLabel(i18n::tr("settings.entities.widget.inspector.add-title", "lane",
|
||||
laneLabel(m_lanePath.empty() ? "" : m_lanePath.back())),
|
||||
Style::fontSizeBody * scale, colorSpecFromRole(ColorRole::OnSurface), true));
|
||||
header->setGap(Style::spaceSm * m_scale);
|
||||
m_headerRow = header.get();
|
||||
|
||||
const std::string lane = laneLabel(m_lanePath.empty() ? "" : m_lanePath.back());
|
||||
const std::string title =
|
||||
m_createFormVisible
|
||||
? i18n::tr("settings.entities.widget.inspector.add-instance-title", "widget", m_createLabel, "lane", lane)
|
||||
: i18n::tr("settings.entities.widget.inspector.add-title", "lane", lane);
|
||||
header->addChild(makeLabel(title, Style::fontSizeBody * m_scale, colorSpecFromRole(ColorRole::OnSurface), true));
|
||||
|
||||
auto spacer = std::make_unique<Flex>();
|
||||
spacer->setFlexGrow(1.0f);
|
||||
header->addChild(std::move(spacer));
|
||||
|
||||
if (!m_createFormVisible) {
|
||||
header->addChild(makeLabel(i18n::tr("settings.entities.widget.picker.instance-toggle"),
|
||||
Style::fontSizeCaption * m_scale, colorSpecFromRole(ColorRole::OnSurfaceVariant),
|
||||
false));
|
||||
|
||||
auto instanceToggle = std::make_unique<Toggle>();
|
||||
instanceToggle->setScale(m_scale);
|
||||
instanceToggle->setChecked(m_instanceModeEnabled);
|
||||
instanceToggle->setOnChange([this](bool value) {
|
||||
m_instanceModeEnabled = value;
|
||||
m_createFormVisible = false;
|
||||
m_createType.clear();
|
||||
m_createLabel.clear();
|
||||
if (m_instanceInput != nullptr) {
|
||||
m_instanceInput->setInvalid(false);
|
||||
}
|
||||
refreshBodyState();
|
||||
requestLayout();
|
||||
});
|
||||
header->addChild(std::move(instanceToggle));
|
||||
}
|
||||
|
||||
auto closeBtn = std::make_unique<Button>();
|
||||
closeBtn->setGlyph("close");
|
||||
closeBtn->setVariant(ButtonVariant::Ghost);
|
||||
closeBtn->setGlyphSize(Style::fontSizeBody * scale);
|
||||
closeBtn->setMinWidth(Style::controlHeightSm * scale);
|
||||
closeBtn->setMinHeight(Style::controlHeightSm * scale);
|
||||
closeBtn->setPadding(Style::spaceXs * scale);
|
||||
closeBtn->setRadius(Style::radiusSm * scale);
|
||||
closeBtn->setOnClick([this]() {
|
||||
if (m_popup != nullptr) {
|
||||
m_popup->close();
|
||||
}
|
||||
});
|
||||
closeBtn->setVariant(ButtonVariant::Default);
|
||||
closeBtn->setGlyphSize(Style::fontSizeBody * m_scale);
|
||||
closeBtn->setMinWidth(Style::controlHeightSm * m_scale);
|
||||
closeBtn->setMinHeight(Style::controlHeightSm * m_scale);
|
||||
closeBtn->setPadding(Style::spaceXs * m_scale);
|
||||
closeBtn->setRadius(Style::radiusMd * m_scale);
|
||||
closeBtn->setOnClick([this]() { DeferredCall::callLater([this]() { close(); }); });
|
||||
header->addChild(std::move(closeBtn));
|
||||
root->addChild(std::move(header));
|
||||
|
||||
auto body = std::make_unique<Flex>();
|
||||
body->setDirection(FlexDirection::Vertical);
|
||||
body->setAlign(FlexAlign::Stretch);
|
||||
body->setGap(Style::spaceSm * m_scale);
|
||||
body->setFlexGrow(1.0f);
|
||||
m_body = body.get();
|
||||
|
||||
auto picker = std::make_unique<SearchPicker>();
|
||||
picker->setPlaceholder(i18n::tr("settings.entities.widget.picker.placeholder"));
|
||||
picker->setEmptyText(i18n::tr("settings.entities.widget.picker.empty"));
|
||||
picker->setOptions(options);
|
||||
picker->setSize(std::max(1.0f, width - panelPadding * 2.0f),
|
||||
std::max(1.0f, height - panelPadding * 2.0f - Style::controlHeightSm * scale));
|
||||
picker->setOptions(m_normalOptions);
|
||||
picker->setOnActivated([this](const SearchPickerOption& option) {
|
||||
if (option.value.empty()) {
|
||||
return;
|
||||
}
|
||||
if (m_onSelect) {
|
||||
m_onSelect(m_lanePath, option.value);
|
||||
}
|
||||
if (m_popup != nullptr) {
|
||||
m_popup->close();
|
||||
}
|
||||
});
|
||||
picker->setOnCancel([this]() {
|
||||
if (m_popup != nullptr) {
|
||||
m_popup->close();
|
||||
}
|
||||
});
|
||||
m_searchPicker = picker.get();
|
||||
root->addChild(std::move(picker));
|
||||
return root;
|
||||
});
|
||||
|
||||
m_popup->setSceneReadyCallback([this](InputDispatcher& dispatcher) {
|
||||
if (!m_focusSearchOnOpen || m_searchPicker == nullptr) {
|
||||
if (m_instanceModeEnabled) {
|
||||
beginCreateFlow(option);
|
||||
return;
|
||||
}
|
||||
auto* area = m_searchPicker->filterInputArea();
|
||||
if (area != nullptr) {
|
||||
dispatcher.setFocus(area);
|
||||
m_focusSearchOnOpen = false;
|
||||
if (m_onSelect) {
|
||||
m_onSelect(m_lanePath, option.value, {}, {});
|
||||
}
|
||||
DeferredCall::callLater([this]() { close(); });
|
||||
});
|
||||
picker->setOnCancel([this]() { DeferredCall::callLater([this]() { close(); }); });
|
||||
m_searchPicker = picker.get();
|
||||
body->addChild(std::move(picker));
|
||||
|
||||
auto createTitle = makeLabel("", Style::fontSizeCaption * m_scale, colorSpecFromRole(ColorRole::OnSurfaceVariant));
|
||||
createTitle->setVisible(false);
|
||||
createTitle->setParticipatesInLayout(false);
|
||||
m_createTitle = createTitle.get();
|
||||
body->addChild(std::move(createTitle));
|
||||
|
||||
auto instanceInput = std::make_unique<Input>();
|
||||
instanceInput->setPlaceholder(i18n::tr("settings.entities.widget.instance.id-placeholder"));
|
||||
instanceInput->setFontSize(Style::fontSizeBody * m_scale);
|
||||
instanceInput->setControlHeight(Style::controlHeight * m_scale);
|
||||
instanceInput->setHorizontalPadding(Style::spaceSm * m_scale);
|
||||
instanceInput->setSize(260.0f * m_scale, Style::controlHeight * m_scale);
|
||||
instanceInput->setVisible(false);
|
||||
instanceInput->setParticipatesInLayout(false);
|
||||
instanceInput->setOnChange([this](const std::string& /*value*/) {
|
||||
if (m_instanceInput != nullptr) {
|
||||
m_instanceInput->setInvalid(false);
|
||||
}
|
||||
});
|
||||
instanceInput->setOnSubmit([this](const std::string& /*value*/) { finishCreateFlow(); });
|
||||
m_instanceInput = instanceInput.get();
|
||||
body->addChild(std::move(instanceInput));
|
||||
|
||||
float anchorAbsX = 0.0f;
|
||||
float anchorAbsY = 0.0f;
|
||||
Node::absolutePosition(anchorButton, anchorAbsX, anchorAbsY);
|
||||
auto actionRow = std::make_unique<Flex>();
|
||||
actionRow->setDirection(FlexDirection::Horizontal);
|
||||
actionRow->setAlign(FlexAlign::Center);
|
||||
actionRow->setGap(Style::spaceSm * m_scale);
|
||||
actionRow->setVisible(false);
|
||||
actionRow->setParticipatesInLayout(false);
|
||||
m_createActions = actionRow.get();
|
||||
|
||||
auto cfg = PopupWindow::makeConfig(static_cast<std::int32_t>(anchorAbsX), static_cast<std::int32_t>(anchorAbsY),
|
||||
std::max(1, static_cast<std::int32_t>(anchorButton->width())),
|
||||
std::max(1, static_cast<std::int32_t>(anchorButton->height())),
|
||||
static_cast<std::uint32_t>(std::max(1.0f, panelWidth)),
|
||||
static_cast<std::uint32_t>(std::max(1.0f, panelHeight)), serial, anchorMode, 0,
|
||||
static_cast<std::int32_t>(Style::spaceXs * scale), true);
|
||||
auto backBtn = std::make_unique<Button>();
|
||||
backBtn->setText(i18n::tr("common.actions.cancel"));
|
||||
backBtn->setVariant(ButtonVariant::Ghost);
|
||||
backBtn->setFontSize(Style::fontSizeCaption * m_scale);
|
||||
backBtn->setMinHeight(Style::controlHeightSm * m_scale);
|
||||
backBtn->setPadding(Style::spaceXs * m_scale, Style::spaceSm * m_scale);
|
||||
backBtn->setRadius(Style::radiusSm * m_scale);
|
||||
backBtn->setOnClick([this]() {
|
||||
m_createFormVisible = false;
|
||||
m_createType.clear();
|
||||
m_createLabel.clear();
|
||||
if (m_instanceInput != nullptr) {
|
||||
m_instanceInput->setInvalid(false);
|
||||
}
|
||||
reopenForCurrentMode();
|
||||
});
|
||||
actionRow->addChild(std::move(backBtn));
|
||||
|
||||
m_focusSearchOnOpen = true;
|
||||
m_popup->openAsChild(cfg, parentXdgSurface, output);
|
||||
auto createBtn = std::make_unique<Button>();
|
||||
createBtn->setText(i18n::tr("settings.entities.widget.instance.create-save"));
|
||||
createBtn->setVariant(ButtonVariant::Default);
|
||||
createBtn->setFontSize(Style::fontSizeCaption * m_scale);
|
||||
createBtn->setMinHeight(Style::controlHeightSm * m_scale);
|
||||
createBtn->setPadding(Style::spaceXs * m_scale, Style::spaceSm * m_scale);
|
||||
createBtn->setRadius(Style::radiusSm * m_scale);
|
||||
createBtn->setOnClick([this]() { finishCreateFlow(); });
|
||||
actionRow->addChild(std::move(createBtn));
|
||||
body->addChild(std::move(actionRow));
|
||||
|
||||
root->addChild(std::move(body));
|
||||
contentParent->addChild(std::move(root));
|
||||
|
||||
refreshBodyState();
|
||||
}
|
||||
|
||||
void WidgetAddPopup::close() {
|
||||
if (m_popup != nullptr) {
|
||||
m_popup->close();
|
||||
void WidgetAddPopup::layoutSheet(float contentWidth, float contentHeight) {
|
||||
if (m_root == nullptr || renderContext() == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float panelPadding = Style::spaceSm * m_scale;
|
||||
m_root->setSize(contentWidth, contentHeight);
|
||||
|
||||
if (m_searchPicker != nullptr) {
|
||||
const float pickerWidth = std::max(1.0f, contentWidth - panelPadding * 2.0f);
|
||||
const float pickerHeight = std::max(1.0f, contentHeight - panelPadding * 2.0f - Style::controlHeightSm * m_scale);
|
||||
m_searchPicker->setSize(pickerWidth, pickerHeight);
|
||||
}
|
||||
|
||||
m_root->layout(*renderContext());
|
||||
}
|
||||
|
||||
std::pair<float, float> WidgetAddPopup::popupSize() const {
|
||||
if (m_createFormVisible) {
|
||||
return {360.0f * m_scale, 165.0f * m_scale};
|
||||
}
|
||||
return {520.0f * m_scale, 420.0f * m_scale};
|
||||
}
|
||||
|
||||
void WidgetAddPopup::reopenForCurrentMode() {
|
||||
if (m_parentXdgSurface == nullptr || m_parentWlSurface == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto [panelWidth, panelHeight] = popupSize();
|
||||
const auto cfg = makePopupConfig(
|
||||
static_cast<std::int32_t>(m_anchorAbsX), static_cast<std::int32_t>(m_anchorAbsY), m_anchorWidth, m_anchorHeight,
|
||||
static_cast<std::uint32_t>(std::max(1.0f, panelWidth)), static_cast<std::uint32_t>(std::max(1.0f, panelHeight)),
|
||||
m_serial, 0, static_cast<std::int32_t>(Style::spaceXs * m_scale), true);
|
||||
|
||||
m_internalReopen = true;
|
||||
const bool opened = openPopupAsChild(cfg, m_parentXdgSurface, m_parentWlSurface, m_output);
|
||||
m_internalReopen = false;
|
||||
if (!opened) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
bool WidgetAddPopup::isOpen() const noexcept { return m_popup != nullptr && m_popup->isOpen(); }
|
||||
void WidgetAddPopup::cancelToFacade() {}
|
||||
|
||||
bool WidgetAddPopup::onPointerEvent(const PointerEvent& event) {
|
||||
return m_popup != nullptr && m_popup->onPointerEvent(event);
|
||||
InputArea* WidgetAddPopup::initialFocusArea() {
|
||||
if (m_createFormVisible && m_instanceInput != nullptr) {
|
||||
return m_instanceInput->inputArea();
|
||||
}
|
||||
return m_searchPicker != nullptr ? m_searchPicker->filterInputArea() : nullptr;
|
||||
}
|
||||
|
||||
void WidgetAddPopup::onKeyboardEvent(const KeyboardEvent& event) {
|
||||
if (m_popup != nullptr) {
|
||||
m_popup->onKeyboardEvent(event);
|
||||
void WidgetAddPopup::onSheetClose() {
|
||||
if (m_internalReopen) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void WidgetAddPopup::requestLayout() {
|
||||
if (m_popup != nullptr) {
|
||||
m_popup->requestLayout();
|
||||
}
|
||||
}
|
||||
|
||||
void WidgetAddPopup::requestRedraw() {
|
||||
if (m_popup != nullptr) {
|
||||
m_popup->requestRedraw();
|
||||
m_normalOptions.clear();
|
||||
m_instanceOptions.clear();
|
||||
m_config = nullptr;
|
||||
m_parentXdgSurface = nullptr;
|
||||
m_parentWlSurface = nullptr;
|
||||
m_output = nullptr;
|
||||
m_serial = 0;
|
||||
m_anchorAbsX = 0.0f;
|
||||
m_anchorAbsY = 0.0f;
|
||||
m_anchorWidth = 1;
|
||||
m_anchorHeight = 1;
|
||||
m_lanePath.clear();
|
||||
m_root = nullptr;
|
||||
m_headerRow = nullptr;
|
||||
m_body = nullptr;
|
||||
m_createActions = nullptr;
|
||||
m_searchPicker = nullptr;
|
||||
m_createTitle = nullptr;
|
||||
m_instanceInput = nullptr;
|
||||
m_instanceModeEnabled = false;
|
||||
m_createFormVisible = false;
|
||||
m_createType.clear();
|
||||
m_createLabel.clear();
|
||||
if (m_onDismissed) {
|
||||
m_onDismissed();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui/controls/popup_window.h"
|
||||
#include "ui/controls/search_picker.h"
|
||||
#include "ui/dialogs/dialog_popup_host.h"
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
@@ -9,45 +9,89 @@
|
||||
#include <vector>
|
||||
|
||||
class Button;
|
||||
class InputDispatcher;
|
||||
class Node;
|
||||
class ConfigService;
|
||||
class RenderContext;
|
||||
class WaylandConnection;
|
||||
class Flex;
|
||||
class Input;
|
||||
class Label;
|
||||
struct Config;
|
||||
struct KeyboardEvent;
|
||||
struct PointerEvent;
|
||||
struct wl_surface;
|
||||
struct wl_output;
|
||||
struct xdg_surface;
|
||||
|
||||
namespace settings {
|
||||
|
||||
class WidgetAddPopup {
|
||||
class WidgetAddPopup final : public DialogPopupHost {
|
||||
public:
|
||||
using SelectCallback = std::function<void(const std::vector<std::string>& lanePath, const std::string& value)>;
|
||||
using SelectCallback = std::function<void(const std::vector<std::string>& lanePath, const std::string& value,
|
||||
const std::string& newInstanceType, const std::string& newInstanceId)>;
|
||||
|
||||
WidgetAddPopup(WaylandConnection& wayland, RenderContext& renderContext);
|
||||
WidgetAddPopup() = default;
|
||||
~WidgetAddPopup();
|
||||
|
||||
void initialize(WaylandConnection& wayland, ConfigService& config, RenderContext& renderContext);
|
||||
|
||||
void setOnSelect(SelectCallback callback);
|
||||
void setOnDismissed(std::function<void()> callback);
|
||||
|
||||
void open(xdg_surface* parentXdgSurface, wl_output* output, std::uint32_t serial, Button* anchorButton,
|
||||
const std::vector<std::string>& lanePath, const Config& config, float scale,
|
||||
PopupWindow::AnchorMode anchorMode = PopupWindow::AnchorMode::CenterOnAnchor);
|
||||
wl_surface* parentWlSurface, const std::vector<std::string>& lanePath, const Config& config, float scale);
|
||||
void close();
|
||||
|
||||
[[nodiscard]] bool isOpen() const noexcept;
|
||||
[[nodiscard]] bool onPointerEvent(const PointerEvent& event);
|
||||
void onKeyboardEvent(const KeyboardEvent& event);
|
||||
[[nodiscard]] wl_surface* wlSurface() const noexcept;
|
||||
void requestLayout();
|
||||
void requestRedraw();
|
||||
|
||||
protected:
|
||||
void populateContent(Node* contentParent, std::uint32_t width, std::uint32_t height) override;
|
||||
void layoutSheet(float contentWidth, float contentHeight) override;
|
||||
void cancelToFacade() override;
|
||||
[[nodiscard]] InputArea* initialFocusArea() override;
|
||||
void onSheetClose() override;
|
||||
|
||||
private:
|
||||
WaylandConnection& m_wayland;
|
||||
RenderContext& m_renderContext;
|
||||
std::unique_ptr<PopupWindow> m_popup;
|
||||
std::vector<SearchPickerOption> m_normalOptions;
|
||||
std::vector<SearchPickerOption> m_instanceOptions;
|
||||
float m_scale = 1.0f;
|
||||
const Config* m_config = nullptr;
|
||||
std::vector<std::string> m_lanePath;
|
||||
Flex* m_root = nullptr;
|
||||
Flex* m_headerRow = nullptr;
|
||||
Flex* m_body = nullptr;
|
||||
Flex* m_createActions = nullptr;
|
||||
SearchPicker* m_searchPicker = nullptr;
|
||||
bool m_focusSearchOnOpen = false;
|
||||
Label* m_createTitle = nullptr;
|
||||
Input* m_instanceInput = nullptr;
|
||||
bool m_instanceModeEnabled = false;
|
||||
bool m_createFormVisible = false;
|
||||
std::string m_createType;
|
||||
std::string m_createLabel;
|
||||
|
||||
void refreshPickerOptions();
|
||||
void refreshBodyState();
|
||||
void beginCreateFlow(const SearchPickerOption& option);
|
||||
void finishCreateFlow();
|
||||
void reopenForCurrentMode();
|
||||
[[nodiscard]] std::pair<float, float> popupSize() const;
|
||||
[[nodiscard]] std::string suggestedInstanceId(std::string_view type) const;
|
||||
[[nodiscard]] bool canCreateInstanceId(std::string_view id) const;
|
||||
|
||||
xdg_surface* m_parentXdgSurface = nullptr;
|
||||
wl_surface* m_parentWlSurface = nullptr;
|
||||
wl_output* m_output = nullptr;
|
||||
std::uint32_t m_serial = 0;
|
||||
float m_anchorAbsX = 0.0f;
|
||||
float m_anchorAbsY = 0.0f;
|
||||
std::int32_t m_anchorWidth = 1;
|
||||
std::int32_t m_anchorHeight = 1;
|
||||
bool m_internalReopen = false;
|
||||
SelectCallback m_onSelect;
|
||||
std::function<void()> m_onDismissed;
|
||||
};
|
||||
|
||||
@@ -1,280 +0,0 @@
|
||||
#include "ui/controls/popup_window.h"
|
||||
|
||||
#include "core/deferred_call.h"
|
||||
#include "core/log.h"
|
||||
#include "core/ui_phase.h"
|
||||
#include "render/render_context.h"
|
||||
#include "render/scene/node.h"
|
||||
#include "wayland/wayland_connection.h"
|
||||
#include "wayland/wayland_seat.h"
|
||||
#include "xdg-shell-client-protocol.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr Logger kLog("popup-window");
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<PopupWindow*>& PopupWindow::openPopups() {
|
||||
static std::vector<PopupWindow*> popups;
|
||||
return popups;
|
||||
}
|
||||
|
||||
PopupWindow::PopupWindow(WaylandConnection& wayland, RenderContext& renderContext)
|
||||
: m_wayland(wayland), m_renderContext(renderContext) {}
|
||||
|
||||
PopupWindow::~PopupWindow() { close(); }
|
||||
|
||||
void PopupWindow::setContentBuilder(ContentBuilder builder) { m_contentBuilder = std::move(builder); }
|
||||
|
||||
void PopupWindow::setSceneReadyCallback(SceneReadyCallback callback) { m_sceneReadyCallback = std::move(callback); }
|
||||
|
||||
void PopupWindow::setOnDismissed(std::function<void()> callback) { m_onDismissed = std::move(callback); }
|
||||
|
||||
void PopupWindow::open(PopupSurfaceConfig config, zwlr_layer_surface_v1* parentLayerSurface, wl_output* output) {
|
||||
openCommon(config, parentLayerSurface, nullptr, output);
|
||||
}
|
||||
|
||||
void PopupWindow::openAsChild(PopupSurfaceConfig config, xdg_surface* parentXdgSurface, wl_output* output) {
|
||||
openCommon(config, nullptr, parentXdgSurface, output);
|
||||
}
|
||||
|
||||
void PopupWindow::openCommon(PopupSurfaceConfig config, zwlr_layer_surface_v1* parentLayerSurface,
|
||||
xdg_surface* parentXdgSurface, wl_output* output) {
|
||||
close();
|
||||
|
||||
m_surface = std::make_unique<PopupSurface>(m_wayland);
|
||||
m_surface->setRenderContext(&m_renderContext);
|
||||
|
||||
auto* self = this;
|
||||
m_surface->setConfigureCallback(
|
||||
[self](std::uint32_t /*w*/, std::uint32_t /*h*/) { self->m_surface->requestLayout(); });
|
||||
|
||||
m_surface->setPrepareFrameCallback([self](bool /*needsUpdate*/, bool needsLayout) {
|
||||
if (self->m_surface == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto width = self->m_surface->width();
|
||||
const auto height = self->m_surface->height();
|
||||
if (width == 0 || height == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
self->m_renderContext.makeCurrent(self->m_surface->renderTarget());
|
||||
|
||||
const bool needsSceneBuild = self->m_sceneRoot == nullptr ||
|
||||
static_cast<std::uint32_t>(std::round(self->m_sceneRoot->width())) != width ||
|
||||
static_cast<std::uint32_t>(std::round(self->m_sceneRoot->height())) != height;
|
||||
if (needsSceneBuild) {
|
||||
UiPhaseScope layoutPhase(UiPhase::Layout);
|
||||
self->buildScene(width, height);
|
||||
return;
|
||||
}
|
||||
|
||||
if (needsLayout && self->m_sceneRoot != nullptr) {
|
||||
UiPhaseScope layoutPhase(UiPhase::Layout);
|
||||
self->m_sceneRoot->setSize(static_cast<float>(width), static_cast<float>(height));
|
||||
self->m_sceneRoot->layout(self->m_renderContext);
|
||||
self->m_surface->setSceneRoot(self->m_sceneRoot.get());
|
||||
}
|
||||
});
|
||||
|
||||
m_surface->setDismissedCallback([self]() { DeferredCall::callLater([self]() { self->close(); }); });
|
||||
|
||||
const bool initialized = parentXdgSurface != nullptr ? m_surface->initializeAsChild(parentXdgSurface, output, config)
|
||||
: m_surface->initialize(parentLayerSurface, output, config);
|
||||
if (!initialized) {
|
||||
kLog.warn("failed to create popup window");
|
||||
m_surface.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
m_wlSurface = m_surface->wlSurface();
|
||||
openPopups().push_back(this);
|
||||
}
|
||||
|
||||
void PopupWindow::buildScene(std::uint32_t width, std::uint32_t height) {
|
||||
const float fw = static_cast<float>(width);
|
||||
const float fh = static_cast<float>(height);
|
||||
|
||||
if (m_contentBuilder) {
|
||||
m_sceneRoot = m_contentBuilder(fw, fh);
|
||||
}
|
||||
|
||||
if (m_sceneRoot == nullptr) {
|
||||
m_sceneRoot = std::make_unique<Node>();
|
||||
}
|
||||
m_sceneRoot->setSize(fw, fh);
|
||||
m_sceneRoot->layout(m_renderContext);
|
||||
|
||||
m_inputDispatcher.setSceneRoot(m_sceneRoot.get());
|
||||
m_inputDispatcher.setCursorShapeCallback(
|
||||
[this](std::uint32_t serial, std::uint32_t shape) { m_wayland.setCursorShape(serial, shape); });
|
||||
m_surface->setSceneRoot(m_sceneRoot.get());
|
||||
|
||||
if (m_sceneReadyCallback) {
|
||||
m_sceneReadyCallback(m_inputDispatcher);
|
||||
}
|
||||
}
|
||||
|
||||
void PopupWindow::close() {
|
||||
const bool wasOpen = m_surface != nullptr;
|
||||
auto& popups = openPopups();
|
||||
popups.erase(std::remove(popups.begin(), popups.end(), this), popups.end());
|
||||
m_sceneRoot.reset();
|
||||
m_surface.reset();
|
||||
m_inputDispatcher.setSceneRoot(nullptr);
|
||||
m_wlSurface = nullptr;
|
||||
m_pointerInside = false;
|
||||
if (wasOpen && m_onDismissed) {
|
||||
m_onDismissed();
|
||||
}
|
||||
}
|
||||
|
||||
bool PopupWindow::isOpen() const noexcept { return m_surface != nullptr; }
|
||||
|
||||
wl_surface* PopupWindow::wlSurface() const noexcept { return m_wlSurface; }
|
||||
|
||||
bool PopupWindow::onPointerEvent(const PointerEvent& event) {
|
||||
if (!isOpen()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool onPopup = (event.surface != nullptr && event.surface == m_wlSurface);
|
||||
|
||||
switch (event.type) {
|
||||
case PointerEvent::Type::Enter:
|
||||
if (onPopup) {
|
||||
m_pointerInside = true;
|
||||
m_inputDispatcher.pointerEnter(static_cast<float>(event.sx), static_cast<float>(event.sy), event.serial);
|
||||
}
|
||||
break;
|
||||
case PointerEvent::Type::Leave:
|
||||
if (onPopup) {
|
||||
m_pointerInside = false;
|
||||
m_inputDispatcher.pointerLeave();
|
||||
}
|
||||
break;
|
||||
case PointerEvent::Type::Motion:
|
||||
if (onPopup || m_pointerInside) {
|
||||
if (onPopup) {
|
||||
m_pointerInside = true;
|
||||
}
|
||||
m_inputDispatcher.pointerMotion(static_cast<float>(event.sx), static_cast<float>(event.sy), 0);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case PointerEvent::Type::Button:
|
||||
if (onPopup || m_pointerInside) {
|
||||
if (onPopup) {
|
||||
m_pointerInside = true;
|
||||
}
|
||||
const bool pressed = (event.state == 1);
|
||||
(void)m_inputDispatcher.pointerButton(static_cast<float>(event.sx), static_cast<float>(event.sy), event.button,
|
||||
pressed);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case PointerEvent::Type::Axis:
|
||||
if (onPopup || m_pointerInside) {
|
||||
return m_inputDispatcher.pointerAxis(static_cast<float>(event.sx), static_cast<float>(event.sy), event.axis,
|
||||
event.axisSource, event.axisValue, event.axisDiscrete, event.axisValue120,
|
||||
event.axisLines);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (m_surface != nullptr && m_sceneRoot != nullptr && m_surface->isRunning()) {
|
||||
if (m_sceneRoot->layoutDirty()) {
|
||||
m_surface->requestLayout();
|
||||
} else if (m_sceneRoot->paintDirty()) {
|
||||
m_surface->requestRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
return onPopup;
|
||||
}
|
||||
|
||||
void PopupWindow::onKeyboardEvent(const KeyboardEvent& event) {
|
||||
if (!isOpen()) {
|
||||
return;
|
||||
}
|
||||
m_inputDispatcher.keyEvent(event.sym, event.utf32, event.modifiers, event.pressed, event.preedit);
|
||||
if (m_surface != nullptr && m_sceneRoot != nullptr && m_surface->isRunning()) {
|
||||
if (m_sceneRoot->layoutDirty()) {
|
||||
m_surface->requestLayout();
|
||||
} else if (m_sceneRoot->paintDirty()) {
|
||||
m_surface->requestRedraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PopupWindow::requestLayout() {
|
||||
if (m_surface != nullptr) {
|
||||
m_surface->requestLayout();
|
||||
}
|
||||
}
|
||||
|
||||
void PopupWindow::requestRedraw() {
|
||||
if (m_surface != nullptr) {
|
||||
m_surface->requestRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
bool PopupWindow::dispatchKeyboardEvent(wl_surface* keyboardSurface, const KeyboardEvent& event) {
|
||||
if (keyboardSurface == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& popups = openPopups();
|
||||
for (auto it = popups.rbegin(); it != popups.rend(); ++it) {
|
||||
auto* popup = *it;
|
||||
if (popup == nullptr || !popup->isOpen()) {
|
||||
continue;
|
||||
}
|
||||
if (popup->wlSurface() == keyboardSurface) {
|
||||
popup->onKeyboardEvent(event);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
PopupSurfaceConfig PopupWindow::makeConfig(std::int32_t anchorX, std::int32_t anchorY, std::int32_t anchorWidth,
|
||||
std::int32_t anchorHeight, std::uint32_t width, std::uint32_t height,
|
||||
std::uint32_t serial, AnchorMode mode, std::int32_t offsetX,
|
||||
std::int32_t offsetY, bool grab) {
|
||||
PopupSurfaceConfig cfg{
|
||||
.anchorX = anchorX,
|
||||
.anchorY = anchorY,
|
||||
.anchorWidth = std::max(1, anchorWidth),
|
||||
.anchorHeight = std::max(1, anchorHeight),
|
||||
.width = std::max<std::uint32_t>(1, width),
|
||||
.height = std::max<std::uint32_t>(1, height),
|
||||
.anchor = XDG_POSITIONER_ANCHOR_BOTTOM,
|
||||
.gravity = XDG_POSITIONER_GRAVITY_BOTTOM,
|
||||
.constraintAdjustment = XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X |
|
||||
XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y |
|
||||
XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_Y,
|
||||
.offsetX = offsetX,
|
||||
.offsetY = offsetY,
|
||||
.serial = serial,
|
||||
.grab = grab,
|
||||
};
|
||||
|
||||
if (mode == AnchorMode::CenterOnAnchor) {
|
||||
cfg.anchor = XDG_POSITIONER_ANCHOR_BOTTOM;
|
||||
cfg.gravity = XDG_POSITIONER_GRAVITY_BOTTOM;
|
||||
cfg.anchorX += cfg.anchorWidth / 2;
|
||||
cfg.anchorY += cfg.anchorHeight / 2;
|
||||
cfg.anchorWidth = 1;
|
||||
cfg.anchorHeight = 1;
|
||||
}
|
||||
|
||||
return cfg;
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "render/scene/input_dispatcher.h"
|
||||
#include "wayland/popup_surface.h"
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
class Node;
|
||||
class PopupSurface;
|
||||
class RenderContext;
|
||||
class WaylandConnection;
|
||||
struct KeyboardEvent;
|
||||
struct PointerEvent;
|
||||
struct wl_output;
|
||||
struct wl_surface;
|
||||
struct xdg_surface;
|
||||
struct zwlr_layer_surface_v1;
|
||||
|
||||
class PopupWindow {
|
||||
public:
|
||||
enum class AnchorMode : std::uint8_t {
|
||||
BelowAnchor,
|
||||
CenterOnAnchor,
|
||||
};
|
||||
|
||||
using ContentBuilder = std::function<std::unique_ptr<Node>(float width, float height)>;
|
||||
using SceneReadyCallback = std::function<void(InputDispatcher&)>;
|
||||
|
||||
PopupWindow(WaylandConnection& wayland, RenderContext& renderContext);
|
||||
~PopupWindow();
|
||||
|
||||
void setContentBuilder(ContentBuilder builder);
|
||||
void setSceneReadyCallback(SceneReadyCallback callback);
|
||||
void setOnDismissed(std::function<void()> callback);
|
||||
|
||||
void open(PopupSurfaceConfig config, zwlr_layer_surface_v1* parentLayerSurface, wl_output* output);
|
||||
void openAsChild(PopupSurfaceConfig config, xdg_surface* parentXdgSurface, wl_output* output);
|
||||
void close();
|
||||
|
||||
[[nodiscard]] bool isOpen() const noexcept;
|
||||
[[nodiscard]] wl_surface* wlSurface() const noexcept;
|
||||
|
||||
[[nodiscard]] bool onPointerEvent(const PointerEvent& event);
|
||||
void onKeyboardEvent(const KeyboardEvent& event);
|
||||
|
||||
void requestLayout();
|
||||
void requestRedraw();
|
||||
|
||||
// Dispatch keyboard events to the popup that currently owns the provided Wayland keyboard surface.
|
||||
[[nodiscard]] static bool dispatchKeyboardEvent(wl_surface* keyboardSurface, const KeyboardEvent& event);
|
||||
|
||||
[[nodiscard]] static PopupSurfaceConfig makeConfig(std::int32_t anchorX, std::int32_t anchorY,
|
||||
std::int32_t anchorWidth, std::int32_t anchorHeight,
|
||||
std::uint32_t width, std::uint32_t height, std::uint32_t serial,
|
||||
AnchorMode mode, std::int32_t offsetX = 0,
|
||||
std::int32_t offsetY = 0, bool grab = true);
|
||||
|
||||
private:
|
||||
void openCommon(PopupSurfaceConfig config, zwlr_layer_surface_v1* parentLayerSurface, xdg_surface* parentXdgSurface,
|
||||
wl_output* output);
|
||||
void buildScene(std::uint32_t width, std::uint32_t height);
|
||||
|
||||
WaylandConnection& m_wayland;
|
||||
RenderContext& m_renderContext;
|
||||
std::unique_ptr<PopupSurface> m_surface;
|
||||
std::unique_ptr<Node> m_sceneRoot;
|
||||
InputDispatcher m_inputDispatcher;
|
||||
wl_surface* m_wlSurface = nullptr;
|
||||
bool m_pointerInside = false;
|
||||
|
||||
ContentBuilder m_contentBuilder;
|
||||
SceneReadyCallback m_sceneReadyCallback;
|
||||
std::function<void()> m_onDismissed;
|
||||
|
||||
static std::vector<PopupWindow*>& openPopups();
|
||||
};
|
||||
@@ -34,6 +34,13 @@ DialogPopupHost::~DialogPopupHost() {
|
||||
assert(m_surface == nullptr && "subclass must call destroyPopup() in its destructor");
|
||||
}
|
||||
|
||||
void DialogPopupHost::initializeBase(WaylandConnection& wayland, ConfigService& config, RenderContext& renderContext) {
|
||||
m_wayland = &wayland;
|
||||
m_config = &config;
|
||||
m_renderContext = &renderContext;
|
||||
m_popupHosts = nullptr;
|
||||
}
|
||||
|
||||
void DialogPopupHost::initializeBase(WaylandConnection& wayland, ConfigService& config, RenderContext& renderContext,
|
||||
LayerPopupHostRegistry& popupHosts) {
|
||||
m_wayland = &wayland;
|
||||
@@ -75,6 +82,31 @@ bool DialogPopupHost::openPopup(std::uint32_t width, std::uint32_t height) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DialogPopupHost::openPopupAsChild(PopupSurfaceConfig config, xdg_surface* parentXdgSurface,
|
||||
wl_surface* parentWlSurface, wl_output* output) {
|
||||
if (m_wayland == nullptr || m_renderContext == nullptr || parentXdgSurface == nullptr || parentWlSurface == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
destroyPopup();
|
||||
m_parentSurface = parentWlSurface;
|
||||
|
||||
auto surface = std::make_unique<PopupSurface>(*m_wayland);
|
||||
surface->setAnimationManager(&m_animations);
|
||||
surface->setRenderContext(m_renderContext);
|
||||
surface->setConfigureCallback([this](std::uint32_t /*w*/, std::uint32_t /*h*/) { requestLayout(); });
|
||||
surface->setPrepareFrameCallback(
|
||||
[this](bool needsUpdate, bool needsLayout) { prepareFrame(needsUpdate, needsLayout); });
|
||||
surface->setDismissedCallback([this]() { cancel(); });
|
||||
|
||||
m_surface = std::move(surface);
|
||||
if (!m_surface->initializeAsChild(parentXdgSurface, output, config)) {
|
||||
destroyPopup();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void DialogPopupHost::destroyPopup() {
|
||||
if (m_attachedToHost && m_popupHosts != nullptr) {
|
||||
m_popupHosts->endAttachedPopup(m_parentSurface);
|
||||
|
||||
@@ -18,6 +18,8 @@ class WaylandConnection;
|
||||
struct KeyboardEvent;
|
||||
struct PointerEvent;
|
||||
struct wl_surface;
|
||||
struct wl_output;
|
||||
struct xdg_surface;
|
||||
|
||||
// Shared base for the three xdg_popup-backed dialog popups (GlyphPicker,
|
||||
// ColorPicker, FileDialog). Owns the PopupSurface, the scene scaffolding
|
||||
@@ -62,6 +64,7 @@ protected:
|
||||
DialogPopupHost();
|
||||
|
||||
// Called once during subclass `initialize` to capture the shared deps.
|
||||
void initializeBase(WaylandConnection& wayland, ConfigService& config, RenderContext& renderContext);
|
||||
void initializeBase(WaylandConnection& wayland, ConfigService& config, RenderContext& renderContext,
|
||||
LayerPopupHostRegistry& popupHosts);
|
||||
|
||||
@@ -72,6 +75,12 @@ protected:
|
||||
// automatically calls `destroyPopup()`.
|
||||
[[nodiscard]] bool openPopup(std::uint32_t width, std::uint32_t height);
|
||||
|
||||
// Build the PopupSurface as a child of an xdg parent. Uses the same scene/
|
||||
// input/prepareFrame plumbing as openPopup() but bypasses LayerPopupHostRegistry
|
||||
// parent resolution.
|
||||
[[nodiscard]] bool openPopupAsChild(PopupSurfaceConfig config, xdg_surface* parentXdgSurface,
|
||||
wl_surface* parentWlSurface, wl_output* output);
|
||||
|
||||
// Tear the popup down — endAttachedPopup, invoke `onSheetClose()` hook,
|
||||
// reset the scene tree, drop the PopupSurface. Safe to call repeatedly.
|
||||
void destroyPopup();
|
||||
|
||||
Reference in New Issue
Block a user