mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
feat(bar): middle clicking on any widget will open its setting
This commit is contained in:
@@ -1259,6 +1259,10 @@
|
||||
"label": "Password Style",
|
||||
"description": "Visual style for password input masking"
|
||||
},
|
||||
"middle-click-opens-widget-settings": {
|
||||
"label": "Middle Click Opens Widget Settings",
|
||||
"description": "Open a bar widget's settings from the bar with middle click"
|
||||
},
|
||||
"show-location": {
|
||||
"label": "Show Location",
|
||||
"description": "Show location name in weather widgets"
|
||||
|
||||
@@ -12,6 +12,7 @@ telemetry_enabled = false # send an anonymous usage ping on startup
|
||||
polkit_agent = false
|
||||
password_style = "default" # default | random
|
||||
settings_show_advanced = false # show advanced settings by default in Settings
|
||||
middle_click_opens_widget_settings = true # middle-click bar widgets to open their Settings entry
|
||||
show_location = true # hide weather location text in shell UI when false
|
||||
clipboard_auto_paste = "auto" # off | auto | ctrl_v | ctrl_shift_v | shift_insert
|
||||
# avatar_path = "~/Pictures/avatar.png"
|
||||
|
||||
@@ -909,6 +909,12 @@ void Application::initUi() {
|
||||
&m_httpClient, &m_weatherService, &m_renderContext, &m_nightLightManager, &m_themeService,
|
||||
m_bluetoothService.get(), m_brightnessService.get(), kLockKeysEnabled ? &m_lockKeysService : nullptr,
|
||||
&m_fileWatcher);
|
||||
m_bar.setOpenWidgetSettingsCallback([this](std::string barName, std::string widgetName) {
|
||||
if (m_panelManager.isOpen()) {
|
||||
m_panelManager.closePanel();
|
||||
}
|
||||
m_settingsWindow.openToBarWidget(std::move(barName), std::move(widgetName));
|
||||
});
|
||||
m_panelManager.setAttachedPanelGeometryCallback(
|
||||
[this](wl_output* output, std::optional<AttachedPanelGeometry> geometry) {
|
||||
m_bar.setAttachedPanelGeometry(output, geometry);
|
||||
|
||||
@@ -289,11 +289,11 @@ namespace {
|
||||
a.polkitAgent == b.polkitAgent && a.passwordMaskStyle == b.passwordMaskStyle &&
|
||||
a.animation.enabled == b.animation.enabled && nearlyEqual(a.animation.speed, b.animation.speed) &&
|
||||
a.avatarPath == b.avatarPath && a.settingsShowAdvanced == b.settingsShowAdvanced &&
|
||||
a.showLocation == b.showLocation && a.clipboardAutoPaste == b.clipboardAutoPaste &&
|
||||
a.shadow.blur == b.shadow.blur && a.shadow.offsetX == b.shadow.offsetX &&
|
||||
a.shadow.offsetY == b.shadow.offsetY && nearlyEqual(a.shadow.alpha, b.shadow.alpha) &&
|
||||
a.panel.backgroundBlur == b.panel.backgroundBlur && a.panel.attachLauncher == b.panel.attachLauncher &&
|
||||
a.panel.attachClipboard == b.panel.attachClipboard &&
|
||||
a.middleClickOpensWidgetSettings == b.middleClickOpensWidgetSettings && a.showLocation == b.showLocation &&
|
||||
a.clipboardAutoPaste == b.clipboardAutoPaste && a.shadow.blur == b.shadow.blur &&
|
||||
a.shadow.offsetX == b.shadow.offsetX && a.shadow.offsetY == b.shadow.offsetY &&
|
||||
nearlyEqual(a.shadow.alpha, b.shadow.alpha) && a.panel.backgroundBlur == b.panel.backgroundBlur &&
|
||||
a.panel.attachLauncher == b.panel.attachLauncher && a.panel.attachClipboard == b.panel.attachClipboard &&
|
||||
a.panel.attachControlCenter == b.panel.attachControlCenter &&
|
||||
a.panel.attachWallpaper == b.panel.attachWallpaper && a.screenCorners.enabled == b.screenCorners.enabled &&
|
||||
a.screenCorners.size == b.screenCorners.size && a.mpris.blacklist == b.mpris.blacklist;
|
||||
|
||||
@@ -1253,6 +1253,9 @@ void ConfigService::parseTableInto(const toml::table& tbl, Config& config, bool
|
||||
if (auto v = (*shellTbl)["settings_show_advanced"].value<bool>()) {
|
||||
shell.settingsShowAdvanced = *v;
|
||||
}
|
||||
if (auto v = (*shellTbl)["middle_click_opens_widget_settings"].value<bool>()) {
|
||||
shell.middleClickOpensWidgetSettings = *v;
|
||||
}
|
||||
if (auto v = (*shellTbl)["show_location"].value<bool>()) {
|
||||
shell.showLocation = *v;
|
||||
}
|
||||
|
||||
@@ -377,6 +377,7 @@ struct ShellConfig {
|
||||
AnimationConfig animation;
|
||||
std::string avatarPath;
|
||||
bool settingsShowAdvanced = false;
|
||||
bool middleClickOpensWidgetSettings = true;
|
||||
bool showLocation = true;
|
||||
ClipboardAutoPasteMode clipboardAutoPaste = ClipboardAutoPasteMode::Auto;
|
||||
ShadowConfig shadow;
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
|
||||
#include "cursor-shape-v1-client-protocol.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::uint32_t kMouseButtonBase = BTN_MOUSE;
|
||||
constexpr std::uint32_t kMaxTrackedMouseButtons = 32;
|
||||
|
||||
} // namespace
|
||||
|
||||
InputArea::InputArea() : Node(NodeType::Base) {}
|
||||
|
||||
InputArea::~InputArea() {
|
||||
@@ -12,6 +19,25 @@ InputArea::~InputArea() {
|
||||
|
||||
void InputArea::setDestroyCallback(DestroyCallback callback) { m_destroyCallback = std::move(callback); }
|
||||
|
||||
std::uint32_t InputArea::buttonMask(std::uint32_t button) noexcept {
|
||||
if (button < kMouseButtonBase) {
|
||||
return 0;
|
||||
}
|
||||
const std::uint32_t index = button - kMouseButtonBase;
|
||||
if (index >= kMaxTrackedMouseButtons) {
|
||||
return 0;
|
||||
}
|
||||
return 1u << index;
|
||||
}
|
||||
|
||||
std::uint32_t InputArea::buttonMask(std::initializer_list<std::uint32_t> buttons) noexcept {
|
||||
std::uint32_t mask = 0;
|
||||
for (const auto button : buttons) {
|
||||
mask |= buttonMask(button);
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
void InputArea::setOnEnter(PointerCallback callback) { m_onEnter = std::move(callback); }
|
||||
void InputArea::setOnLeave(VoidCallback callback) { m_onLeave = std::move(callback); }
|
||||
void InputArea::setOnMotion(PointerCallback callback) { m_onMotion = std::move(callback); }
|
||||
@@ -32,6 +58,9 @@ void InputArea::setOnClick(PointerCallback callback) {
|
||||
|
||||
void InputArea::setCursorShape(std::uint32_t shape) { m_cursorShape = shape; }
|
||||
void InputArea::setAcceptedButtons(std::uint32_t mask) { m_acceptedButtons = mask; }
|
||||
bool InputArea::acceptsButton(std::uint32_t button) const noexcept {
|
||||
return (m_acceptedButtons & buttonMask(button)) != 0;
|
||||
}
|
||||
void InputArea::setPropagateEvents(bool propagate) { m_propagateEvents = propagate; }
|
||||
void InputArea::setEnabled(bool enabled) { m_enabled = enabled; }
|
||||
void InputArea::setFocusable(bool focusable) { m_focusable = focusable; }
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <initializer_list>
|
||||
#include <linux/input-event-codes.h>
|
||||
|
||||
class InputArea : public Node {
|
||||
@@ -45,6 +46,9 @@ public:
|
||||
InputArea();
|
||||
~InputArea() override;
|
||||
|
||||
[[nodiscard]] static std::uint32_t buttonMask(std::uint32_t button) noexcept;
|
||||
[[nodiscard]] static std::uint32_t buttonMask(std::initializer_list<std::uint32_t> buttons) noexcept;
|
||||
|
||||
// InputArea is a transparent hit-test wrapper with no layout semantics of its
|
||||
// own; its internal layout hook forwards to visible children so callers can
|
||||
// use it as a clickable container without manually re-laying children.
|
||||
@@ -73,6 +77,7 @@ public:
|
||||
|
||||
void setAcceptedButtons(std::uint32_t mask);
|
||||
[[nodiscard]] std::uint32_t acceptedButtons() const noexcept { return m_acceptedButtons; }
|
||||
[[nodiscard]] bool acceptsButton(std::uint32_t button) const noexcept;
|
||||
|
||||
void setPropagateEvents(bool propagate);
|
||||
[[nodiscard]] bool propagateEvents() const noexcept { return m_propagateEvents; }
|
||||
@@ -114,7 +119,7 @@ private:
|
||||
VoidCallback m_onFocusLoss;
|
||||
|
||||
std::uint32_t m_cursorShape = 0;
|
||||
std::uint32_t m_acceptedButtons = BTN_LEFT;
|
||||
std::uint32_t m_acceptedButtons = buttonMask(BTN_LEFT);
|
||||
bool m_propagateEvents = false;
|
||||
bool m_enabled = true;
|
||||
bool m_hovered = false;
|
||||
|
||||
@@ -17,6 +17,16 @@ namespace {
|
||||
return false;
|
||||
}
|
||||
|
||||
InputArea* inputAreaAcceptingButton(InputArea* area, std::uint32_t button) {
|
||||
for (Node* node = area; node != nullptr; node = node->parent()) {
|
||||
auto* candidate = dynamic_cast<InputArea*>(node);
|
||||
if (candidate != nullptr && candidate->enabled() && candidate->acceptsButton(button)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void InputDispatcher::setSceneRoot(Node* root) {
|
||||
@@ -77,12 +87,12 @@ bool InputDispatcher::pointerButton(float x, float y, std::uint32_t button, bool
|
||||
|
||||
pruneDetachedAreas();
|
||||
|
||||
InputArea* target = m_capturedArea != nullptr ? m_capturedArea : m_hoveredArea;
|
||||
InputArea* target = m_capturedArea != nullptr ? m_capturedArea : inputAreaAcceptingButton(m_hoveredArea, button);
|
||||
|
||||
// Press with no hover target: subtree may have been rebuilt (same global coords, new InputArea*).
|
||||
if (target == nullptr && m_capturedArea == nullptr && pressed && m_hasPointerPosition) {
|
||||
updateHover(x, y, m_lastSerial);
|
||||
target = m_hoveredArea;
|
||||
target = inputAreaAcceptingButton(m_hoveredArea, button);
|
||||
}
|
||||
|
||||
if (target != nullptr) {
|
||||
@@ -92,6 +102,7 @@ bool InputDispatcher::pointerButton(float x, float y, std::uint32_t button, bool
|
||||
target->dispatchPress(localX, localY, button, pressed);
|
||||
if (pressed) {
|
||||
m_capturedArea = target;
|
||||
trackArea(target);
|
||||
if (target->focusable()) {
|
||||
setFocus(target);
|
||||
}
|
||||
|
||||
@@ -45,6 +45,40 @@ namespace {
|
||||
return localX >= 0.0f && localX < node->width() && localY >= 0.0f && localY < node->height();
|
||||
}
|
||||
|
||||
Widget* widgetAtPoint(const std::vector<std::unique_ptr<Widget>>& widgets, float sceneX, float sceneY) {
|
||||
for (auto it = widgets.rbegin(); it != widgets.rend(); ++it) {
|
||||
auto* widget = it->get();
|
||||
if (widget == nullptr || widget->root() == nullptr || !widget->root()->visible()) {
|
||||
continue;
|
||||
}
|
||||
if (pointInsideNode(widget->root(), sceneX, sceneY)) {
|
||||
return widget;
|
||||
}
|
||||
}
|
||||
for (auto it = widgets.rbegin(); it != widgets.rend(); ++it) {
|
||||
auto* widget = it->get();
|
||||
auto* root = widget != nullptr ? widget->root() : nullptr;
|
||||
auto* bounds = widget != nullptr ? widget->layoutBoundsNode() : nullptr;
|
||||
if (root == nullptr || bounds == nullptr || bounds == root || root->parent() != bounds || !bounds->visible()) {
|
||||
continue;
|
||||
}
|
||||
if (pointInsideNode(bounds, sceneX, sceneY)) {
|
||||
return widget;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Widget* widgetAtPoint(const BarInstance& instance, float sceneX, float sceneY) {
|
||||
if (auto* widget = widgetAtPoint(instance.endWidgets, sceneX, sceneY); widget != nullptr) {
|
||||
return widget;
|
||||
}
|
||||
if (auto* widget = widgetAtPoint(instance.centerWidgets, sceneX, sceneY); widget != nullptr) {
|
||||
return widget;
|
||||
}
|
||||
return widgetAtPoint(instance.startWidgets, sceneX, sceneY);
|
||||
}
|
||||
|
||||
std::pair<float, float> surfaceOriginForOutputLocal(const BarInstance& instance, const WaylandOutput& outputInfo) {
|
||||
if (instance.surface == nullptr) {
|
||||
return {0.0f, 0.0f};
|
||||
@@ -647,6 +681,10 @@ void Bar::setAutoHideSuppressionCallback(std::function<bool()> callback) {
|
||||
m_autoHideSuppressionCallback = std::move(callback);
|
||||
}
|
||||
|
||||
void Bar::setOpenWidgetSettingsCallback(std::function<void(std::string, std::string)> callback) {
|
||||
m_openWidgetSettingsCallback = std::move(callback);
|
||||
}
|
||||
|
||||
bool Bar::isRunning() const noexcept {
|
||||
if (m_forceHidden) {
|
||||
return true; // hidden but still alive — do not exit the main loop
|
||||
@@ -984,6 +1022,7 @@ void Bar::populateWidgets(BarInstance& instance) {
|
||||
auto widget =
|
||||
m_widgetFactory->create(name, instance.output, instance.barConfig.scale, instance.barConfig.position);
|
||||
if (widget != nullptr) {
|
||||
widget->setConfigName(name);
|
||||
const WidgetConfig* wcPtr = nullptr;
|
||||
if (auto it = widgetConfigs.find(name); it != widgetConfigs.end()) {
|
||||
wcPtr = &it->second;
|
||||
@@ -1674,6 +1713,15 @@ bool Bar::onPointerEvent(const PointerEvent& event) {
|
||||
}
|
||||
}
|
||||
|
||||
if (targetInstance != nullptr && event.type == PointerEvent::Type::Button && event.button == BTN_MIDDLE &&
|
||||
event.state == 1 && m_config != nullptr && m_config->config().shell.middleClickOpensWidgetSettings) {
|
||||
auto* widget = widgetAtPoint(*targetInstance, static_cast<float>(event.sx), static_cast<float>(event.sy));
|
||||
if (widget != nullptr && !widget->configName().empty() && m_openWidgetSettingsCallback) {
|
||||
m_openWidgetSettingsCallback(targetInstance->barConfig.name, std::string(widget->configName()));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetInstance != nullptr && targetInstance->attachedPopupCount > 0) {
|
||||
switch (event.type) {
|
||||
case PointerEvent::Type::Enter:
|
||||
|
||||
@@ -63,6 +63,7 @@ public:
|
||||
void refresh();
|
||||
void requestLayout();
|
||||
void setAutoHideSuppressionCallback(std::function<bool()> callback);
|
||||
void setOpenWidgetSettingsCallback(std::function<void(std::string, std::string)> callback);
|
||||
// Requests a redraw on every bar surface without re-running widget update/layout.
|
||||
// Intended for reactive restyling (palette changes) where the scene graph has
|
||||
// already been mutated in place and only a repaint is needed.
|
||||
@@ -139,4 +140,5 @@ private:
|
||||
std::unordered_map<wl_surface*, BarInstance*> m_surfaceMap;
|
||||
BarInstance* m_hoveredInstance = nullptr;
|
||||
std::function<bool()> m_autoHideSuppressionCallback;
|
||||
std::function<void(std::string, std::string)> m_openWidgetSettingsCallback;
|
||||
};
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
class AnimationManager;
|
||||
class Box;
|
||||
@@ -54,6 +56,8 @@ public:
|
||||
void setPanelToggleCallback(PanelToggleCallback callback);
|
||||
void setContentScale(float scale) noexcept { m_contentScale = scale; }
|
||||
[[nodiscard]] float contentScale() const noexcept { return m_contentScale; }
|
||||
void setConfigName(std::string name) { m_configName = std::move(name); }
|
||||
[[nodiscard]] std::string_view configName() const noexcept { return m_configName; }
|
||||
void setAnchor(bool anchor) noexcept { m_anchor = anchor; }
|
||||
[[nodiscard]] bool isAnchor() const noexcept { return m_anchor; }
|
||||
|
||||
@@ -86,6 +90,7 @@ protected:
|
||||
virtual void doUpdate(Renderer& renderer) { (void)renderer; }
|
||||
|
||||
float m_contentScale = 1.0f;
|
||||
std::string m_configName;
|
||||
bool m_anchor = false;
|
||||
AnimationManager* m_animations = nullptr;
|
||||
UpdateCallback m_updateCallback;
|
||||
|
||||
@@ -33,7 +33,7 @@ MediaWidget::MediaWidget(MprisService* mpris, HttpClient* httpClient, wl_output*
|
||||
|
||||
void MediaWidget::create() {
|
||||
auto area = std::make_unique<InputArea>();
|
||||
area->setAcceptedButtons(BTN_LEFT | BTN_RIGHT);
|
||||
area->setAcceptedButtons(InputArea::buttonMask({BTN_LEFT, BTN_RIGHT}));
|
||||
area->setOnEnter([this](const InputArea::PointerData&) {
|
||||
applyTitleScrollMode(m_label != nullptr && m_label->visible());
|
||||
this->requestUpdate();
|
||||
|
||||
@@ -25,7 +25,7 @@ NightLightWidget::NightLightWidget(NightLightManager* nightLight) : m_nightLight
|
||||
|
||||
void NightLightWidget::create() {
|
||||
auto area = std::make_unique<InputArea>();
|
||||
area->setAcceptedButtons(BTN_LEFT | BTN_RIGHT);
|
||||
area->setAcceptedButtons(InputArea::buttonMask({BTN_LEFT, BTN_RIGHT}));
|
||||
area->setOnClick([this](const InputArea::PointerData& data) {
|
||||
if (m_nightLight == nullptr) {
|
||||
return;
|
||||
|
||||
@@ -20,6 +20,7 @@ NotificationWidget::NotificationWidget(NotificationManager* manager, wl_output*
|
||||
|
||||
void NotificationWidget::create() {
|
||||
auto area = std::make_unique<InputArea>();
|
||||
area->setAcceptedButtons(InputArea::buttonMask({BTN_LEFT, BTN_RIGHT}));
|
||||
area->setOnClick([this](const InputArea::PointerData& data) {
|
||||
if (data.button == BTN_RIGHT) {
|
||||
if (m_manager != nullptr) {
|
||||
|
||||
@@ -61,7 +61,7 @@ void ScriptedWidget::create() {
|
||||
m_host = std::make_unique<LuauHost>();
|
||||
|
||||
auto area = std::make_unique<InputArea>();
|
||||
area->setAcceptedButtons(BTN_LEFT | BTN_RIGHT | BTN_MIDDLE);
|
||||
area->setAcceptedButtons(InputArea::buttonMask({BTN_LEFT, BTN_RIGHT, BTN_MIDDLE}));
|
||||
area->setCursorShape(WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_POINTER);
|
||||
area->setOnClick([this](const InputArea::PointerData& data) {
|
||||
if (!m_host)
|
||||
|
||||
@@ -162,7 +162,7 @@ void TaskbarWidget::buildTaskButtons(Renderer& renderer) {
|
||||
auto createTaskTile = [&](const TaskModel& task) {
|
||||
auto area = std::make_unique<InputArea>();
|
||||
area->setFrameSize(tileSize, tileSize);
|
||||
area->setAcceptedButtons(BTN_LEFT | BTN_RIGHT);
|
||||
area->setAcceptedButtons(InputArea::buttonMask({BTN_LEFT, BTN_RIGHT}));
|
||||
area->setOnAxisHandler(workspaceAxisHandler);
|
||||
|
||||
const WorkspaceModel* taskWorkspace = nullptr;
|
||||
@@ -276,7 +276,7 @@ void TaskbarWidget::buildTaskButtons(Renderer& renderer) {
|
||||
auto switcher = std::make_unique<InputArea>();
|
||||
switcher->setFrameSize(groupWidth, groupHeight);
|
||||
switcher->setPosition(0.0f, 0.0f);
|
||||
switcher->setAcceptedButtons(BTN_LEFT);
|
||||
switcher->setAcceptedButtons(InputArea::buttonMask(BTN_LEFT));
|
||||
switcher->setOnAxisHandler(workspaceAxisHandler);
|
||||
auto wsCopy = ws.workspace;
|
||||
switcher->setOnClick([this, wsCopy](const InputArea::PointerData& data) {
|
||||
@@ -303,7 +303,7 @@ void TaskbarWidget::buildTaskButtons(Renderer& renderer) {
|
||||
auto badgeHit = std::make_unique<InputArea>();
|
||||
badgeHit->setFrameSize(badgeWidth, badgeBase);
|
||||
badgeHit->setPosition(badgeLeft, badgeTop);
|
||||
badgeHit->setAcceptedButtons(BTN_LEFT);
|
||||
badgeHit->setAcceptedButtons(InputArea::buttonMask(BTN_LEFT));
|
||||
badgeHit->setOnAxisHandler(workspaceAxisHandler);
|
||||
auto wsForBadge = ws.workspace;
|
||||
badgeHit->setOnClick([this, wsForBadge](const InputArea::PointerData& data) {
|
||||
|
||||
@@ -616,6 +616,7 @@ void TrayWidget::rebuild(Renderer& renderer) {
|
||||
area->setSize(itemSize, itemSize);
|
||||
iconNode->setPosition(std::round((itemSize - iconW) * 0.5f), std::round((itemSize - iconH) * 0.5f));
|
||||
auto itemId = item.id;
|
||||
area->setAcceptedButtons(InputArea::buttonMask({BTN_LEFT, BTN_RIGHT}));
|
||||
area->setOnClick([this, itemId](const InputArea::PointerData& data) {
|
||||
if (m_tray == nullptr) {
|
||||
return;
|
||||
@@ -627,8 +628,6 @@ void TrayWidget::rebuild(Renderer& renderer) {
|
||||
}
|
||||
} else if (data.button == BTN_RIGHT) {
|
||||
m_tray->requestMenuToggle(itemId);
|
||||
} else if (data.button == BTN_MIDDLE) {
|
||||
(void)m_tray->openContextMenu(itemId);
|
||||
}
|
||||
});
|
||||
area->addChild(std::move(iconNode));
|
||||
|
||||
@@ -1068,7 +1068,7 @@ void Dock::rebuildItems(DockInstance& instance) {
|
||||
instPtr->sceneRoot->markPaintDirty();
|
||||
}
|
||||
});
|
||||
areaNode->setAcceptedButtons(BTN_LEFT | BTN_RIGHT);
|
||||
areaNode->setAcceptedButtons(InputArea::buttonMask({BTN_LEFT, BTN_RIGHT}));
|
||||
areaNode->setOnClick([itemPtr, instPtr, this](const InputArea::PointerData& d) {
|
||||
if (d.button == BTN_LEFT) {
|
||||
handleItemClick(*instPtr, *itemPtr);
|
||||
|
||||
@@ -1484,7 +1484,7 @@ InputArea* NotificationToast::buildCard(const PopupEntry& entry, Node** outCardC
|
||||
auto viewport = std::make_unique<InputArea>();
|
||||
viewport->setSize(kCardWidth, cardHeight);
|
||||
viewport->setClipChildren(true);
|
||||
viewport->setAcceptedButtons(BTN_LEFT | BTN_RIGHT);
|
||||
viewport->setAcceptedButtons(InputArea::buttonMask({BTN_LEFT, BTN_RIGHT}));
|
||||
// Right-clicking anywhere dismisses the card, while the visual (X) keeps its
|
||||
// familiar left-click close affordance without adding a nested hover target.
|
||||
viewport->setOnClick([this, id = entry.notificationId](const InputArea::PointerData& data) {
|
||||
|
||||
@@ -1040,6 +1040,9 @@ namespace settings {
|
||||
const bool hasEdit = !ctx.editingWidgetName.empty();
|
||||
|
||||
auto inspector = std::make_unique<Flex>();
|
||||
if (ctx.setScrollTarget) {
|
||||
ctx.setScrollTarget(inspector.get());
|
||||
}
|
||||
inspector->setDirection(FlexDirection::Vertical);
|
||||
inspector->setAlign(FlexAlign::Stretch);
|
||||
inspector->setGap(Style::spaceSm * ctx.scale);
|
||||
|
||||
@@ -30,6 +30,7 @@ namespace settings {
|
||||
|
||||
std::function<void()> requestRebuild;
|
||||
std::function<void()> resetContentScroll;
|
||||
std::function<void(Node*)> setScrollTarget;
|
||||
std::function<void(InputArea*)> focusArea;
|
||||
std::function<void(const std::vector<std::string>&, Button*)> openWidgetAddPopup;
|
||||
std::function<void(std::vector<std::string>, ConfigOverrideValue)> setOverride;
|
||||
|
||||
@@ -1234,6 +1234,7 @@ namespace settings {
|
||||
.creatingWidgetType = ctx.creatingWidgetType,
|
||||
.requestRebuild = ctx.requestRebuild,
|
||||
.resetContentScroll = ctx.resetContentScroll,
|
||||
.setScrollTarget = ctx.setScrollTarget,
|
||||
.focusArea = ctx.focusArea,
|
||||
.openWidgetAddPopup = ctx.openBarWidgetAddPopup,
|
||||
.setOverride = ctx.setOverride,
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
class Flex;
|
||||
class InputArea;
|
||||
class Button;
|
||||
class Node;
|
||||
|
||||
namespace settings {
|
||||
|
||||
@@ -38,6 +39,7 @@ namespace settings {
|
||||
std::function<void()> requestRebuild;
|
||||
std::function<void()> requestContentRebuild;
|
||||
std::function<void()> resetContentScroll;
|
||||
std::function<void(Node*)> setScrollTarget;
|
||||
std::function<void(InputArea*)> focusArea;
|
||||
std::function<void(const std::vector<std::string>&, Button*)> openBarWidgetAddPopup;
|
||||
std::function<void(std::vector<std::string>, ConfigOverrideValue)> setOverride;
|
||||
|
||||
@@ -588,6 +588,11 @@ namespace settings {
|
||||
tr("settings.schema.shell.password-style.description"), {"shell", "password_style"},
|
||||
asSegmented(enumSelect(kPasswordMaskStyles, cfg.shell.passwordMaskStyle)),
|
||||
"polkit lock mask"));
|
||||
entries.push_back(makeEntry(
|
||||
"shell", "behavior", tr("settings.schema.shell.middle-click-opens-widget-settings.label"),
|
||||
tr("settings.schema.shell.middle-click-opens-widget-settings.description"),
|
||||
{"shell", "middle_click_opens_widget_settings"}, ToggleSetting{cfg.shell.middleClickOpensWidgetSettings},
|
||||
"bar widget settings middle click configure"));
|
||||
entries.push_back(makeEntry("shell", "location", tr("settings.schema.shell.show-location.label"),
|
||||
tr("settings.schema.shell.show-location.description"), {"shell", "show_location"},
|
||||
ToggleSetting{cfg.shell.showLocation}, "weather"));
|
||||
|
||||
@@ -254,6 +254,26 @@ void SettingsWindow::open() {
|
||||
m_lastSceneHeight = 0;
|
||||
}
|
||||
|
||||
void SettingsWindow::openToBarWidget(std::string barName, std::string widgetName) {
|
||||
clearTransientSettingsState();
|
||||
clearStatusMessage();
|
||||
m_searchQuery.clear();
|
||||
m_selectedSection = "bar";
|
||||
m_selectedBarName = std::move(barName);
|
||||
m_selectedMonitorOverride.clear();
|
||||
m_editingWidgetName = std::move(widgetName);
|
||||
m_contentScrollState.offset = 0.0f;
|
||||
m_scrollToPendingContentTarget = true;
|
||||
m_pendingContentScrollTarget = nullptr;
|
||||
m_sidebarScrollState.offset = 0.0f;
|
||||
|
||||
const bool wasOpen = isOpen();
|
||||
open();
|
||||
if (wasOpen && isOpen()) {
|
||||
requestSceneRebuild();
|
||||
}
|
||||
}
|
||||
|
||||
void SettingsWindow::close() {
|
||||
if (!isOpen()) {
|
||||
return;
|
||||
@@ -268,6 +288,7 @@ void SettingsWindow::destroyWindow() {
|
||||
}
|
||||
m_mainContainer = nullptr;
|
||||
m_contentContainer = nullptr;
|
||||
m_contentScrollView = nullptr;
|
||||
m_actionsMenuButton = nullptr;
|
||||
if (m_actionsMenuPopup != nullptr) {
|
||||
m_actionsMenuPopup->close();
|
||||
@@ -287,6 +308,8 @@ void SettingsWindow::destroyWindow() {
|
||||
m_rebuildRequested = false;
|
||||
m_contentRebuildRequested = false;
|
||||
m_focusSearchOnRebuild = false;
|
||||
m_scrollToPendingContentTarget = false;
|
||||
m_pendingContentScrollTarget = nullptr;
|
||||
m_statusMessage.clear();
|
||||
m_statusIsError = false;
|
||||
m_creatingBarName.clear();
|
||||
@@ -347,6 +370,7 @@ void SettingsWindow::prepareFrame(bool /*needsUpdate*/, bool needsLayout) {
|
||||
m_contentRebuildRequested = false;
|
||||
}
|
||||
m_sceneRoot->layout(*m_renderContext);
|
||||
applyPendingContentScrollTarget(Style::spaceMd * uiScale());
|
||||
m_lastSceneWidth = width;
|
||||
m_lastSceneHeight = height;
|
||||
}
|
||||
@@ -377,6 +401,57 @@ void SettingsWindow::requestContentRebuild() {
|
||||
});
|
||||
}
|
||||
|
||||
void SettingsWindow::applyPendingContentScrollTarget(float margin) {
|
||||
if (!m_scrollToPendingContentTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto clearPending = [this]() {
|
||||
m_scrollToPendingContentTarget = false;
|
||||
m_pendingContentScrollTarget = nullptr;
|
||||
};
|
||||
|
||||
if (m_contentScrollView == nullptr || m_contentScrollView->content() == nullptr ||
|
||||
m_pendingContentScrollTarget == nullptr) {
|
||||
clearPending();
|
||||
return;
|
||||
}
|
||||
|
||||
const float viewportHeight =
|
||||
std::max(0.0f, m_contentScrollView->height() - m_contentScrollView->viewportPaddingV() * 2.0f);
|
||||
if (viewportHeight <= 0.0f) {
|
||||
clearPending();
|
||||
return;
|
||||
}
|
||||
|
||||
float targetX = 0.0f;
|
||||
float targetY = 0.0f;
|
||||
float contentX = 0.0f;
|
||||
float contentY = 0.0f;
|
||||
Node::absolutePosition(m_pendingContentScrollTarget, targetX, targetY);
|
||||
Node::absolutePosition(m_contentScrollView->content(), contentX, contentY);
|
||||
(void)targetX;
|
||||
(void)contentX;
|
||||
|
||||
const float targetTop = std::max(0.0f, targetY - contentY - margin);
|
||||
const float targetBottom = targetY - contentY + m_pendingContentScrollTarget->height() + margin;
|
||||
const float currentTop = m_contentScrollView->scrollOffset();
|
||||
const float currentBottom = currentTop + viewportHeight;
|
||||
|
||||
float desiredOffset = currentTop;
|
||||
if (targetBottom - targetTop >= viewportHeight) {
|
||||
desiredOffset = targetTop;
|
||||
} else if (targetTop < currentTop) {
|
||||
desiredOffset = targetTop;
|
||||
} else if (targetBottom > currentBottom) {
|
||||
desiredOffset = targetBottom - viewportHeight;
|
||||
}
|
||||
|
||||
m_contentScrollView->setScrollOffset(desiredOffset);
|
||||
m_contentScrollState.offset = m_contentScrollView->scrollOffset();
|
||||
clearPending();
|
||||
}
|
||||
|
||||
void SettingsWindow::clearStatusMessage() {
|
||||
m_statusMessage.clear();
|
||||
m_statusIsError = false;
|
||||
@@ -945,6 +1020,7 @@ void SettingsWindow::rebuildSettingsContent() {
|
||||
return;
|
||||
}
|
||||
|
||||
m_pendingContentScrollTarget = nullptr;
|
||||
while (!m_contentContainer->children().empty()) {
|
||||
m_contentContainer->removeChild(m_contentContainer->children().back().get());
|
||||
}
|
||||
@@ -1030,6 +1106,7 @@ void SettingsWindow::rebuildSettingsContent() {
|
||||
.requestRebuild = requestRebuild,
|
||||
.requestContentRebuild = requestContent,
|
||||
.resetContentScroll = [this]() { m_contentScrollState.offset = 0.0f; },
|
||||
.setScrollTarget = [this](Node* target) { m_pendingContentScrollTarget = target; },
|
||||
.focusArea = [this](InputArea* area) { m_inputDispatcher.setFocus(area); },
|
||||
.openBarWidgetAddPopup = [this](const std::vector<std::string>& lanePath,
|
||||
Button* anchorButton) { openBarWidgetAddPopup(lanePath, anchorButton); },
|
||||
@@ -1050,6 +1127,7 @@ void SettingsWindow::buildScene(std::uint32_t width, std::uint32_t height) {
|
||||
const float h = static_cast<float>(height);
|
||||
const float scale = uiScale();
|
||||
m_actionsMenuButton = nullptr;
|
||||
m_contentScrollView = nullptr;
|
||||
const Config fallbackCfg{};
|
||||
const Config& cfg = m_config != nullptr ? m_config->config() : fallbackCfg;
|
||||
const auto availableBars = settings::barNames(cfg);
|
||||
@@ -1396,6 +1474,7 @@ void SettingsWindow::buildScene(std::uint32_t width, std::uint32_t height) {
|
||||
scroll->setViewportPaddingV(Style::spaceSm * scale);
|
||||
scroll->clearFill();
|
||||
scroll->clearBorder();
|
||||
m_contentScrollView = scroll.get();
|
||||
auto* content = scroll->content();
|
||||
m_contentContainer = content;
|
||||
content->setDirection(FlexDirection::Vertical);
|
||||
@@ -1415,6 +1494,7 @@ void SettingsWindow::buildScene(std::uint32_t width, std::uint32_t height) {
|
||||
|
||||
main->setSize(w, h);
|
||||
main->layout(*m_renderContext);
|
||||
applyPendingContentScrollTarget(Style::spaceMd * scale);
|
||||
m_mainContainer = static_cast<Flex*>(m_sceneRoot->addChild(std::move(main)));
|
||||
|
||||
m_inputDispatcher.setSceneRoot(m_sceneRoot.get());
|
||||
|
||||
@@ -36,6 +36,7 @@ public:
|
||||
DependencyService* dependencies);
|
||||
|
||||
void open();
|
||||
void openToBarWidget(std::string barName, std::string widgetName);
|
||||
void close();
|
||||
[[nodiscard]] bool isOpen() const noexcept { return m_surface != nullptr && m_surface->isRunning(); }
|
||||
[[nodiscard]] wl_surface* wlSurface() const noexcept {
|
||||
@@ -57,6 +58,7 @@ private:
|
||||
void rebuildSettingsContent();
|
||||
void requestSceneRebuild();
|
||||
void requestContentRebuild();
|
||||
void applyPendingContentScrollTarget(float margin);
|
||||
void clearStatusMessage();
|
||||
void clearTransientSettingsState();
|
||||
void openActionsMenu();
|
||||
@@ -89,6 +91,7 @@ private:
|
||||
Box* m_panelBackground = nullptr; // Window-sized background panel inside m_sceneRoot
|
||||
Button* m_actionsMenuButton = nullptr;
|
||||
Flex* m_contentContainer = nullptr;
|
||||
ScrollView* m_contentScrollView = nullptr;
|
||||
std::unique_ptr<ContextMenuPopup> m_actionsMenuPopup;
|
||||
std::unique_ptr<settings::WidgetAddPopup> m_widgetAddPopup;
|
||||
InputDispatcher m_inputDispatcher;
|
||||
@@ -104,6 +107,8 @@ private:
|
||||
bool m_rebuildRequested = false;
|
||||
bool m_contentRebuildRequested = false;
|
||||
bool m_focusSearchOnRebuild = false;
|
||||
bool m_scrollToPendingContentTarget = false;
|
||||
Node* m_pendingContentScrollTarget = nullptr;
|
||||
std::string m_searchQuery;
|
||||
std::string m_openWidgetPickerPath;
|
||||
std::string m_openSearchPickerPath;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
WallpaperTile::WallpaperTile(float cellWidth, float cellHeight, float contentScale)
|
||||
: m_cellWidth(cellWidth), m_cellHeight(cellHeight), m_contentScale(contentScale) {
|
||||
setAcceptedButtons(BTN_LEFT);
|
||||
setAcceptedButtons(InputArea::buttonMask(BTN_LEFT));
|
||||
setCursorShape(WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_POINTER);
|
||||
setOnClick([this](const InputArea::PointerData&) {
|
||||
if (m_hasEntry && m_onClick) {
|
||||
|
||||
@@ -168,7 +168,7 @@ Button::Button() {
|
||||
}
|
||||
if (data.button == BTN_RIGHT && m_onRightClick) {
|
||||
m_onRightClick();
|
||||
} else if (m_onClick) {
|
||||
} else if (data.button == BTN_LEFT && m_onClick) {
|
||||
m_onClick();
|
||||
}
|
||||
});
|
||||
@@ -228,7 +228,8 @@ void Button::setOnClick(std::function<void()> callback) {
|
||||
void Button::setOnRightClick(std::function<void()> callback) {
|
||||
m_onRightClick = std::move(callback);
|
||||
if (m_inputArea != nullptr) {
|
||||
m_inputArea->setAcceptedButtons(BTN_LEFT | BTN_RIGHT);
|
||||
m_inputArea->setAcceptedButtons(m_onRightClick ? InputArea::buttonMask({BTN_LEFT, BTN_RIGHT})
|
||||
: InputArea::buttonMask(BTN_LEFT));
|
||||
}
|
||||
refreshInputAreaEnabled();
|
||||
}
|
||||
@@ -329,7 +330,8 @@ void Button::refreshInputAreaEnabled() {
|
||||
if (m_inputArea != nullptr) {
|
||||
m_inputArea->setEnabled(m_enabled && (static_cast<bool>(m_onClick) || static_cast<bool>(m_onMotion) ||
|
||||
static_cast<bool>(m_onPointerMotion) || static_cast<bool>(m_onPress) ||
|
||||
static_cast<bool>(m_onEnter) || static_cast<bool>(m_onLeave)));
|
||||
static_cast<bool>(m_onEnter) || static_cast<bool>(m_onLeave) ||
|
||||
static_cast<bool>(m_onRightClick)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user