feat: add D-pad fine-tune nudges below the position selector
Add unit-by-unit manual position control via a four-button D-pad (left, up/down together, right) under the position dropdown. Nudges persist as offsetX/offsetY in the shared config and are silently clamped to the screen edges, so a move that would cross a border is ignored. Selecting a preset position resets the manual offset. While the display is hidden, a nudge flashes a directional arrow preview so the move direction is visible even with no keys on screen. Co-Authored-By: opencode <opencode@anthropic.com>
This commit is contained in:
+110
-33
@@ -39,6 +39,10 @@ Item {
|
|||||||
// show": entries never expire, so the stack only shrinks when newer
|
// show": entries never expire, so the stack only shrinks when newer
|
||||||
// combos push old ones out of the history window.
|
// combos push old ones out of the history window.
|
||||||
property int lingerMs: 1000
|
property int lingerMs: 1000
|
||||||
|
// Manual fine-tune offsets (px) applied on top of the preset position.
|
||||||
|
// Written by the panel D-pad buttons; reset when a dropdown preset is chosen.
|
||||||
|
property int offsetX: 0
|
||||||
|
property int offsetY: 0
|
||||||
|
|
||||||
// Combo mode — "game mode". Toggle in the panel. Chords that contain a
|
// Combo mode — "game mode". Toggle in the panel. Chords that contain a
|
||||||
// modifier (Super/Ctrl/Alt/Shift/Menu/AltGr) are COMBOs: they build a
|
// modifier (Super/Ctrl/Alt/Shift/Menu/AltGr) are COMBOs: they build a
|
||||||
@@ -136,6 +140,73 @@ Item {
|
|||||||
return root.entries.length * root.chipHeight + (root.entries.length - 1) * root.entryGap
|
return root.entries.length * root.chipHeight + (root.entries.length - 1) * root.entryGap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- geometry helpers for card/banner group clamp ----
|
||||||
|
|
||||||
|
function clamp(v, lo, hi) {
|
||||||
|
return Math.max(lo, Math.min(hi, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupWidth() {
|
||||||
|
return card.width
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupHeight() {
|
||||||
|
if (root.bannerVisible()) return card.height + root.bannerHeight + root.bannerGap
|
||||||
|
return card.height
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupLeftOffset() {
|
||||||
|
if (!root.bannerVisible()) return 0
|
||||||
|
var mode = root.sideMode()
|
||||||
|
if (mode === "left") return 0
|
||||||
|
if (mode === "right") return Math.min(0, card.width - banner.width)
|
||||||
|
return Math.min(0, (card.width - banner.width) / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupRightOffset() {
|
||||||
|
if (!root.bannerVisible()) return card.width
|
||||||
|
var mode = root.sideMode()
|
||||||
|
if (mode === "left") return Math.max(card.width, banner.width)
|
||||||
|
if (mode === "right") return card.width
|
||||||
|
return Math.max(card.width, (card.width + banner.width) / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the y of the topmost element of the visual group (banner for
|
||||||
|
// top positions, card for bottom), clamped so the whole group stays on screen.
|
||||||
|
function groupY0() {
|
||||||
|
var gH = root.groupHeight()
|
||||||
|
var top = root.position.indexOf("top") !== -1
|
||||||
|
var raw = top ? root.margin + root.offsetY : panel.height - gH - root.margin + root.offsetY
|
||||||
|
return root.clamp(raw, 0, panel.height - gH)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combo-banner horizontal anchor, computed from the card's *target* position
|
||||||
|
// (preset + offset, pre-clamp) so it never feeds back into the clamp. The
|
||||||
|
// banner anchors to the card's outward edge and grows toward the screen
|
||||||
|
// center: "left" grows right, "right" grows left, "center" stays centered
|
||||||
|
// (the look for centered presets, e.g. bottom-center by default).
|
||||||
|
function sideMode() {
|
||||||
|
if (!root.bannerVisible()) return "center"
|
||||||
|
var base = 0
|
||||||
|
var p = root.position
|
||||||
|
if (p.indexOf("left") !== -1) base = root.margin
|
||||||
|
else if (p.indexOf("right") !== -1) base = panel.width - root.groupWidth() - root.margin
|
||||||
|
else base = Math.round((panel.width - root.groupWidth()) / 2)
|
||||||
|
var target = base + root.offsetX
|
||||||
|
var distLeft = target
|
||||||
|
var distRight = panel.width - (target + root.groupWidth())
|
||||||
|
if (distLeft < distRight) return "left"
|
||||||
|
if (distRight < distLeft) return "right"
|
||||||
|
return "center"
|
||||||
|
}
|
||||||
|
|
||||||
|
// History stacks downward (newest at the top) when the card sits in the top
|
||||||
|
// half of the screen, upward when it sits in the bottom half. Recomputed from
|
||||||
|
// the live card position so it follows D-pad nudges.
|
||||||
|
function stackDown() {
|
||||||
|
return card.y + card.height / 2 < panel.height / 2
|
||||||
|
}
|
||||||
|
|
||||||
// Stack fade: the current combo is fully opaque and every older entry
|
// Stack fade: the current combo is fully opaque and every older entry
|
||||||
// steps down in opacity (tunable here). Clamped so the oldest row of a
|
// steps down in opacity (tunable here). Clamped so the oldest row of a
|
||||||
// 5-deep stack stays readable.
|
// 5-deep stack stays readable.
|
||||||
@@ -167,14 +238,14 @@ Item {
|
|||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
// Row order for the card. Top positions stack the history downward with
|
// Row order for the card. When the card sits in the bottom half the history
|
||||||
// the newest combo on top; bottom positions stack it upward with the
|
// stacks upward with the newest on the bottom edge; in the top half it
|
||||||
// newest on the bottom edge. Each item carries its original index so the
|
// stacks downward with the newest on top. Each item carries its original
|
||||||
// fade always measures distance from the newest combo.
|
// index so the fade always measures distance from the newest combo.
|
||||||
function displayModel() {
|
function displayModel() {
|
||||||
var list = []
|
var list = []
|
||||||
var n = root.entries.length
|
var n = root.entries.length
|
||||||
if (root.position.indexOf("bottom") !== -1) {
|
if (!root.stackDown()) {
|
||||||
for (var i = n - 1; i >= 0; i--) list.push({ entry: root.entries[i], pos: i })
|
for (var i = n - 1; i >= 0; i--) list.push({ entry: root.entries[i], pos: i })
|
||||||
} else {
|
} else {
|
||||||
for (var j = 0; j < n; j++) list.push({ entry: root.entries[j], pos: j })
|
for (var j = 0; j < n; j++) list.push({ entry: root.entries[j], pos: j })
|
||||||
@@ -275,6 +346,8 @@ Item {
|
|||||||
popAnim.restart()
|
popAnim.restart()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Screen shake on combo presses. Amplitude grows with modifiers and the
|
// Screen shake on combo presses. Amplitude grows with modifiers and the
|
||||||
// combo tier; at tier 3 (20+ combos) the continuous jitterTimer takes
|
// combo tier; at tier 3 (20+ combos) the continuous jitterTimer takes
|
||||||
// over instead so the two never fight.
|
// over instead so the two never fight.
|
||||||
@@ -512,12 +585,14 @@ Item {
|
|||||||
if (isFinite(cfg.lingerMs) && cfg.lingerMs >= 0) root.lingerMs = Math.round(cfg.lingerMs)
|
if (isFinite(cfg.lingerMs) && cfg.lingerMs >= 0) root.lingerMs = Math.round(cfg.lingerMs)
|
||||||
if (isFinite(cfg.historyCount)) root.historyCount = Math.max(1, Math.min(5, Math.round(cfg.historyCount)))
|
if (isFinite(cfg.historyCount)) root.historyCount = Math.max(1, Math.min(5, Math.round(cfg.historyCount)))
|
||||||
root.comboMode = cfg.comboMode === true
|
root.comboMode = cfg.comboMode === true
|
||||||
|
if (isFinite(cfg.offsetX)) root.offsetX = Math.round(root.clamp(cfg.offsetX, -2000, 2000))
|
||||||
|
if (isFinite(cfg.offsetY)) root.offsetY = Math.round(root.clamp(cfg.offsetY, -2000, 2000))
|
||||||
}
|
}
|
||||||
|
|
||||||
function migrateConfig() {
|
function migrateConfig() {
|
||||||
// First load with the new location: carry over values from the old
|
// First load with the new location: carry over values from the old
|
||||||
// plugin-dir config (if any) and remove it, or seed the defaults.
|
// plugin-dir config (if any) and remove it, or seed the defaults.
|
||||||
var defaults = '{"mode": "all", "position": "bottom-center", "margin": 67, "lingerMs": 1000, "historyCount": 1}'
|
var defaults = '{"mode": "all", "position": "bottom-center", "margin": 67, "lingerMs": 1000, "historyCount": 1, "comboMode": false, "offsetX": 0, "offsetY": 0}'
|
||||||
migrateProc.command = ["sh", "-c",
|
migrateProc.command = ["sh", "-c",
|
||||||
"if [ -f " + Util.shellQuote(root.legacyConfigPath) + " ]; then "
|
"if [ -f " + Util.shellQuote(root.legacyConfigPath) + " ]; then "
|
||||||
+ "cp " + Util.shellQuote(root.legacyConfigPath) + " " + Util.shellQuote(root.configPath) + "; "
|
+ "cp " + Util.shellQuote(root.legacyConfigPath) + " " + Util.shellQuote(root.configPath) + "; "
|
||||||
@@ -643,31 +718,29 @@ Item {
|
|||||||
visible: root.entries.length > 0
|
visible: root.entries.length > 0
|
||||||
width: card.borderLeft + root.cardPad + root.contentWidth() + root.cardPad + card.borderRight
|
width: card.borderLeft + root.cardPad + root.contentWidth() + root.cardPad + card.borderRight
|
||||||
height: card.borderTop + root.cardPad + root.contentHeight() + root.cardPad + card.borderBottom
|
height: card.borderTop + root.cardPad + root.contentHeight() + root.cardPad + card.borderBottom
|
||||||
|
// Preset base position + manual offset, clamped so the whole visual
|
||||||
|
// group (card and the combo banner) stays on screen. A nudge that
|
||||||
|
// would cross a screen edge is silently ignored.
|
||||||
x: {
|
x: {
|
||||||
var p = root.position
|
var p = root.position
|
||||||
var bx = 0
|
var gW = root.groupWidth()
|
||||||
if (p.indexOf("left") !== -1) bx = root.margin
|
var lo = -root.groupLeftOffset()
|
||||||
else if (p.indexOf("right") !== -1) bx = panel.width - width - root.margin
|
var hi = panel.width - root.groupRightOffset()
|
||||||
else bx = Math.round((panel.width - width) / 2)
|
var base = 0
|
||||||
return bx + root.shakeX
|
if (p.indexOf("left") !== -1) base = root.margin
|
||||||
|
else if (p.indexOf("right") !== -1) base = panel.width - gW - root.margin
|
||||||
|
else base = Math.round((panel.width - gW) / 2)
|
||||||
|
return root.clamp(base + root.offsetX, lo, hi) + root.shakeX
|
||||||
}
|
}
|
||||||
// Top positions anchor the stack's first row at the top edge (the
|
// The group's top Y (banner for top positions, card for bottom)
|
||||||
// history grows downward); bottom positions anchor the last row at
|
// follows the preset + offset and is clamped on screen; the card
|
||||||
// the bottom edge (the history grows upward). In combo mode the
|
// sits below the banner on top positions, at the group top on bottom.
|
||||||
// banner sits below the card for bottom positions and above it for
|
|
||||||
// top positions, and the card makes room for it.
|
|
||||||
y: {
|
y: {
|
||||||
var p = root.position
|
var p = root.position
|
||||||
var by = 0
|
var y0 = root.groupY0()
|
||||||
if (root.bannerVisible()) {
|
var y = y0
|
||||||
if (p.indexOf("top") !== -1) by = root.margin + root.bannerHeight + root.bannerGap
|
if (root.bannerVisible() && p.indexOf("top") !== -1) y = y0 + root.bannerHeight + root.bannerGap
|
||||||
else by = panel.height - root.margin - root.bannerHeight - root.bannerGap - height
|
return y + root.shakeY
|
||||||
} else if (p.indexOf("top") !== -1) {
|
|
||||||
by = root.margin
|
|
||||||
} else {
|
|
||||||
by = panel.height - height - root.margin
|
|
||||||
}
|
|
||||||
return by + root.shakeY
|
|
||||||
}
|
}
|
||||||
color: Util.alpha(Color.popups.background, 0.97)
|
color: Util.alpha(Color.popups.background, 0.97)
|
||||||
borderSpec: Border.surfaceSpec("popups", "border", Color.popups.border, Math.max(1, Style.space(2)))
|
borderSpec: Border.surfaceSpec("popups", "border", Color.popups.border, Math.max(1, Style.space(2)))
|
||||||
@@ -724,16 +797,20 @@ Item {
|
|||||||
visible: root.bannerVisible()
|
visible: root.bannerVisible()
|
||||||
width: root.comboBannerWidth()
|
width: root.comboBannerWidth()
|
||||||
height: root.bannerHeight
|
height: root.bannerHeight
|
||||||
|
// The banner anchors to the card's outward edge so it grows toward the
|
||||||
|
// screen center (left-anchored when the card is on the left, right-
|
||||||
|
// anchored when on the right, centered otherwise); the group clamp on
|
||||||
|
// the card keeps it on screen. Vertically it tracks the group Y0 so it
|
||||||
|
// stays stacked with the card as the offset moves.
|
||||||
x: {
|
x: {
|
||||||
var bx = card.x - root.shakeX + (card.width - width) / 2
|
var mode = root.sideMode()
|
||||||
return bx + root.shakeX
|
if (mode === "left") return card.x
|
||||||
|
if (mode === "right") return card.x + card.width - width
|
||||||
|
return card.x + (card.width - width) / 2
|
||||||
}
|
}
|
||||||
// Bottom positions: banner at the bottom edge, card above it.
|
|
||||||
// Top positions: banner at the top edge, card below it.
|
|
||||||
y: {
|
y: {
|
||||||
var by = root.position.indexOf("top") !== -1
|
var y0 = root.groupY0()
|
||||||
? root.margin
|
var by = root.position.indexOf("top") !== -1 ? y0 : y0 + card.height + root.bannerGap
|
||||||
: panel.height - height - root.margin
|
|
||||||
return by + root.shakeY
|
return by + root.shakeY
|
||||||
}
|
}
|
||||||
color: Util.alpha(Color.popups.background, 0.97)
|
color: Util.alpha(Color.popups.background, 0.97)
|
||||||
|
|||||||
@@ -30,6 +30,18 @@ Panel {
|
|||||||
property int lingerMs: 1000
|
property int lingerMs: 1000
|
||||||
property int historyCount: 1
|
property int historyCount: 1
|
||||||
property bool comboMode: false
|
property bool comboMode: false
|
||||||
|
// Manual fine-tune offsets (px) written by the D-pad; reset when a preset
|
||||||
|
// position is chosen from the dropdown.
|
||||||
|
property int offsetX: 0
|
||||||
|
property int offsetY: 0
|
||||||
|
// How many pixels a D-pad click moves the card.
|
||||||
|
readonly property int nudgeStep: 4
|
||||||
|
// The display's state file: the D-pad injects a virtual arrow key here so
|
||||||
|
// the visualizer renders the move like a real keypress.
|
||||||
|
readonly property string statePath: {
|
||||||
|
var runtime = Quickshell.env("XDG_RUNTIME_DIR")
|
||||||
|
return (runtime && runtime.length > 0 ? runtime : "/tmp") + "/omarchy-key-visualizer.json"
|
||||||
|
}
|
||||||
|
|
||||||
// Config lives outside the plugin folder (the shell reloads all plugin
|
// Config lives outside the plugin folder (the shell reloads all plugin
|
||||||
// code on any file change under ~/.config/omarchy/plugins/), so editing it
|
// code on any file change under ~/.config/omarchy/plugins/), so editing it
|
||||||
@@ -77,6 +89,8 @@ Panel {
|
|||||||
if (isFinite(cfg.lingerMs) && cfg.lingerMs >= 0) root.lingerMs = Math.round(cfg.lingerMs)
|
if (isFinite(cfg.lingerMs) && cfg.lingerMs >= 0) root.lingerMs = Math.round(cfg.lingerMs)
|
||||||
if (isFinite(cfg.historyCount)) root.historyCount = Math.max(1, Math.min(5, Math.round(cfg.historyCount)))
|
if (isFinite(cfg.historyCount)) root.historyCount = Math.max(1, Math.min(5, Math.round(cfg.historyCount)))
|
||||||
root.comboMode = cfg.comboMode === true
|
root.comboMode = cfg.comboMode === true
|
||||||
|
if (isFinite(cfg.offsetX)) root.offsetX = Math.round(cfg.offsetX)
|
||||||
|
if (isFinite(cfg.offsetY)) root.offsetY = Math.round(cfg.offsetY)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Round-trips every option so a menu edit never drops values the display
|
// Round-trips every option so a menu edit never drops values the display
|
||||||
@@ -88,18 +102,88 @@ Panel {
|
|||||||
margin: root.margin,
|
margin: root.margin,
|
||||||
lingerMs: root.lingerMs,
|
lingerMs: root.lingerMs,
|
||||||
historyCount: root.historyCount,
|
historyCount: root.historyCount,
|
||||||
comboMode: root.comboMode
|
comboMode: root.comboMode,
|
||||||
|
offsetX: root.offsetX,
|
||||||
|
offsetY: root.offsetY
|
||||||
}
|
}
|
||||||
for (var k in update) cfg[k] = update[k]
|
for (var k in update) cfg[k] = update[k]
|
||||||
if (update.mode !== undefined) root.mode = update.mode
|
if (update.mode !== undefined) root.mode = update.mode
|
||||||
if (update.position !== undefined) root.position = update.position
|
if (update.position !== undefined) root.position = update.position
|
||||||
if (update.historyCount !== undefined) root.historyCount = update.historyCount
|
if (update.historyCount !== undefined) root.historyCount = update.historyCount
|
||||||
if (update.comboMode !== undefined) root.comboMode = update.comboMode
|
if (update.comboMode !== undefined) root.comboMode = update.comboMode
|
||||||
|
if (update.offsetX !== undefined) root.offsetX = update.offsetX
|
||||||
|
if (update.offsetY !== undefined) root.offsetY = update.offsetY
|
||||||
writeProc.command = ["sh", "-c",
|
writeProc.command = ["sh", "-c",
|
||||||
"printf '%s\\n' '" + JSON.stringify(cfg) + "' > " + Util.shellQuote(root.configPath)]
|
"printf '%s\\n' '" + JSON.stringify(cfg) + "' > " + Util.shellQuote(root.configPath)]
|
||||||
writeProc.running = true
|
writeProc.running = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Nudge the card by one step and feed a virtual arrow key to the visualizer
|
||||||
|
// through its state file, so the move shows up in the history like a real
|
||||||
|
// keypress: we write the arrow down, then release it a moment later.
|
||||||
|
function nudge(dx, dy) {
|
||||||
|
var nx = Math.max(-2000, Math.min(2000, root.offsetX + dx * root.nudgeStep))
|
||||||
|
var ny = Math.max(-2000, Math.min(2000, root.offsetY + dy * root.nudgeStep))
|
||||||
|
root.writeConfig({ offsetX: nx, offsetY: ny })
|
||||||
|
var arrow = dx < 0 ? "←" : dx > 0 ? "→" : dy < 0 ? "↑" : "↓"
|
||||||
|
var now = Math.floor(Date.now() / 1000)
|
||||||
|
nudgeKeyProc.command = ["sh", "-c",
|
||||||
|
"printf '%s' '" + JSON.stringify({ keys: [arrow], t: now }) + "' > " + Util.shellQuote(root.statePath)]
|
||||||
|
nudgeKeyProc.running = true
|
||||||
|
nudgeReleaseTimer.interval = 120
|
||||||
|
nudgeReleaseTimer.restart()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Press-and-hold helpers for the D-pad: a tap moves once; holding starts the
|
||||||
|
// repeat timer so the card keeps moving (and the arrow keeps feeding the
|
||||||
|
// visualizer) until the button is released.
|
||||||
|
function startNudge(dx, dy) {
|
||||||
|
root.nudge(dx, dy)
|
||||||
|
nudgeHoldTimer.dx = dx
|
||||||
|
nudgeHoldTimer.dy = dy
|
||||||
|
nudgeHoldTimer.interval = 280
|
||||||
|
nudgeHoldTimer.repeat = false
|
||||||
|
nudgeHoldTimer.restart()
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopNudge() {
|
||||||
|
nudgeHoldTimer.interval = 280
|
||||||
|
nudgeHoldTimer.repeat = false
|
||||||
|
nudgeHoldTimer.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: nudgeKeyProc
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: nudgeReleaseTimer
|
||||||
|
interval: 120
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
|
var now = Math.floor(Date.now() / 1000)
|
||||||
|
nudgeKeyProc.command = ["sh", "-c",
|
||||||
|
"printf '%s' '" + JSON.stringify({ keys: [], t: now }) + "' > " + Util.shellQuote(root.statePath)]
|
||||||
|
nudgeKeyProc.running = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// While a D-pad button is held: the first move happens on press (in
|
||||||
|
// startNudge), then after a short delay this timer repeats every 60ms.
|
||||||
|
Timer {
|
||||||
|
id: nudgeHoldTimer
|
||||||
|
interval: 280
|
||||||
|
repeat: false
|
||||||
|
property int dx: 0
|
||||||
|
property int dy: 0
|
||||||
|
onTriggered: {
|
||||||
|
root.nudge(dx, dy)
|
||||||
|
interval = 60
|
||||||
|
repeat = true
|
||||||
|
restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: writeProc
|
id: writeProc
|
||||||
}
|
}
|
||||||
@@ -250,7 +334,75 @@ Panel {
|
|||||||
{ value: "bottom-center", label: "Bottom center" },
|
{ value: "bottom-center", label: "Bottom center" },
|
||||||
{ value: "bottom-right", label: "Bottom right" }
|
{ value: "bottom-right", label: "Bottom right" }
|
||||||
]
|
]
|
||||||
onChanged: function(v) { root.writeConfig({ position: v }) }
|
onChanged: function(v) { root.writeConfig({ position: v, offsetX: 0, offsetY: 0 }) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fine-tune D-pad: 4px per click; holding a button keeps moving (the
|
||||||
|
// move repeats after a short delay). Each nudge also feeds a virtual
|
||||||
|
// arrow key to the visualizer, so the move shows up in its history.
|
||||||
|
// The Button draws the look; a transparent MouseArea on top captures
|
||||||
|
// press/release so a held button can repeat.
|
||||||
|
RowLayout {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Style.spacing.xs
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: Style.spacing.controlHeight
|
||||||
|
Button {
|
||||||
|
anchors.fill: parent
|
||||||
|
text: "←"
|
||||||
|
tooltipText: "Move left"
|
||||||
|
}
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: false
|
||||||
|
onPressed: root.startNudge(-1, 0)
|
||||||
|
onReleased: root.stopNudge()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: Style.spacing.xxs
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Repeater {
|
||||||
|
model: [
|
||||||
|
{ dx: 0, dy: -1, label: "↑", tip: "Move up" },
|
||||||
|
{ dx: 0, dy: 1, label: "↓", tip: "Move down" }
|
||||||
|
]
|
||||||
|
delegate: Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: Math.round(Style.spacing.controlHeight / 2)
|
||||||
|
Button {
|
||||||
|
anchors.fill: parent
|
||||||
|
text: modelData.label
|
||||||
|
tooltipText: modelData.tip
|
||||||
|
}
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: false
|
||||||
|
onPressed: root.startNudge(modelData.dx, modelData.dy)
|
||||||
|
onReleased: root.stopNudge()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: Style.spacing.controlHeight
|
||||||
|
Button {
|
||||||
|
anchors.fill: parent
|
||||||
|
text: "→"
|
||||||
|
tooltipText: "Move right"
|
||||||
|
}
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: false
|
||||||
|
onPressed: root.startNudge(1, 0)
|
||||||
|
onReleased: root.stopNudge()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Linger --------------------------------------------------------
|
// Linger --------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user