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:
@@ -704,6 +704,7 @@
|
||||
"appearance": "Appearance",
|
||||
"desktop": "Desktop",
|
||||
"dock": "Dock",
|
||||
"hooks": "Hooks",
|
||||
"panels": "Panels",
|
||||
"notifications": "Notifications",
|
||||
"backdrop": "Backdrop",
|
||||
@@ -727,6 +728,7 @@
|
||||
"focus-styling": "Focus Styling",
|
||||
"general": "General",
|
||||
"grouping": "Grouping",
|
||||
"lifecycle": "Lifecycle",
|
||||
"interface": "Interface",
|
||||
"launcher": "Launcher",
|
||||
"layout": "Layout",
|
||||
@@ -739,6 +741,7 @@
|
||||
"overview": "Overview",
|
||||
"pinned-apps": "Pinned Apps",
|
||||
"profile": "Profile",
|
||||
"power": "Power",
|
||||
"screen-corners": "Screen Corners",
|
||||
"security": "Security",
|
||||
"shape": "Shape",
|
||||
@@ -1546,6 +1549,71 @@
|
||||
"description": "Color temperature at night (Kelvin)"
|
||||
}
|
||||
},
|
||||
"hooks": {
|
||||
"events": {
|
||||
"started": {
|
||||
"label": "On Started",
|
||||
"description": "Command to run after Noctalia starts"
|
||||
},
|
||||
"wallpaper_changed": {
|
||||
"label": "On Wallpaper Changed",
|
||||
"description": "Command to run when the wallpaper changes"
|
||||
},
|
||||
"colors_changed": {
|
||||
"label": "On Colors Changed",
|
||||
"description": "Command to run when the active color palette changes"
|
||||
},
|
||||
"session_locked": {
|
||||
"label": "On Session Locked",
|
||||
"description": "Command to run when the session is locked"
|
||||
},
|
||||
"session_unlocked": {
|
||||
"label": "On Session Unlocked",
|
||||
"description": "Command to run after the session is unlocked"
|
||||
},
|
||||
"logging_out": {
|
||||
"label": "On Logout",
|
||||
"description": "Command to run before logging out"
|
||||
},
|
||||
"rebooting": {
|
||||
"label": "On Reboot",
|
||||
"description": "Command to run before rebooting"
|
||||
},
|
||||
"shutting_down": {
|
||||
"label": "On Shutdown",
|
||||
"description": "Command to run before shutdown"
|
||||
},
|
||||
"wifi_enabled": {
|
||||
"label": "On Wi-Fi Enabled",
|
||||
"description": "Command to run when Wi-Fi is enabled"
|
||||
},
|
||||
"wifi_disabled": {
|
||||
"label": "On Wi-Fi Disabled",
|
||||
"description": "Command to run when Wi-Fi is disabled"
|
||||
},
|
||||
"bluetooth_enabled": {
|
||||
"label": "On Bluetooth Enabled",
|
||||
"description": "Command to run when Bluetooth is enabled"
|
||||
},
|
||||
"bluetooth_disabled": {
|
||||
"label": "On Bluetooth Disabled",
|
||||
"description": "Command to run when Bluetooth is disabled"
|
||||
},
|
||||
"battery_state_changed": {
|
||||
"label": "On Battery State Changed",
|
||||
"description": "Command to run when charging state changes"
|
||||
},
|
||||
"battery_under_threshold": {
|
||||
"label": "On Battery Under Threshold",
|
||||
"description": "Command to run when battery percent drops under the configured threshold"
|
||||
}
|
||||
},
|
||||
"command-placeholder": "shell command or script path",
|
||||
"battery-low-threshold": {
|
||||
"label": "Battery Low Threshold",
|
||||
"description": "Percent threshold for the low-battery hook (0 disables it)"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"daemon": {
|
||||
"label": "Notification Daemon",
|
||||
|
||||
@@ -475,6 +475,7 @@ _noctalia_sources = files(
|
||||
'src/shell/settings/settings_registry.cpp',
|
||||
'src/shell/settings/settings_sidebar.cpp',
|
||||
'src/shell/settings/widget_settings_registry.cpp',
|
||||
'src/shell/settings/widget_add_popup.cpp',
|
||||
'src/shell/settings/settings_window.cpp',
|
||||
'src/shell/test/test_panel.cpp',
|
||||
'src/shell/tray/tray_drawer_panel.cpp',
|
||||
@@ -550,6 +551,7 @@ _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',
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#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"
|
||||
@@ -594,8 +595,8 @@ void Application::initServices() {
|
||||
}
|
||||
|
||||
try {
|
||||
m_brightnessService = std::make_unique<BrightnessService>(
|
||||
m_systemBus.get(), m_wayland, m_configService.config().brightness, &m_dependencyService);
|
||||
m_brightnessService =
|
||||
std::make_unique<BrightnessService>(m_systemBus.get(), m_wayland, m_configService.config().brightness);
|
||||
m_brightnessService->setChangeCallback([this, shouldRefreshControlCenter]() {
|
||||
m_brightnessOsd.onBrightnessChanged(*m_brightnessService);
|
||||
m_bar.refresh();
|
||||
@@ -808,6 +809,9 @@ 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()) {
|
||||
m_settingsWindow.onKeyboardEvent(event);
|
||||
|
||||
+68
-56
@@ -204,6 +204,71 @@ namespace {
|
||||
return {-(contentRight + k), 0.0f};
|
||||
}
|
||||
|
||||
void applyBarShadowStyle(BarInstance& instance, const ShellConfig::ShadowConfig& shadowConfig, float surfaceWidth,
|
||||
float surfaceHeight) {
|
||||
if (instance.shadow == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Radii barRadii{
|
||||
static_cast<float>(instance.barConfig.radiusTopLeft),
|
||||
static_cast<float>(instance.barConfig.radiusTopRight),
|
||||
static_cast<float>(instance.barConfig.radiusBottomRight),
|
||||
static_cast<float>(instance.barConfig.radiusBottomLeft),
|
||||
};
|
||||
const auto barVisual = computeBarVisualGeometry(instance.barConfig, shadowConfig, surfaceWidth, surfaceHeight);
|
||||
const float barAreaW = barVisual.width;
|
||||
const float barAreaH = barVisual.height;
|
||||
const float bgOpacity = std::clamp(instance.barConfig.backgroundOpacity, 0.0f, 1.0f);
|
||||
const float shadowX = barVisual.x + static_cast<float>(shadowConfig.offsetX);
|
||||
const float shadowY = barVisual.y + static_cast<float>(shadowConfig.offsetY);
|
||||
RoundedRectStyle shadowStyle =
|
||||
shell::surface_shadow::style(shadowConfig, bgOpacity, shell::surface_shadow::Shape{.radius = barRadii});
|
||||
|
||||
const bool panelShadowExclusion = instance.attachedPanelGeometry.has_value() &&
|
||||
instance.attachedPanelGeometry->width > 0.0f &&
|
||||
instance.attachedPanelGeometry->height > 0.0f;
|
||||
if (panelShadowExclusion) {
|
||||
const auto& attached = *instance.attachedPanelGeometry;
|
||||
const float convexRadius = std::max(0.0f, attached.cornerRadius);
|
||||
const float bulgeRadius = std::max(0.0f, attached.bulgeRadius);
|
||||
const std::string_view barPosition = instance.barConfig.position;
|
||||
const auto corners = attached_panel::cornerShapes(barPosition);
|
||||
const auto pickRadius = [&](CornerShape shape) {
|
||||
return shape == CornerShape::Concave ? bulgeRadius : convexRadius;
|
||||
};
|
||||
shadowStyle.shadowExclusion = true;
|
||||
shadowStyle.shadowExclusionOffsetX = shadowX - attached.x;
|
||||
shadowStyle.shadowExclusionOffsetY = shadowY - attached.y;
|
||||
shadowStyle.shadowExclusionWidth = attached.width;
|
||||
shadowStyle.shadowExclusionHeight = attached.height;
|
||||
shadowStyle.shadowExclusionCorners = corners;
|
||||
shadowStyle.shadowExclusionLogicalInset = attached_panel::logicalInset(barPosition, bulgeRadius);
|
||||
shadowStyle.shadowExclusionRadius =
|
||||
Radii{pickRadius(corners.tl), pickRadius(corners.tr), pickRadius(corners.br), pickRadius(corners.bl)};
|
||||
}
|
||||
|
||||
auto configureShadow = [&](Box* node, float x, float y) {
|
||||
if (node == nullptr) {
|
||||
return;
|
||||
}
|
||||
node->setStyle(shadowStyle);
|
||||
node->setZIndex(-1);
|
||||
node->setPosition(x, y);
|
||||
node->setSize(barAreaW, barAreaH);
|
||||
};
|
||||
|
||||
instance.shadow->setVisible(true);
|
||||
configureShadow(instance.shadow, shadowX, shadowY);
|
||||
|
||||
if (instance.shadowLeftClip != nullptr) {
|
||||
instance.shadowLeftClip->setVisible(false);
|
||||
}
|
||||
if (instance.shadowRightClip != nullptr) {
|
||||
instance.shadowRightClip->setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
void layoutBarSections(BarInstance& instance, Renderer& renderer, float barAreaW, float barAreaH, float padding,
|
||||
bool isVertical) {
|
||||
const float slotCross = isVertical ? barAreaW : barAreaH;
|
||||
@@ -711,10 +776,8 @@ void Bar::setAttachedPanelGeometry(wl_output* output, std::optional<AttachedPane
|
||||
|
||||
instance->attachedPanelGeometry = geometry;
|
||||
if (instance->surface != nullptr && instance->surface->width() > 0 && instance->surface->height() > 0) {
|
||||
if (m_renderContext != nullptr) {
|
||||
m_renderContext->syncContentScale(instance->surface->renderTarget());
|
||||
}
|
||||
buildScene(*instance, instance->surface->width(), instance->surface->height());
|
||||
applyBarShadowStyle(*instance, m_config->config().shell.shadow, static_cast<float>(instance->surface->width()),
|
||||
static_cast<float>(instance->surface->height()));
|
||||
instance->surface->requestRedraw();
|
||||
}
|
||||
}
|
||||
@@ -1425,58 +1488,7 @@ void Bar::buildScene(BarInstance& instance, std::uint32_t width, std::uint32_t h
|
||||
instance.contentClip->setSize(barAreaW, barAreaH);
|
||||
}
|
||||
|
||||
// Shadow — same shape as the bar, offset by (shadowOffsetX, shadowOffsetY), rendered with large
|
||||
// SDF softness to produce a Gaussian-like blurred drop shadow. Rendered at z=-1 so the bar sits
|
||||
// on top and hides the shadow's opaque interior.
|
||||
if (instance.shadow != nullptr) {
|
||||
const float bgOpacity = std::clamp(instance.barConfig.backgroundOpacity, 0.0f, 1.0f);
|
||||
const float shadowX = barAreaX + shadowOffsetX;
|
||||
const float shadowY = barAreaY + shadowOffsetY;
|
||||
RoundedRectStyle shadowStyle =
|
||||
shell::surface_shadow::style(shadowConfig, bgOpacity, shell::surface_shadow::Shape{.radius = barRadii});
|
||||
const bool panelShadowExclusion = instance.attachedPanelGeometry.has_value() &&
|
||||
instance.attachedPanelGeometry->width > 0.0f &&
|
||||
instance.attachedPanelGeometry->height > 0.0f;
|
||||
if (panelShadowExclusion) {
|
||||
const auto& attached = *instance.attachedPanelGeometry;
|
||||
const float convexRadius = std::max(0.0f, attached.cornerRadius);
|
||||
const float bulgeRadius = std::max(0.0f, attached.bulgeRadius);
|
||||
const std::string_view barPosition = instance.barConfig.position;
|
||||
const auto corners = attached_panel::cornerShapes(barPosition);
|
||||
const auto pickRadius = [&](CornerShape shape) {
|
||||
return shape == CornerShape::Concave ? bulgeRadius : convexRadius;
|
||||
};
|
||||
shadowStyle.shadowExclusion = true;
|
||||
shadowStyle.shadowExclusionOffsetX = shadowX - attached.x;
|
||||
shadowStyle.shadowExclusionOffsetY = shadowY - attached.y;
|
||||
shadowStyle.shadowExclusionWidth = attached.width;
|
||||
shadowStyle.shadowExclusionHeight = attached.height;
|
||||
shadowStyle.shadowExclusionCorners = corners;
|
||||
shadowStyle.shadowExclusionLogicalInset = attached_panel::logicalInset(barPosition, bulgeRadius);
|
||||
shadowStyle.shadowExclusionRadius =
|
||||
Radii{pickRadius(corners.tl), pickRadius(corners.tr), pickRadius(corners.br), pickRadius(corners.bl)};
|
||||
}
|
||||
|
||||
auto configureShadow = [&](Box* node, float x, float y) {
|
||||
if (node == nullptr) {
|
||||
return;
|
||||
}
|
||||
node->setStyle(shadowStyle);
|
||||
node->setZIndex(-1);
|
||||
node->setPosition(x, y);
|
||||
node->setSize(barAreaW, barAreaH);
|
||||
};
|
||||
|
||||
instance.shadow->setVisible(true);
|
||||
configureShadow(instance.shadow, shadowX, shadowY);
|
||||
|
||||
if (instance.shadowLeftClip != nullptr) {
|
||||
instance.shadowLeftClip->setVisible(false);
|
||||
}
|
||||
if (instance.shadowRightClip != nullptr) {
|
||||
instance.shadowRightClip->setVisible(false);
|
||||
}
|
||||
}
|
||||
applyBarShadowStyle(instance, shadowConfig, w, h);
|
||||
|
||||
layoutBarSections(instance, *renderer, barAreaW, barAreaH, padding, isVertical);
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include "render/core/renderer.h"
|
||||
#include "render/scene/input_area.h"
|
||||
#include "shell/panel/panel_manager.h"
|
||||
#include "system/brightness_service.h"
|
||||
#include "system/dependency_service.h"
|
||||
#include "ui/controls/button.h"
|
||||
#include "ui/controls/flex.h"
|
||||
@@ -25,7 +24,6 @@ ControlCenterPanel::ControlCenterPanel(
|
||||
IdleInhibitor* idleInhibitor, DependencyService* dependencies, WaylandConnection* wayland, Wallpaper* wallpaper) {
|
||||
(void)upower;
|
||||
m_config = config;
|
||||
m_brightness = brightness;
|
||||
m_notificationManager = notifications;
|
||||
m_dependencies = dependencies;
|
||||
m_tabs[tabIndex(TabId::Overview)] =
|
||||
@@ -251,9 +249,6 @@ void ControlCenterPanel::onOpen(std::string_view context) {
|
||||
if (m_dependencies != nullptr) {
|
||||
m_dependencies->rescan();
|
||||
}
|
||||
if (m_brightness != nullptr && m_config != nullptr) {
|
||||
m_brightness->reload(m_config->config().brightness);
|
||||
}
|
||||
selectTab(tabFromContext(context));
|
||||
}
|
||||
|
||||
|
||||
@@ -136,7 +136,6 @@ private:
|
||||
std::array<Flex*, kTabCount> m_tabHeaderActions{};
|
||||
TabId m_activeTab = TabId::Overview;
|
||||
ConfigService* m_config = nullptr;
|
||||
BrightnessService* m_brightness = nullptr;
|
||||
NotificationManager* m_notificationManager = nullptr;
|
||||
DependencyService* m_dependencies = nullptr;
|
||||
};
|
||||
|
||||
@@ -364,6 +364,8 @@ void LauncherPanel::onOpen(std::string_view context) {
|
||||
if (m_grid != nullptr) {
|
||||
m_grid->scrollView().setScrollOffset(0.0f);
|
||||
}
|
||||
// Clear cached icon misses before each open so newly installed app icons appear.
|
||||
m_iconResolver.invalidateCache();
|
||||
onInputChanged(initialValue);
|
||||
}
|
||||
|
||||
|
||||
@@ -1921,6 +1921,7 @@ namespace settings {
|
||||
if (!inherited) {
|
||||
const bool pickerOpenForLane = ctx.openWidgetPickerPath == pickerKey;
|
||||
auto addBtn = std::make_unique<Button>();
|
||||
auto* addBtnPtr = addBtn.get();
|
||||
addBtn->setText(i18n::tr("settings.entities.widget.add"));
|
||||
addBtn->setGlyph("add");
|
||||
addBtn->setVariant(pickerOpenForLane ? ButtonVariant::Default : ButtonVariant::Ghost);
|
||||
@@ -1933,7 +1934,18 @@ namespace settings {
|
||||
&editingWidgetName = ctx.editingWidgetName, &renamingWidgetName = ctx.renamingWidgetName,
|
||||
&pendingDeleteWidgetName = ctx.pendingDeleteWidgetName, pickerKey,
|
||||
&pendingDeleteWidgetSettingPath = ctx.pendingDeleteWidgetSettingPath,
|
||||
&creatingWidgetType = ctx.creatingWidgetType, requestRebuild = ctx.requestRebuild]() {
|
||||
&creatingWidgetType = ctx.creatingWidgetType, requestRebuild = ctx.requestRebuild,
|
||||
openWidgetAddPopup = ctx.openWidgetAddPopup, lanePath, addBtnPtr]() {
|
||||
if (openWidgetAddPopup) {
|
||||
openWidgetPickerPath.clear();
|
||||
editingWidgetName.clear();
|
||||
renamingWidgetName.clear();
|
||||
pendingDeleteWidgetName.clear();
|
||||
pendingDeleteWidgetSettingPath.clear();
|
||||
creatingWidgetType.clear();
|
||||
openWidgetAddPopup(lanePath, addBtnPtr);
|
||||
return;
|
||||
}
|
||||
openWidgetPickerPath = openWidgetPickerPath == pickerKey ? std::string{} : pickerKey;
|
||||
editingWidgetName.clear();
|
||||
renamingWidgetName.clear();
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace settings {
|
||||
std::function<void()> requestRebuild;
|
||||
std::function<void()> resetContentScroll;
|
||||
std::function<void(InputArea*)> focusArea;
|
||||
std::function<void(const std::vector<std::string>&, Button*)> openWidgetAddPopup;
|
||||
std::function<void(std::vector<std::string>, ConfigOverrideValue)> setOverride;
|
||||
std::function<void(std::vector<std::pair<std::vector<std::string>, ConfigOverrideValue>>)> setOverrides;
|
||||
std::function<void(std::vector<std::string>)> clearOverride;
|
||||
|
||||
@@ -648,7 +648,8 @@ namespace settings {
|
||||
return wrap;
|
||||
};
|
||||
|
||||
const auto makeText = [&](const std::string& value, const std::string& placeholder, std::vector<std::string> path) {
|
||||
const auto makeText = [&](const std::string& value, const std::string& placeholder, std::vector<std::string> path,
|
||||
float width = 0.0f) {
|
||||
auto input = std::make_unique<Input>();
|
||||
input->setValue(value);
|
||||
input->setPlaceholder(placeholder.empty() ? i18n::tr("settings.controls.list.add-entry-placeholder")
|
||||
@@ -656,7 +657,8 @@ namespace settings {
|
||||
input->setFontSize(Style::fontSizeBody * scale);
|
||||
input->setControlHeight(Style::controlHeight * scale);
|
||||
input->setHorizontalPadding(Style::spaceSm * scale);
|
||||
input->setSize(190.0f * scale, Style::controlHeight * scale);
|
||||
const float inputWidth = (width > 0.0f ? width : 190.0f) * scale;
|
||||
input->setSize(inputWidth, Style::controlHeight * scale);
|
||||
input->setOnSubmit([setOverride = ctx.setOverride, path](const std::string& v) { setOverride(path, v); });
|
||||
return input;
|
||||
};
|
||||
@@ -1182,7 +1184,7 @@ namespace settings {
|
||||
return makeSlider(control.value, control.minValue, control.maxValue, control.step, entry.path,
|
||||
control.integerValue, control.linkedCommit);
|
||||
} else if constexpr (std::is_same_v<T, TextSetting>) {
|
||||
return makeText(control.value, control.placeholder, entry.path);
|
||||
return makeText(control.value, control.placeholder, entry.path, control.width);
|
||||
} else if constexpr (std::is_same_v<T, OptionalNumberSetting>) {
|
||||
return makeOptionalNumber(control, entry.path);
|
||||
} else if constexpr (std::is_same_v<T, ColorSetting>) {
|
||||
@@ -1233,6 +1235,7 @@ namespace settings {
|
||||
.requestRebuild = ctx.requestRebuild,
|
||||
.resetContentScroll = ctx.resetContentScroll,
|
||||
.focusArea = ctx.focusArea,
|
||||
.openWidgetAddPopup = ctx.openBarWidgetAddPopup,
|
||||
.setOverride = ctx.setOverride,
|
||||
.setOverrides = ctx.setOverrides,
|
||||
.clearOverride = ctx.clearOverride,
|
||||
@@ -1249,8 +1252,10 @@ namespace settings {
|
||||
bool integerValue) -> std::unique_ptr<Node> {
|
||||
return makeSlider(value, minValue, maxValue, step, std::move(path), integerValue);
|
||||
},
|
||||
.makeText = [&](const std::string& value, const std::string& placeholder, std::vector<std::string> path)
|
||||
-> std::unique_ptr<Node> { return makeText(value, placeholder, std::move(path)); },
|
||||
.makeText = [&](const std::string& value, const std::string& placeholder,
|
||||
std::vector<std::string> path) -> std::unique_ptr<Node> {
|
||||
return makeText(value, placeholder, std::move(path));
|
||||
}, // width not used in search
|
||||
.makeColorRolePicker = [&](const ColorRolePickerSetting& setting, std::vector<std::string> path)
|
||||
-> std::unique_ptr<Node> { return makeColorRolePicker(setting, std::move(path)); },
|
||||
.makeListBlock = [&](Flex& section, const SettingEntry& entry,
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
class Flex;
|
||||
class InputArea;
|
||||
class Button;
|
||||
|
||||
namespace settings {
|
||||
|
||||
@@ -38,6 +39,7 @@ namespace settings {
|
||||
std::function<void()> requestContentRebuild;
|
||||
std::function<void()> resetContentScroll;
|
||||
std::function<void(InputArea*)> focusArea;
|
||||
std::function<void(const std::vector<std::string>&, Button*)> openBarWidgetAddPopup;
|
||||
std::function<void(std::vector<std::string>, ConfigOverrideValue)> setOverride;
|
||||
std::function<void(std::vector<std::pair<std::vector<std::string>, ConfigOverrideValue>>)> setOverrides;
|
||||
std::function<void(std::vector<std::string>)> clearOverride;
|
||||
|
||||
@@ -222,6 +222,8 @@ namespace settings {
|
||||
return "layout-board";
|
||||
if (section == "services")
|
||||
return "stack-2";
|
||||
if (section == "hooks")
|
||||
return "link";
|
||||
if (section == "notifications")
|
||||
return "bell";
|
||||
if (section == "bar")
|
||||
@@ -750,6 +752,68 @@ namespace settings {
|
||||
{"nightlight", "temperature_night"}, std::move(nightSlider), "wlsunset kelvin"));
|
||||
}
|
||||
|
||||
// Hooks
|
||||
auto hookGroup = [](HookKind kind) -> std::string {
|
||||
switch (kind) {
|
||||
case HookKind::Started:
|
||||
case HookKind::SessionLocked:
|
||||
case HookKind::SessionUnlocked:
|
||||
case HookKind::LoggingOut:
|
||||
case HookKind::Rebooting:
|
||||
case HookKind::ShuttingDown:
|
||||
return "lifecycle";
|
||||
case HookKind::WallpaperChanged:
|
||||
case HookKind::ColorsChanged:
|
||||
return "theme";
|
||||
case HookKind::WifiEnabled:
|
||||
case HookKind::WifiDisabled:
|
||||
case HookKind::BluetoothEnabled:
|
||||
case HookKind::BluetoothDisabled:
|
||||
return "network";
|
||||
case HookKind::BatteryStateChanged:
|
||||
case HookKind::BatteryUnderThreshold:
|
||||
return "power";
|
||||
case HookKind::Count:
|
||||
break;
|
||||
}
|
||||
return "general";
|
||||
};
|
||||
|
||||
auto hookTags = [](HookKind kind) -> std::string {
|
||||
std::string tags = "hook command script exec event trigger";
|
||||
if (kind == HookKind::BatteryUnderThreshold || kind == HookKind::BatteryStateChanged) {
|
||||
tags += " battery power";
|
||||
}
|
||||
if (kind == HookKind::WallpaperChanged || kind == HookKind::ColorsChanged) {
|
||||
tags += " wallpaper colors theme";
|
||||
}
|
||||
if (kind == HookKind::WifiEnabled || kind == HookKind::WifiDisabled || kind == HookKind::BluetoothEnabled ||
|
||||
kind == HookKind::BluetoothDisabled) {
|
||||
tags += " network wifi bluetooth";
|
||||
}
|
||||
if (kind == HookKind::SessionLocked || kind == HookKind::SessionUnlocked || kind == HookKind::LoggingOut ||
|
||||
kind == HookKind::Rebooting || kind == HookKind::ShuttingDown || kind == HookKind::Started) {
|
||||
tags += " session startup";
|
||||
}
|
||||
return tags;
|
||||
};
|
||||
|
||||
for (const auto& kind : kHookKinds) {
|
||||
const auto index = static_cast<std::size_t>(kind.value);
|
||||
const std::string key(kind.key);
|
||||
const std::string baseKey = "settings.schema.hooks.events." + key;
|
||||
const std::string hookCmd = cfg.hooks.commands[index].empty() ? "" : cfg.hooks.commands[index][0];
|
||||
entries.push_back(makeEntry(
|
||||
"hooks", hookGroup(kind.value), tr(baseKey + ".label"), tr(baseKey + ".description"), {"hooks", key},
|
||||
TextSetting{hookCmd, tr("settings.schema.hooks.command-placeholder"), 320.0f}, hookTags(kind.value)));
|
||||
}
|
||||
|
||||
entries.push_back(makeEntry(
|
||||
"hooks", "power", tr("settings.schema.hooks.battery-low-threshold.label"),
|
||||
tr("settings.schema.hooks.battery-low-threshold.description"), {"hooks", "battery_low_percent_threshold"},
|
||||
SliderSetting{static_cast<float>(cfg.hooks.batteryLowPercentThreshold), 0.0f, 100.0f, 1.0f, true},
|
||||
"battery threshold hook trigger"));
|
||||
|
||||
// Notifications
|
||||
entries.push_back(makeEntry("notifications", "general", tr("settings.schema.notifications.daemon.label"),
|
||||
tr("settings.schema.notifications.daemon.description"),
|
||||
|
||||
@@ -59,6 +59,7 @@ namespace settings {
|
||||
struct TextSetting {
|
||||
std::string value;
|
||||
std::string placeholder;
|
||||
float width = 0.0f; // 0 = use default
|
||||
};
|
||||
|
||||
struct OptionalNumberSetting {
|
||||
|
||||
@@ -49,6 +49,7 @@ 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;
|
||||
@@ -111,6 +112,73 @@ 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;
|
||||
}
|
||||
const auto& key = path.back();
|
||||
return key == "start" || key == "center" || key == "end";
|
||||
}
|
||||
|
||||
std::vector<std::string> barWidgetItemsForPath(const Config& cfg, const std::vector<std::string>& path) {
|
||||
if (!isBarWidgetListPath(path) || path.size() < 3) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto* bar = settings::findBar(cfg, path[1]);
|
||||
if (bar == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto& lane = path.back();
|
||||
if (path.size() >= 5 && path[2] == "monitor") {
|
||||
const auto* ovr = settings::findMonitorOverride(*bar, path[3]);
|
||||
if (ovr != nullptr) {
|
||||
if (lane == "start") {
|
||||
return ovr->startWidgets.value_or(bar->startWidgets);
|
||||
}
|
||||
if (lane == "center") {
|
||||
return ovr->centerWidgets.value_or(bar->centerWidgets);
|
||||
}
|
||||
if (lane == "end") {
|
||||
return ovr->endWidgets.value_or(bar->endWidgets);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lane == "start") {
|
||||
return bar->startWidgets;
|
||||
}
|
||||
if (lane == "center") {
|
||||
return bar->centerWidgets;
|
||||
}
|
||||
if (lane == "end") {
|
||||
return bar->endWidgets;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SettingsWindow::~SettingsWindow() = default;
|
||||
@@ -217,6 +285,10 @@ void SettingsWindow::destroyWindow() {
|
||||
m_actionsMenuPopup->close();
|
||||
m_actionsMenuPopup.reset();
|
||||
}
|
||||
if (m_widgetAddPopup != nullptr) {
|
||||
m_widgetAddPopup->close();
|
||||
m_widgetAddPopup.reset();
|
||||
}
|
||||
m_sceneRoot.reset();
|
||||
m_surface.reset();
|
||||
m_pointerInside = false;
|
||||
@@ -340,6 +412,9 @@ void SettingsWindow::clearTransientSettingsState() {
|
||||
m_pendingDeleteMonitorOverrideBarName.clear();
|
||||
m_pendingDeleteMonitorOverrideMatch.clear();
|
||||
m_pendingResetPageScope.clear();
|
||||
if (m_widgetAddPopup != nullptr && m_widgetAddPopup->isOpen()) {
|
||||
m_widgetAddPopup->close();
|
||||
}
|
||||
}
|
||||
|
||||
void SettingsWindow::openActionsMenu() {
|
||||
@@ -401,6 +476,50 @@ void SettingsWindow::openActionsMenu() {
|
||||
static_cast<std::int32_t>(m_actionsMenuButton->height()), m_surface->xdgSurface(), output);
|
||||
}
|
||||
|
||||
void SettingsWindow::openBarWidgetAddPopup(const std::vector<std::string>& lanePath, Button* anchorButton) {
|
||||
if (m_wayland == nullptr || m_renderContext == nullptr || m_surface == nullptr || anchorButton == nullptr ||
|
||||
m_surface->xdgSurface() == nullptr || m_config == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
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) {
|
||||
if (value.empty() || m_config == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Config& activeConfig = m_config->config();
|
||||
auto laneItems = barWidgetItemsForPath(activeConfig, selectedLanePath);
|
||||
|
||||
m_pendingDeleteWidgetName.clear();
|
||||
m_pendingDeleteWidgetSettingPath.clear();
|
||||
m_renamingWidgetName.clear();
|
||||
m_editingWidgetName.clear();
|
||||
|
||||
if (const auto type = createInstanceTypeFromValue(value); !type.empty()) {
|
||||
m_creatingWidgetType = type;
|
||||
m_openWidgetPickerPath = pathKey(selectedLanePath);
|
||||
requestSceneRebuild();
|
||||
return;
|
||||
}
|
||||
|
||||
m_creatingWidgetType.clear();
|
||||
m_openWidgetPickerPath.clear();
|
||||
laneItems.push_back(value);
|
||||
setSettingOverride(selectedLanePath, laneItems);
|
||||
});
|
||||
}
|
||||
|
||||
wl_output* output = m_wayland->lastPointerOutput();
|
||||
if (output == nullptr) {
|
||||
output = m_output;
|
||||
}
|
||||
|
||||
m_widgetAddPopup->open(m_surface->xdgSurface(), output, m_wayland->lastInputSerial(), anchorButton, lanePath,
|
||||
m_config->config(), uiScale(), PopupWindow::AnchorMode::CenterOnAnchor);
|
||||
}
|
||||
|
||||
void SettingsWindow::saveSupportReport() {
|
||||
if (m_config == nullptr) {
|
||||
return;
|
||||
@@ -898,33 +1017,36 @@ void SettingsWindow::rebuildSettingsContent() {
|
||||
},
|
||||
});
|
||||
|
||||
settings::addSettingsContentSections(*m_contentContainer, m_settingsRegistry,
|
||||
settings::SettingsContentContext{
|
||||
.config = cfg,
|
||||
.configService = m_config,
|
||||
.scale = scale,
|
||||
.searchQuery = m_searchQuery,
|
||||
.selectedSection = m_selectedSection,
|
||||
.selectedBar = selectedBar,
|
||||
.selectedMonitorOverride = selectedMonitorOverride,
|
||||
.showAdvanced = m_showAdvanced,
|
||||
.showOverriddenOnly = m_showOverriddenOnly,
|
||||
.openWidgetPickerPath = m_openWidgetPickerPath,
|
||||
.openSearchPickerPath = m_openSearchPickerPath,
|
||||
.editingWidgetName = m_editingWidgetName,
|
||||
.pendingDeleteWidgetName = m_pendingDeleteWidgetName,
|
||||
.pendingDeleteWidgetSettingPath = m_pendingDeleteWidgetSettingPath,
|
||||
.renamingWidgetName = m_renamingWidgetName,
|
||||
.creatingWidgetType = m_creatingWidgetType,
|
||||
.requestRebuild = requestRebuild,
|
||||
.requestContentRebuild = requestContent,
|
||||
.resetContentScroll = [this]() { m_contentScrollState.offset = 0.0f; },
|
||||
.focusArea = [this](InputArea* area) { m_inputDispatcher.setFocus(area); },
|
||||
.setOverride = setOverride,
|
||||
.setOverrides = setOverrides,
|
||||
.clearOverride = clearOverride,
|
||||
.renameWidgetInstance = renameWidget,
|
||||
});
|
||||
settings::addSettingsContentSections(
|
||||
*m_contentContainer, m_settingsRegistry,
|
||||
settings::SettingsContentContext{
|
||||
.config = cfg,
|
||||
.configService = m_config,
|
||||
.scale = scale,
|
||||
.searchQuery = m_searchQuery,
|
||||
.selectedSection = m_selectedSection,
|
||||
.selectedBar = selectedBar,
|
||||
.selectedMonitorOverride = selectedMonitorOverride,
|
||||
.showAdvanced = m_showAdvanced,
|
||||
.showOverriddenOnly = m_showOverriddenOnly,
|
||||
.openWidgetPickerPath = m_openWidgetPickerPath,
|
||||
.openSearchPickerPath = m_openSearchPickerPath,
|
||||
.editingWidgetName = m_editingWidgetName,
|
||||
.pendingDeleteWidgetName = m_pendingDeleteWidgetName,
|
||||
.pendingDeleteWidgetSettingPath = m_pendingDeleteWidgetSettingPath,
|
||||
.renamingWidgetName = m_renamingWidgetName,
|
||||
.creatingWidgetType = m_creatingWidgetType,
|
||||
.requestRebuild = requestRebuild,
|
||||
.requestContentRebuild = requestContent,
|
||||
.resetContentScroll = [this]() { m_contentScrollState.offset = 0.0f; },
|
||||
.focusArea = [this](InputArea* area) { m_inputDispatcher.setFocus(area); },
|
||||
.openBarWidgetAddPopup = [this](const std::vector<std::string>& lanePath,
|
||||
Button* anchorButton) { openBarWidgetAddPopup(lanePath, anchorButton); },
|
||||
.setOverride = setOverride,
|
||||
.setOverrides = setOverrides,
|
||||
.clearOverride = clearOverride,
|
||||
.renameWidgetInstance = renameWidget,
|
||||
});
|
||||
}
|
||||
|
||||
void SettingsWindow::buildScene(std::uint32_t width, std::uint32_t height) {
|
||||
@@ -1315,6 +1437,15 @@ bool SettingsWindow::onPointerEvent(const PointerEvent& event) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_widgetAddPopup != nullptr && m_widgetAddPopup->onPointerEvent(event)) {
|
||||
return true;
|
||||
}
|
||||
if (m_widgetAddPopup != nullptr && m_widgetAddPopup->isOpen() && event.type == PointerEvent::Type::Button &&
|
||||
event.state == 1) {
|
||||
m_widgetAddPopup->close();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (m_actionsMenuPopup != nullptr && m_actionsMenuPopup->onPointerEvent(event)) {
|
||||
return true;
|
||||
}
|
||||
@@ -1390,6 +1521,16 @@ void SettingsWindow::onKeyboardEvent(const KeyboardEvent& event) {
|
||||
if (!isOpen() || m_config == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_widgetAddPopup != nullptr && m_widgetAddPopup->isOpen()) {
|
||||
if (event.pressed && m_config->matchesKeybind(KeybindAction::Cancel, event.sym, event.modifiers)) {
|
||||
m_widgetAddPopup->close();
|
||||
return;
|
||||
}
|
||||
m_widgetAddPopup->onKeyboardEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto requestRebuild = [this]() {
|
||||
if (m_surface != nullptr) {
|
||||
m_rebuildRequested = true;
|
||||
@@ -1443,12 +1584,18 @@ void SettingsWindow::onKeyboardEvent(const KeyboardEvent& event) {
|
||||
|
||||
void SettingsWindow::onThemeChanged() {
|
||||
if (isOpen()) {
|
||||
if (m_widgetAddPopup != nullptr && m_widgetAddPopup->isOpen()) {
|
||||
m_widgetAddPopup->requestRedraw();
|
||||
}
|
||||
m_surface->requestRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
void SettingsWindow::onFontChanged() {
|
||||
if (isOpen()) {
|
||||
if (m_widgetAddPopup != nullptr && m_widgetAddPopup->isOpen()) {
|
||||
m_widgetAddPopup->requestLayout();
|
||||
}
|
||||
m_surface->requestLayout();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "render/scene/input_dispatcher.h"
|
||||
#include "render/scene/node.h"
|
||||
#include "shell/settings/settings_registry.h"
|
||||
#include "shell/settings/widget_add_popup.h"
|
||||
#include "ui/controls/context_menu_popup.h"
|
||||
#include "ui/controls/scroll_view.h"
|
||||
#include "wayland/toplevel_surface.h"
|
||||
@@ -58,6 +59,7 @@ private:
|
||||
void clearStatusMessage();
|
||||
void clearTransientSettingsState();
|
||||
void openActionsMenu();
|
||||
void openBarWidgetAddPopup(const std::vector<std::string>& lanePath, Button* anchorButton);
|
||||
void saveSupportReport();
|
||||
void saveFlattenedConfig();
|
||||
void setSettingOverride(std::vector<std::string> path, ConfigOverrideValue value);
|
||||
@@ -87,6 +89,7 @@ private:
|
||||
Button* m_actionsMenuButton = nullptr;
|
||||
Flex* m_contentContainer = nullptr;
|
||||
std::unique_ptr<ContextMenuPopup> m_actionsMenuPopup;
|
||||
std::unique_ptr<settings::WidgetAddPopup> m_widgetAddPopup;
|
||||
InputDispatcher m_inputDispatcher;
|
||||
AnimationManager m_animations;
|
||||
bool m_pointerInside = false;
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
#include "shell/settings/widget_add_popup.h"
|
||||
|
||||
#include "config/config_service.h"
|
||||
#include "i18n/i18n.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/label.h"
|
||||
#include "ui/palette.h"
|
||||
#include "ui/style.h"
|
||||
#include "wayland/wayland_connection.h"
|
||||
#include "wayland/wayland_seat.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#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");
|
||||
}
|
||||
if (lane == "center") {
|
||||
return i18n::tr("settings.entities.widget.lanes.center");
|
||||
}
|
||||
if (lane == "end") {
|
||||
return i18n::tr("settings.entities.widget.lanes.end");
|
||||
}
|
||||
return std::string(lane);
|
||||
}
|
||||
|
||||
std::unique_ptr<Label> makeLabel(std::string_view text, float fontSize, const ColorSpec& color, bool bold = false) {
|
||||
auto label = std::make_unique<Label>();
|
||||
label->setText(text);
|
||||
label->setFontSize(fontSize);
|
||||
label->setColor(color);
|
||||
label->setBold(bold);
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
WidgetAddPopup::~WidgetAddPopup() = default;
|
||||
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto pickerEntries = widgetPickerEntries(config);
|
||||
std::vector<SearchPickerOption> options;
|
||||
options.reserve(pickerEntries.size() * 2);
|
||||
for (const auto& entry : pickerEntries) {
|
||||
options.push_back(SearchPickerOption{.value = entry.value,
|
||||
.label = entry.label,
|
||||
.description = entry.description,
|
||||
.category = entry.category,
|
||||
.enabled = true});
|
||||
if (entry.kind != WidgetReferenceKind::BuiltIn) {
|
||||
continue;
|
||||
}
|
||||
for (const auto& spec : widgetTypeSpecs()) {
|
||||
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"),
|
||||
.enabled = true,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_lanePath = lanePath;
|
||||
m_searchPicker = nullptr;
|
||||
|
||||
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_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);
|
||||
|
||||
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));
|
||||
|
||||
auto spacer = std::make_unique<Flex>();
|
||||
spacer->setFlexGrow(1.0f);
|
||||
header->addChild(std::move(spacer));
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
header->addChild(std::move(closeBtn));
|
||||
root->addChild(std::move(header));
|
||||
|
||||
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->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) {
|
||||
return;
|
||||
}
|
||||
auto* area = m_searchPicker->filterInputArea();
|
||||
if (area != nullptr) {
|
||||
dispatcher.setFocus(area);
|
||||
m_focusSearchOnOpen = false;
|
||||
}
|
||||
});
|
||||
|
||||
float anchorAbsX = 0.0f;
|
||||
float anchorAbsY = 0.0f;
|
||||
Node::absolutePosition(anchorButton, anchorAbsX, anchorAbsY);
|
||||
|
||||
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);
|
||||
|
||||
m_focusSearchOnOpen = true;
|
||||
m_popup->openAsChild(cfg, parentXdgSurface, output);
|
||||
}
|
||||
|
||||
void WidgetAddPopup::close() {
|
||||
if (m_popup != nullptr) {
|
||||
m_popup->close();
|
||||
}
|
||||
}
|
||||
|
||||
bool WidgetAddPopup::isOpen() const noexcept { return m_popup != nullptr && m_popup->isOpen(); }
|
||||
|
||||
bool WidgetAddPopup::onPointerEvent(const PointerEvent& event) {
|
||||
return m_popup != nullptr && m_popup->onPointerEvent(event);
|
||||
}
|
||||
|
||||
void WidgetAddPopup::onKeyboardEvent(const KeyboardEvent& event) {
|
||||
if (m_popup != nullptr) {
|
||||
m_popup->onKeyboardEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
void WidgetAddPopup::requestLayout() {
|
||||
if (m_popup != nullptr) {
|
||||
m_popup->requestLayout();
|
||||
}
|
||||
}
|
||||
|
||||
void WidgetAddPopup::requestRedraw() {
|
||||
if (m_popup != nullptr) {
|
||||
m_popup->requestRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace settings
|
||||
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui/controls/popup_window.h"
|
||||
#include "ui/controls/search_picker.h"
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class Button;
|
||||
class InputDispatcher;
|
||||
class RenderContext;
|
||||
class WaylandConnection;
|
||||
struct Config;
|
||||
struct KeyboardEvent;
|
||||
struct PointerEvent;
|
||||
struct wl_output;
|
||||
struct xdg_surface;
|
||||
|
||||
namespace settings {
|
||||
|
||||
class WidgetAddPopup {
|
||||
public:
|
||||
using SelectCallback = std::function<void(const std::vector<std::string>& lanePath, const std::string& value)>;
|
||||
|
||||
WidgetAddPopup(WaylandConnection& wayland, RenderContext& renderContext);
|
||||
~WidgetAddPopup();
|
||||
|
||||
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);
|
||||
void close();
|
||||
|
||||
[[nodiscard]] bool isOpen() const noexcept;
|
||||
[[nodiscard]] bool onPointerEvent(const PointerEvent& event);
|
||||
void onKeyboardEvent(const KeyboardEvent& event);
|
||||
void requestLayout();
|
||||
void requestRedraw();
|
||||
|
||||
private:
|
||||
WaylandConnection& m_wayland;
|
||||
RenderContext& m_renderContext;
|
||||
std::unique_ptr<PopupWindow> m_popup;
|
||||
std::vector<std::string> m_lanePath;
|
||||
SearchPicker* m_searchPicker = nullptr;
|
||||
bool m_focusSearchOnOpen = false;
|
||||
SelectCallback m_onSelect;
|
||||
std::function<void()> m_onDismissed;
|
||||
};
|
||||
|
||||
} // namespace settings
|
||||
@@ -512,14 +512,14 @@ void TrayMenu::refreshEntries() {
|
||||
m_entries = m_tray->menuEntries(m_activeItemId);
|
||||
if (!m_entries.empty() && trayDrawerEnabled(m_config)) {
|
||||
const bool pinned = activeItemPinned();
|
||||
m_entries.push_back(TrayMenuEntry{
|
||||
.id = kPinToggleEntryId,
|
||||
.label = i18n::tr(pinned ? "tray.menu.unpin" : "tray.menu.pin"),
|
||||
.enabled = true,
|
||||
.visible = true,
|
||||
.separator = false,
|
||||
.hasSubmenu = false,
|
||||
});
|
||||
m_entries.insert(m_entries.begin(), TrayMenuEntry{
|
||||
.id = kPinToggleEntryId,
|
||||
.label = i18n::tr(pinned ? "tray.menu.unpin" : "tray.menu.pin"),
|
||||
.enabled = true,
|
||||
.visible = true,
|
||||
.separator = false,
|
||||
.hasSubmenu = false,
|
||||
});
|
||||
}
|
||||
if (m_entries.empty()) {
|
||||
m_entries.push_back(TrayMenuEntry{
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
#include "config/config_service.h"
|
||||
#include "core/log.h"
|
||||
#include "core/process.h"
|
||||
#include "core/timer_manager.h"
|
||||
#include "dbus/system_bus.h"
|
||||
#include "ipc/ipc_arg_parse.h"
|
||||
#include "ipc/ipc_service.h"
|
||||
#include "system/dependency_service.h"
|
||||
#include "wayland/wayland_connection.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -586,7 +586,6 @@ namespace {
|
||||
struct BrightnessService::Impl {
|
||||
SystemBus* bus = nullptr;
|
||||
WaylandConnection& wayland;
|
||||
DependencyService* dependencies = nullptr;
|
||||
BrightnessConfig activeConfig;
|
||||
ChangeCallback changeCallback;
|
||||
|
||||
@@ -614,8 +613,8 @@ struct BrightnessService::Impl {
|
||||
std::unordered_map<std::string, DdcJob> pendingRefreshes;
|
||||
std::queue<WorkerCompletion> completions;
|
||||
|
||||
Impl(SystemBus* systemBus, WaylandConnection& wl, const BrightnessConfig& config, DependencyService* deps)
|
||||
: bus(systemBus), wayland(wl), dependencies(deps), activeConfig(config) {
|
||||
Impl(SystemBus* systemBus, WaylandConnection& wl, const BrightnessConfig& config)
|
||||
: bus(systemBus), wayland(wl), activeConfig(config) {
|
||||
setupPollFds();
|
||||
workerThread = std::thread([this]() { workerLoop(); });
|
||||
}
|
||||
@@ -817,7 +816,7 @@ struct BrightnessService::Impl {
|
||||
if (!activeConfig.enableDdcutil) {
|
||||
return;
|
||||
}
|
||||
if (dependencies != nullptr && !dependencies->hasDdcutil()) {
|
||||
if (!process::commandExists("ddcutil")) {
|
||||
if (!warnedMissingDdcutil) {
|
||||
kLog.warn("brightness.enable_ddcutil is set but ddcutil is not installed");
|
||||
warnedMissingDdcutil = true;
|
||||
@@ -1308,9 +1307,8 @@ struct BrightnessService::Impl {
|
||||
}
|
||||
};
|
||||
|
||||
BrightnessService::BrightnessService(SystemBus* bus, WaylandConnection& wayland, const BrightnessConfig& config,
|
||||
DependencyService* dependencies)
|
||||
: m_impl(new Impl(bus, wayland, config, dependencies)) {
|
||||
BrightnessService::BrightnessService(SystemBus* bus, WaylandConnection& wayland, const BrightnessConfig& config)
|
||||
: m_impl(new Impl(bus, wayland, config)) {
|
||||
m_impl->rebuildState(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include <vector>
|
||||
|
||||
class IpcService;
|
||||
class DependencyService;
|
||||
class SystemBus;
|
||||
class WaylandConnection;
|
||||
struct BrightnessConfig;
|
||||
@@ -30,8 +29,7 @@ class BrightnessService {
|
||||
public:
|
||||
using ChangeCallback = std::function<void()>;
|
||||
|
||||
BrightnessService(SystemBus* bus, WaylandConnection& wayland, const BrightnessConfig& config,
|
||||
DependencyService* dependencies = nullptr);
|
||||
BrightnessService(SystemBus* bus, WaylandConnection& wayland, const BrightnessConfig& config);
|
||||
~BrightnessService();
|
||||
|
||||
BrightnessService(const BrightnessService&) = delete;
|
||||
|
||||
@@ -392,6 +392,8 @@ namespace {
|
||||
|
||||
IconResolver::IconResolver() { rebuild(); }
|
||||
|
||||
void IconResolver::invalidateCache() { rebuild(); }
|
||||
|
||||
bool IconResolver::checkThemeChanged() {
|
||||
auto& state = iconThemeState();
|
||||
IconThemePlan next = buildThemePlan();
|
||||
|
||||
@@ -10,6 +10,7 @@ public:
|
||||
IconResolver();
|
||||
|
||||
const std::string& resolve(const std::string& iconName);
|
||||
void invalidateCache();
|
||||
|
||||
static bool checkThemeChanged();
|
||||
static std::uint64_t themeGeneration();
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
#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;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
#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();
|
||||
};
|
||||
Reference in New Issue
Block a user