diff options
author | bptato <nincsnevem662@gmail.com> | 2024-09-24 19:36:21 +0200 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2024-11-23 20:52:38 +0100 |
commit | 2138e8eb0f57bc78bfc336f8c5cd8092f61f0700 (patch) | |
tree | a04e83b67940fb45c662fc6063804e121030cf58 /src | |
parent | 23f2b3aca50c286eb1b65ef84b827826b47bd1ca (diff) | |
download | chawan-2138e8eb0f57bc78bfc336f8c5cd8092f61f0700.tar.gz |
pager, select: add right click menu, fix some bugs
It's *not* a context menu: items are fixed, and currently not even customizable. Former is a feature, latter is a bug. Also, select now has improved mouse support; its behavior is a combination of the w3m menu (for btn1) and GTK Firefox context menu (for btn3). Also, fix some bugs in select: * lines with double width chars are handled better (not perfectly, but at least the grid isn't completely mangled anymore) * non-multiple select now highlights the currently selected option * small selects at the bottom of the screen are handled correctly * selects at the right edge of the screen are handled correctly * select multiple no longer discards selected items on cursorLeft
Diffstat (limited to 'src')
-rw-r--r-- | src/local/client.nim | 182 | ||||
-rw-r--r-- | src/local/container.nim | 3 | ||||
-rw-r--r-- | src/local/pager.nim | 58 | ||||
-rw-r--r-- | src/local/select.nim | 58 |
4 files changed, 219 insertions, 82 deletions
diff --git a/src/local/client.nim b/src/local/client.nim index 93c9f3cd..6b7e0a67 100644 --- a/src/local/client.nim +++ b/src/local/client.nim @@ -58,6 +58,7 @@ type pressed: tuple[col, row: int] exitCode: int inEval: bool + blockTillRelease: bool ContainerData = ref object of MapData container: Container @@ -214,6 +215,130 @@ proc evalAction(client: Client; action: string; arg0: int32): EmptyPromise = JS_FreeValue(ctx, ret) return p +#TODO move mouse input handling somewhere else... +proc handleMouseInputGeneric(client: Client; input: MouseInput) = + case input.button + of mibLeft: + case input.t + of mitPress: + client.pressed = (input.col, input.row) + of mitRelease: + if client.pressed != (-1, -1): + let diff = (input.col - client.pressed.col, + input.row - client.pressed.row) + if diff[0] > 0: + discard client.evalAction("cmd.buffer.scrollLeft", int32(diff[0])) + elif diff[0] < 0: + discard client.evalAction("cmd.buffer.scrollRight", -int32(diff[0])) + if diff[1] > 0: + discard client.evalAction("cmd.buffer.scrollUp", int32(diff[1])) + elif diff[1] < 0: + discard client.evalAction("cmd.buffer.scrollDown", -int32(diff[1])) + client.pressed = (-1, -1) + else: discard + of mibWheelUp: + if input.t == mitPress: + discard client.evalAction("cmd.buffer.scrollUp", 5) + of mibWheelDown: + if input.t == mitPress: + discard client.evalAction("cmd.buffer.scrollDown", 5) + of mibWheelLeft: + if input.t == mitPress: + discard client.evalAction("cmd.buffer.scrollLeft", 5) + of mibWheelRight: + if input.t == mitPress: + discard client.evalAction("cmd.buffer.scrollRight", 5) + else: discard + +proc handleMouseInput(client: Client; input: MouseInput; select: Select) = + let y = select.fromy + input.row - select.y - 1 # one off because of border + case input.button + of mibRight: + if (input.col, input.row) != client.pressed: + # Prevent immediate movement/submission in case the menu appeared under + # the cursor. + select.setCursorY(y) + case input.t + of mitPress: + # Do not include borders, so that a double right click closes the + # menu again. + if input.row notin select.y + 1 ..< select.y + select.height - 1 or + input.col notin select.x + 1 ..< select.x + select.width - 1: + client.blockTillRelease = true + select.cursorLeft() + of mitRelease: + if input.row in select.y + 1 ..< select.y + select.height - 1 and + input.col in select.x + 1 ..< select.x + select.width - 1 and + (input.col, input.row) != client.pressed: + select.click() + # forget about where we started once btn3 is released + client.pressed = (-1, -1) + of mitMove: discard + of mibLeft: + case input.t + of mitPress: + if input.row notin select.y ..< select.y + select.height or + input.col notin select.x ..< select.x + select.width: + # clicked outside the select + client.blockTillRelease = true + select.cursorLeft() + of mitRelease: + let at = (input.col, input.row) + if at == client.pressed and + (input.row in select.y + 1 ..< select.y + select.height - 1 and + input.col in select.x + 1 ..< select.x + select.width - 1 or + select.multiple and at == (select.x, select.y)): + # clicked inside the select + select.setCursorY(y) + select.click() + of mitMove: discard + else: discard + +proc handleMouseInput(client: Client; input: MouseInput; container: Container) = + case input.button + of mibLeft: + if input.t == mitRelease and client.pressed == (input.col, input.row): + let prevx = container.cursorx + let prevy = container.cursory + #TODO I wish we could avoid setCursorXY if we're just going to + # click, but that doesn't work with double-width chars + container.setCursorXY(container.fromx + input.col, + container.fromy + input.row) + if container.cursorx == prevx and container.cursory == prevy: + discard client.evalAction("cmd.buffer.click", 0) + of mibMiddle: + if input.t == mitRelease: # release, to emulate w3m + discard client.evalAction("cmd.pager.discardBuffer", 0) + of mibRight: + if input.t == mitPress: # w3m uses release, but I like this better + client.pressed = (input.col, input.row) + container.setCursorXY(container.fromx + input.col, + container.fromy + input.row) + client.pager.openMenu(input.col, input.row) + of mibThumbInner: + if input.t == mitPress: + discard client.evalAction("cmd.pager.prevBuffer", 0) + of mibThumbTip: + if input.t == mitPress: + discard client.evalAction("cmd.pager.nextBuffer", 0) + else: discard + +proc handleMouseInput(client: Client; input: MouseInput) = + if client.blockTillRelease: + if input.t == mitRelease: + client.blockTillRelease = false + else: + return + if client.pager.menu != nil: + client.handleMouseInput(input, client.pager.menu) + elif (let container = client.pager.container; container != nil): + if container.select != nil: + client.handleMouseInput(input, container.select) + else: + client.handleMouseInput(input, container) + if not client.blockTillRelease: + client.handleMouseInputGeneric(input) + # The maximum number we are willing to accept. # This should be fine for 32-bit signed ints (which precnum currently is). # We can always increase it further (e.g. by switching to uint32, uint64...) if @@ -243,62 +368,7 @@ proc handleCommandInput(client: Client; c: char): EmptyPromise = let input = client.pager.term.parseMouseInput() if input.isSome: let input = input.get - let container = client.pager.container - if container != nil: - case input.button - of mibLeft: - case input.t - of mitPress: - client.pressed = (input.col, input.row) - of mitRelease: - if client.pressed == (input.col, input.row): - let prevx = container.cursorx - let prevy = container.cursory - #TODO I wish we could avoid setCursorXY if we're just going to - # click, but that doesn't work with double-width chars - container.setCursorXY(container.fromx + input.col, - container.fromy + input.row) - if container.cursorx == prevx and container.cursory == prevy: - discard client.evalAction("cmd.buffer.click", 0) - else: - let diff = (input.col - client.pressed.col, - input.row - client.pressed.row) - if diff[0] > 0: - discard client.evalAction("cmd.buffer.scrollLeft", - int32(diff[0])) - else: - discard client.evalAction("cmd.buffer.scrollRight", - -int32(diff[0])) - if diff[1] > 0: - discard client.evalAction("cmd.buffer.scrollUp", - int32(diff[1])) - else: - discard client.evalAction("cmd.buffer.scrollDown", - -int32(diff[1])) - client.pressed = (-1, -1) - else: discard - of mibMiddle: - if input.t == mitRelease: # release, to emulate w3m - discard client.evalAction("cmd.pager.discardBuffer", 0) - of mibWheelUp: - if input.t == mitPress: - discard client.evalAction("cmd.buffer.scrollUp", 5) - of mibWheelDown: - if input.t == mitPress: - discard client.evalAction("cmd.buffer.scrollDown", 5) - of mibWheelLeft: - if input.t == mitPress: - discard client.evalAction("cmd.buffer.scrollLeft", 5) - of mibWheelRight: - if input.t == mitPress: - discard client.evalAction("cmd.buffer.scrollRight", 5) - of mibThumbInner: - if input.t == mitPress: - discard client.evalAction("cmd.pager.prevBuffer", 0) - of mibThumbTip: - if input.t == mitPress: - discard client.evalAction("cmd.pager.nextBuffer", 0) - else: discard + client.handleMouseInput(input) client.pager.inputBuffer = "" elif "\e[<".startsWith(client.pager.inputBuffer): client.feednext = true diff --git a/src/local/container.nim b/src/local/container.nim index bee93e73..dbc510c9 100644 --- a/src/local/container.nim +++ b/src/local/container.nim @@ -1618,6 +1618,7 @@ proc displaySelect(container: Container; selectResult: SelectResult) = selectResult.selected, container.acursorx, container.acursory, + container.width, container.height, selectFinish, container @@ -1671,7 +1672,7 @@ proc windowChange*(container: Container; attrs: WindowAttributes) = container.needslines = true ) if container.select != nil: - container.select.windowChange(container.height) + container.select.windowChange(container.width, container.height) proc peek(container: Container) {.jsfunc.} = container.alert($container.url) diff --git a/src/local/pager.nim b/src/local/pager.nim index fbfd3710..eec61715 100644 --- a/src/local/pager.nim +++ b/src/local/pager.nim @@ -116,7 +116,7 @@ type container*: Container prev*: Container - Pager* = ref object + Pager* = ref object of RootObj alertState: PagerAlertState alerts*: seq[string] askcharpromise*: Promise[string] @@ -142,6 +142,7 @@ type linemode: LineMode loader*: FileLoader luctx: LUContext + menu*: Select navDirection {.jsget.}: NavDirection notnum*: bool # has a non-numeric character been input already? numload*: int # number of pages currently being loaded @@ -235,7 +236,9 @@ proc reflect(ctx: JSContext; this_val: JSValue; argc: cint; proc getter(ctx: JSContext; pager: Pager; a: JSAtom): JSValue {.jsgetownprop.} = if pager.container != nil: - let cval = if pager.container.select != nil: + let cval = if pager.menu != nil: + ctx.toJS(pager.menu) + elif pager.container.select != nil: ctx.toJS(pager.container.select) else: ctx.toJS(pager.container) @@ -715,10 +718,17 @@ proc draw*(pager: Pager) = imageRedraw = true if container.select != nil: container.select.redraw = true - if (let select = container.select; select != nil and select.redraw): + if (let select = container.select; select != nil and + (select.redraw or pager.display.redraw)): select.drawSelect(pager.display.grid) select.redraw = false pager.display.redraw = true + if (let menu = pager.menu; menu != nil and + (menu.redraw or pager.display.redraw)): + menu.drawSelect(pager.display.grid) + menu.redraw = false + pager.display.redraw = true + imageRedraw = false if pager.display.redraw: pager.term.writeGrid(pager.display.grid) pager.display.redraw = false @@ -749,6 +759,8 @@ proc draw*(pager: Pager) = pager.term.setCursor(pager.askcursor, pager.attrs.height - 1) elif pager.lineedit != nil: pager.term.setCursor(pager.lineedit.getCursorX(), pager.attrs.height - 1) + elif (let menu = pager.menu; menu != nil): + pager.term.setCursor(menu.getCursorX(), menu.getCursorY()) elif container != nil: if (let select = container.select; select != nil): pager.term.setCursor(select.getCursorX(), select.getCursorY()) @@ -2237,6 +2249,46 @@ proc metaRefresh(pager: Pager; container: Container; n: int; url: URL) = for arg in args: JS_FreeValue(ctx, arg) +const MenuMap = [ + ("Previous buffer (,)", "pager.prevBuffer"), + ("Next buffer (.)", "pager.nextBuffer"), + ("Discard buffer (D)", "pager.discardBuffer"), + ("View source (\\)", "pager.toggleSource"), + ("Edit source (sE)", "buffer.sourceEdit"), + ("Save source (sS)", "buffer.saveSource"), + ("Reload (U)", "pager.reloadBuffer"), + ("Save link (s<Enter>)", "buffer.saveLink"), + ("View image (I)", "buffer.viewImage") +] + +proc menuFinish(opaque: RootRef; select: Select; sr: SubmitResult) = + let pager = Pager(opaque) + case sr + of srCancel: discard + of srSubmit: + let action = MenuMap[select.selected[0]][1] + let fun = pager.config.cmd.map.getOrDefault(action, JS_UNDEFINED) + discard pager.timeouts.setTimeout(ttTimeout, fun, 0, []) + pager.menu = nil + if pager.container != nil: + pager.container.queueDraw() + pager.draw() + +proc openMenu*(pager: Pager; x = -1; y = -1) {.jsfunc.} = + let x = if x == -1 and pager.container != nil: + pager.container.acursorx + else: + max(x, 0) + let y = if y == -1 and pager.container != nil: + pager.container.acursory + else: + max(y, 0) + var options: seq[string] = @[] + for (s, _) in MenuMap: + options.add(s) + pager.menu = newSelect(false, options, @[], x, y, pager.bufWidth, + pager.bufHeight, menuFinish, pager) + proc handleEvent0(pager: Pager; container: Container; event: ContainerEvent): bool = case event.t diff --git a/src/local/select.nim b/src/local/select.nim index 24330257..bf4d5500 100644 --- a/src/local/select.nim +++ b/src/local/select.nim @@ -11,17 +11,17 @@ type Select* = ref object options: seq[string] - multiple: bool + multiple* {.jsget.}: bool oselected*: seq[int] # old selection selected*: seq[int] # new selection - fromy {.jsget.}: int # first index to display + fromy* {.jsget.}: int # first index to display cursory {.jsget.}: int # selected index maxw: int # widest option maxh: int # maximum height on screen (yes the naming is dumb) # location on screen #TODO make this absolute - x: int - y: int + x*: int + y*: int redraw*: bool bpos: seq[int] opaque: RootRef @@ -41,15 +41,22 @@ func dispheight(select: Select): int = proc setFromY(select: Select; y: int) = select.fromy = max(min(y, select.options.len - select.maxh), 0) -proc setCursorY(select: Select; y: int) = - let y = clamp(y, 0, select.options.high) +func width*(select: Select): int = + return select.maxw + 2 + +func height*(select: Select): int = + return select.maxh + 2 + +proc setCursorY*(select: Select; y: int) = + var y = clamp(y, -1, select.options.high) + if not select.multiple: + y = max(y, 0) if select.fromy > y: select.setFromY(y) if select.fromy + select.dispheight <= y: select.setFromY(y - select.dispheight + 1) select.cursory = y select.queueDraw() - assert select.cursory >= select.fromy proc getCursorX*(select: Select): int = if select.cursory == -1: @@ -93,15 +100,19 @@ proc cursorPrevLink(select: Select) {.jsfunc.} = proc cursorNextLink(select: Select) {.jsfunc.} = select.cursorDown() +proc gotoLine(select: Select; n: int) {.jsfunc.} = + select.setCursorY(n + 1) + proc cancel(select: Select) {.jsfunc.} = select.finishImpl(select.opaque, select, srCancel) proc submit(select: Select) {.jsfunc.} = + if not select.multiple: + select.selected = @[select.cursory] select.finishImpl(select.opaque, select, srSubmit) -proc click(select: Select) {.jsfunc.} = +proc click*(select: Select) {.jsfunc.} = if not select.multiple: - select.selected = @[select.cursory] select.submit() elif select.cursory == -1: select.submit() @@ -118,8 +129,11 @@ proc click(select: Select) {.jsfunc.} = select.selected.insert(i, k) select.queueDraw() -proc cursorLeft(select: Select) {.jsfunc.} = - select.submit() +proc cursorLeft*(select: Select) {.jsfunc.} = + if select.multiple: + select.submit() + else: + select.cancel() proc cursorRight(select: Select) {.jsfunc.} = select.click() @@ -205,8 +219,7 @@ proc drawBorders(display: var FixedGrid; sx, ex, sy, ey: int; display[y * display.width + x].str = " " inc x else: - #x = display[y * display.width + x].str.width() - inc x + x += display[y * display.width + x].str.width() # Draw corners. let tl = if upmore: VerticalBar else: CornerTopLeft let tr = if upmore: VerticalBar else: CornerTopRight @@ -275,7 +288,9 @@ proc drawSelect*(select: Select; display: var FixedGrid) = var j = 0 var x = sx let dls = y * display.width - if k < select.selected.len and select.selected[k] == i: + if select.multiple and k < select.selected.len and + select.selected[k] == i or + not select.multiple and select.getCursorY() == y: format.flags.incl(ffReverse) inc k else: @@ -298,17 +313,18 @@ proc drawSelect*(select: Select; display: var FixedGrid) = display[dls + x].format = format inc x -proc windowChange*(select: Select; height: int) = +proc windowChange*(select: Select; width, height: int) = select.maxh = height - 2 if select.y + select.options.len >= select.maxh: - select.y = height - select.options.len - if select.y < 0: - select.y = 0 + select.y = max(select.maxh - select.options.len, 0) + if select.x + select.maxw + 2 > width: + select.x = max(width - select.maxw, 0) select.setCursorY(select.cursory) select.queueDraw() proc newSelect*(multiple: bool; options: seq[string]; selected: seq[int]; - x, y, height: int; finishImpl: SelectFinish; opaque: RootRef): Select = + x, y, width, height: int; finishImpl: SelectFinish; opaque: RootRef): + Select = let select = Select( multiple: multiple, options: options, @@ -319,12 +335,10 @@ proc newSelect*(multiple: bool; options: seq[string]; selected: seq[int]; finishImpl: finishImpl, opaque: opaque ) - select.windowChange(height) for opt in select.options.mitems: opt.mnormalize() select.maxw = max(select.maxw, opt.width()) - select.windowChange(height) - select.queueDraw() + select.windowChange(width, height) return select proc addSelectModule*(ctx: JSContext) = |