Use applet geometry for panel scroll margins
This commit is contained in:
@@ -15,6 +15,11 @@ Other choices are screen brightness, keyboard backlight, virtual desktops, and
|
||||
normal widget behaviour. Actions use Plasma's existing shortcuts, including its
|
||||
usual volume/brightness steps and on-screen indicators.
|
||||
|
||||
For scrolling, applet areas extend through the panel's thickness, and the first
|
||||
and last visible items include the outer end padding. These areas follow the
|
||||
actual layout, without fixed pixel offsets. Interior gaps, explicit spacers,
|
||||
and unused Task Manager space remain empty. Click hit areas are unchanged.
|
||||
|
||||
Left and middle clicks, and double clicks, on empty space can toggle mute, show
|
||||
the desktop, open Overview, play/pause media, launch an installed application,
|
||||
or run a custom shell command as the current user. All default to normal
|
||||
|
||||
@@ -2,24 +2,28 @@
|
||||
.pragma library
|
||||
|
||||
// Task Manager fills unused panel space. Only delegates are task buttons.
|
||||
function hasTaskAt(item, x, y) {
|
||||
if (!item.visible || !item.contains(item.mapFromItem(null, x, y))) return false;
|
||||
function hasTaskAt(item, x, y, vertical) {
|
||||
if (!item.visible) return false;
|
||||
const point = item.mapFromItem(null, x, y);
|
||||
if (vertical === undefined) {
|
||||
if (!item.contains(point)) return false;
|
||||
} else if (item.width <= 0 || item.height <= 0
|
||||
|| (vertical ? point.y < 0 || point.y > item.height : point.x < 0 || point.x > item.width)) {
|
||||
return false;
|
||||
}
|
||||
if (item.tasksRoot !== undefined && item.isWindow !== undefined && item.index !== undefined) return true;
|
||||
for (const child of item.children) {
|
||||
if (hasTaskAt(child, x, y)) return true;
|
||||
if (hasTaskAt(child, x, y, vertical)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function areaAt(panelLayout, x, y) {
|
||||
if (!panelLayout) return "";
|
||||
for (const child of panelLayout.children) {
|
||||
if (!child.visible || !child.contains(child.mapFromItem(null, x, y))) continue;
|
||||
function appletArea(child, x, y, vertical) {
|
||||
const applet = child.applet;
|
||||
const name = applet?.plasmoid?.pluginName;
|
||||
if (!name) return "other";
|
||||
if (name === "org.kde.plasma.taskmanager" || name === "org.kde.plasma.icontasks") {
|
||||
return hasTaskAt(applet, x, y) ? "tasks" : "empty";
|
||||
return hasTaskAt(applet, x, y, vertical) ? "tasks" : "empty";
|
||||
}
|
||||
if (name === "se.ajpanton.panelactions" || name === "org.kde.plasma.panelspacer"
|
||||
|| name === "org.kde.plasma.marginsseparator") return "empty";
|
||||
@@ -29,5 +33,39 @@ function areaAt(panelLayout, x, y) {
|
||||
|| name === "org.kde.plasma.kickerdash") return "launcher";
|
||||
return "other";
|
||||
}
|
||||
|
||||
function areaAt(panelLayout, x, y) {
|
||||
if (!panelLayout) return "";
|
||||
for (const child of panelLayout.children) {
|
||||
if (child.visible && child.contains(child.mapFromItem(null, x, y))) {
|
||||
return appletArea(child, x, y);
|
||||
}
|
||||
}
|
||||
return "empty";
|
||||
}
|
||||
|
||||
// Scrolling owns the full panel thickness and its outer end padding. Interior
|
||||
// gaps and explicit spacers remain empty; clicks keep their exact hit testing.
|
||||
function scrollAreaAt(panelLayout, x, y, vertical) {
|
||||
if (!panelLayout) return "";
|
||||
const point = panelLayout.mapFromItem(null, x, y);
|
||||
const along = vertical ? point.y : point.x;
|
||||
let first = null;
|
||||
let last = null;
|
||||
for (const child of panelLayout.children) {
|
||||
if (!child.visible || child.width <= 0 || child.height <= 0) continue;
|
||||
const a = child.mapToItem(panelLayout, 0, 0);
|
||||
const b = child.mapToItem(panelLayout, child.width, child.height);
|
||||
const start = vertical ? Math.min(a.y, b.y) : Math.min(a.x, b.x);
|
||||
const end = vertical ? Math.max(a.y, b.y) : Math.max(a.x, b.x);
|
||||
if (end <= start) continue;
|
||||
if (along >= start && along < end) return appletArea(child, x, y, vertical);
|
||||
if (!first || start < first.start) first = {child, start};
|
||||
if (!last || end > last.end) last = {child, end};
|
||||
}
|
||||
const edge = first && along < first.start ? first : last && along >= last.end ? last : null;
|
||||
if (!edge) return "empty";
|
||||
const boundary = edge === first ? first.start : last.end;
|
||||
const projected = panelLayout.mapToItem(null, vertical ? point.x : boundary, vertical ? boundary : point.y);
|
||||
return appletArea(edge.child, projected.x, projected.y, vertical);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,9 @@ PlasmoidItem {
|
||||
PanelSettings { id: settings }
|
||||
PanelController {
|
||||
active: root.panelLayout !== null && !(Plasmoid.containment.corona?.editMode ?? false)
|
||||
locateArea: (x, y) => Areas.areaAt(root.panelLayout, x, y)
|
||||
locateArea: (x, y, scrolling) => scrolling
|
||||
? Areas.scrollAreaAt(root.panelLayout, x, y, Plasmoid.formFactor === PlasmaCore.Types.Vertical)
|
||||
: Areas.areaAt(root.panelLayout, x, y)
|
||||
scrollActions: ({
|
||||
empty: settings.values.emptyAction,
|
||||
tasks: settings.values.tasksAction,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Name: plasma-panel-actions
|
||||
Version: 0.1.0
|
||||
Release: 8%{?dist}
|
||||
Release: 9%{?dist}
|
||||
Summary: Configurable panel scrolling and empty-space click actions
|
||||
License: MIT
|
||||
URL: https://git.ajpanton.se/ajp_anton/fedora-tools
|
||||
@@ -47,6 +47,8 @@ install -Dpm 0644 %{SOURCE1} %{buildroot}%{_licensedir}/%{name}/LICENSE
|
||||
%{_sysconfdir}/xdg/autostart/se.ajpanton.plasma-panel-actions-autostart.desktop
|
||||
%{_datadir}/fedora-tools/settings/plasma-panel-actions.json
|
||||
%changelog
|
||||
* Wed Sep 09 2026 fedora-tools contributors - 0.1.0-9
|
||||
- Assign panel scroll margins using applet geometry without changing clicks
|
||||
* Mon Sep 07 2026 fedora-tools contributors - 0.1.0-8
|
||||
- Invalidate settings caches on UI changes with content-specific install paths
|
||||
* Mon Sep 07 2026 fedora-tools contributors - 0.1.0-7
|
||||
|
||||
@@ -50,12 +50,12 @@ PanelController::~PanelController()
|
||||
}
|
||||
}
|
||||
|
||||
QString PanelController::areaAt(const QPointF &position)
|
||||
QString PanelController::areaAt(const QPointF &position, bool scrolling)
|
||||
{
|
||||
if (!m_locateArea.isCallable()) {
|
||||
return {};
|
||||
}
|
||||
const QJSValue result = m_locateArea.call({position.x(), position.y()});
|
||||
const QJSValue result = m_locateArea.call({position.x(), position.y(), scrolling});
|
||||
if (result.isError()) {
|
||||
qWarning() << "Panel Actions: could not identify panel area:" << result.toString();
|
||||
return {};
|
||||
@@ -74,7 +74,7 @@ bool PanelController::eventFilter(QObject *object, QEvent *event)
|
||||
}
|
||||
if (event->type() == QEvent::Wheel) {
|
||||
auto *wheel = static_cast<QWheelEvent *>(event);
|
||||
const QString area = areaAt(wheel->position());
|
||||
const QString area = areaAt(wheel->position(), true);
|
||||
const QString action = m_scrollActions.value(area, QStringLiteral("normal")).toString();
|
||||
if (action == QLatin1String("normal") || action.isEmpty()) {
|
||||
m_remainder = 0;
|
||||
|
||||
@@ -27,7 +27,7 @@ Q_SIGNALS:
|
||||
protected:
|
||||
bool eventFilter(QObject *object, QEvent *event) override;
|
||||
private:
|
||||
QString areaAt(const QPointF &position);
|
||||
QString areaAt(const QPointF &position, bool scrolling = false);
|
||||
QPointer<QQuickWindow> m_panel;
|
||||
bool m_active = false;
|
||||
QJSValue m_locateArea;
|
||||
|
||||
@@ -52,6 +52,25 @@ private:
|
||||
QCoreApplication::sendEvent(&window, &event);
|
||||
}
|
||||
private Q_SLOTS:
|
||||
void scrollHitTestingDoesNotExpandClickTargets()
|
||||
{
|
||||
QJSEngine engine;
|
||||
PanelWindow window;
|
||||
PanelController controller;
|
||||
setup(controller, window, engine);
|
||||
controller.setLocateArea(engine.evaluate(u"(function(x,y,scrolling) { return scrolling ? 'launcher' : 'empty'; })"_s));
|
||||
controller.setProperty("scrollActions", QVariantMap{{u"launcher"_s, u"brightness"_s}, {u"empty"_s, u"volume"_s}});
|
||||
controller.setProperty("clickActions", QVariantMap{{u"leftClick"_s, u"mute"_s}});
|
||||
QSignalSpy scrolling(&controller, &PanelController::actionRequested);
|
||||
QSignalSpy clicking(&controller, &PanelController::clickRequested);
|
||||
wheel(window, 120);
|
||||
QCOMPARE(scrolling.count(), 1);
|
||||
QCOMPARE(scrolling.first(), QVariantList({u"brightness"_s, 1}));
|
||||
mouse(window, QEvent::MouseButtonPress, 20);
|
||||
mouse(window, QEvent::MouseButtonRelease, 20);
|
||||
QCOMPARE(clicking.count(), 1);
|
||||
QCOMPARE(clicking.first(), QVariantList({u"leftClick"_s}));
|
||||
}
|
||||
void wheelOverridesAndNormalBehaviour()
|
||||
{
|
||||
QJSEngine engine;
|
||||
|
||||
@@ -48,9 +48,98 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
Item {
|
||||
id: edges
|
||||
x: 40
|
||||
y: 10
|
||||
property bool vertical: false
|
||||
property bool reversed: false
|
||||
width: vertical ? 60 : 400
|
||||
height: vertical ? 400 : 60
|
||||
Item { width: 0; height: 60 } // invisible controller's zero-width slot
|
||||
Item { x: -100; y: -100; width: 50; height: 50; visible: false }
|
||||
Item {
|
||||
id: launcher
|
||||
x: edges.vertical ? 8 : edges.reversed ? 350 : 12
|
||||
y: edges.vertical ? (edges.reversed ? 350 : 12) : 8
|
||||
width: edges.vertical ? 44 : 36
|
||||
height: edges.vertical ? 36 : 44
|
||||
property QtObject applet: QtObject {
|
||||
property QtObject plasmoid: QtObject { property string pluginName: "org.kde.plasma.kickoff" }
|
||||
}
|
||||
}
|
||||
Item {
|
||||
x: edges.vertical ? 8 : 90
|
||||
y: edges.vertical ? 90 : 8
|
||||
width: edges.vertical ? 44 : 80
|
||||
height: edges.vertical ? 80 : 44
|
||||
property QtObject applet: QtObject {
|
||||
property QtObject plasmoid: QtObject { property string pluginName: "org.kde.plasma.panelspacer" }
|
||||
}
|
||||
}
|
||||
Item {
|
||||
id: clock
|
||||
x: edges.vertical ? 8 : edges.reversed ? 12 : 350
|
||||
y: edges.vertical ? (edges.reversed ? 12 : 350) : 8
|
||||
width: edges.vertical ? 44 : 36
|
||||
height: edges.vertical ? 36 : 44
|
||||
property QtObject applet: QtObject {
|
||||
property QtObject plasmoid: QtObject { property string pluginName: "org.kde.plasma.digitalclock" }
|
||||
}
|
||||
}
|
||||
}
|
||||
TestCase {
|
||||
name: "PanelAreas"
|
||||
when: windowShown
|
||||
function init() {
|
||||
edges.vertical = false;
|
||||
edges.reversed = false;
|
||||
edges.scale = 1;
|
||||
launcher.visible = true;
|
||||
}
|
||||
function scrollAt(along, across) {
|
||||
const p = edges.mapToItem(null, edges.vertical ? across : along, edges.vertical ? along : across);
|
||||
return Areas.scrollAreaAt(edges, p.x, p.y, edges.vertical);
|
||||
}
|
||||
function test_scrollMargins_data() {
|
||||
return [
|
||||
{tag: "horizontal", vertical: false, scale: 1},
|
||||
{tag: "vertical", vertical: true, scale: 1},
|
||||
{tag: "scaled", vertical: false, scale: 1.5},
|
||||
{tag: "vertical-scaled", vertical: true, scale: 1.5}
|
||||
];
|
||||
}
|
||||
function test_scrollMargins(data) {
|
||||
edges.vertical = data.vertical;
|
||||
edges.scale = data.scale;
|
||||
compare(scrollAt(-20, 58), "launcher"); // leading outer corner
|
||||
compare(scrollAt(30, 2), "launcher"); // thickness padding above/beside icon
|
||||
compare(scrollAt(420, 58), "clock"); // trailing outer corner
|
||||
compare(scrollAt(65, 58), "empty"); // internal gap
|
||||
compare(scrollAt(120, 58), "empty"); // explicit spacer
|
||||
compare(scrollAt(200, 58), "empty");
|
||||
edges.reversed = true; // physical order, not QObject child order
|
||||
compare(scrollAt(-20, 58), "clock");
|
||||
compare(scrollAt(420, 58), "launcher");
|
||||
}
|
||||
function test_clickMarginsStayEmpty() {
|
||||
const p = edges.mapToItem(null, -20, 58);
|
||||
compare(Areas.areaAt(edges, p.x, p.y), "empty");
|
||||
compare(Areas.scrollAreaAt(edges, p.x, p.y, false), "launcher");
|
||||
const inside = launcher.mapToItem(null, 10, 10);
|
||||
compare(Areas.areaAt(edges, inside.x, inside.y), "launcher");
|
||||
}
|
||||
function test_hiddenAndZeroSizeDoNotOwnPadding() {
|
||||
launcher.visible = false;
|
||||
compare(scrollAt(-20, 58), "empty"); // spacer becomes first
|
||||
compare(Areas.scrollAreaAt(null, 0, 0, false), "");
|
||||
}
|
||||
function test_taskThicknessPaddingKeepsUnusedSpaceEmpty() {
|
||||
compare(Areas.scrollAreaAt(panel, 40, 65, false), "tasks");
|
||||
compare(Areas.scrollAreaAt(panel, 150, 65, false), "empty");
|
||||
compare(Areas.areaAt(panel, 40, 65), "empty");
|
||||
compare(Areas.scrollAreaAt(panel, 0, 65, false), "tasks");
|
||||
}
|
||||
function test_taskButtonsVersusUnusedSpace() {
|
||||
compare(Areas.areaAt(panel, 40, 30), "tasks");
|
||||
compare(Areas.areaAt(panel, 150, 30), "empty");
|
||||
|
||||
Reference in New Issue
Block a user