From 635dcecc7ba2862527d8802adb21d42e461f3532 Mon Sep 17 00:00:00 2001 From: Joshua Bell Date: Wed, 9 Oct 2024 23:36:25 -0700 Subject: [PATCH] DeskTop: Make arrow keys navigate icons spatially Previously, arrow keys would iterate through icons in index order, with up/down behaving the same as left/right. While simple, this was surprising to users. Now, in icon views, Arrow keys will navigate the icons "spatially". That is, pressing the Right Arrow key will try to find an icon to the right of the current selection, pressing the Down Arrow key will try to find an icon below the current selection, and so on. If there is no such icon, selection remains unchanged. This works by searching for an icon in the indicated direction, which is fast if there is an icon. At 1MHz if there is no such icon then there can be a bit of a delay depending on the number of icons in the window. Also, if icons are in the regular grid then the behavior is very predictable. If you move icons so that they overlap, things are a bit weird... so don't do that. In list views, the Up/Down Arrow keys retain their current behavior, but the Left/Right Arrow keys are now ignored. This also corrects several edge cases: * In icon views, if nothing is selected in the active window (or desktop if none), the first icon becomes selected. * In list views, if nothing is selected in the active window, the first or last icon is selected depending on whether Down Arrow or Up Arrow was pressed. * If selection doesn't change (e.g. there's nothing to select in the active window, or there's nothing further to select), then selection doesn't change. Resolves #300 --- RELEASE-NOTES.md | 1 + desktop/main.s | 339 ++++++++++++++++++++++++++++++------------ res/notes/testplan.md | 22 ++- 3 files changed, 264 insertions(+), 98 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 439cf18b..3f06a6fb 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -26,6 +26,7 @@ Project Page: https://github.com/a2stuff/a2d * Fix Apple+O to not "Open and Close" when menu already showing. ([#796](https://github.com/a2stuff/a2d/issues/796)) * Allow keyboard selection of icons in all windows. * Show tip about copying PRODOS during Format/Erase process. +* Arrow keys now move icon selection in appropriate direction. ([#300](https://github.com/a2stuff/a2d/issues/300)) ### Selector diff --git a/desktop/main.s b/desktop/main.s index 06eafa03..b56a9d91 100644 --- a/desktop/main.s +++ b/desktop/main.s @@ -350,13 +350,13 @@ HandleKeydown: lda event_params::key cmp #CHAR_LEFT - jeq CmdHighlightPrev + jeq CmdHighlightLeft cmp #CHAR_UP - jeq CmdHighlightPrev + jeq CmdHighlightUp cmp #CHAR_RIGHT - jeq CmdHighlightNext + jeq CmdHighlightRight cmp #CHAR_DOWN - jeq CmdHighlightNext + jeq CmdHighlightDown cmp #CHAR_TAB jeq CmdHighlightAlpha cmp #'`' @@ -3377,39 +3377,50 @@ error: .proc CmdHighlightImpl +;;; Local variables on ZP +PARAM_BLOCK, $50 +delta .byte +END_PARAM_BLOCK + + ;; ---------------------------------------- ;; Next/prev in sorted order -a_prev: lda #$80 - bne store ; always -a_next: lda #$00 - beq store ; always - ;; Tab / Shift+Tab - next/prev in sorted order, based on shift + ;; Tab / Shift+Tab alpha: jsr ShiftDown + bpl a_next + FALL_THROUGH_TO a_prev + +a_prev: lda #AS_BYTE(-1) + .byte OPC_BIT_abs ; skip next 2-byte instruction +a_next: lda #1 -store: sta flag - jsr GetNameSelectableIconsSorted + sta delta + jsr GetKeyboardSelectableIconsSorted jmp common + ;; ---------------------------------------- ;; Arrows - next/prev in icon order - -prev: lda #$80 +prev: lda #AS_BYTE(-1) .byte OPC_BIT_abs ; skip next 2-byte instruction -next: lda #$00 - sta flag +next: lda #1 -;;; First byte is icon count. Rest is a list of selectable icons. - buffer := $1800 - jsr GetSelectableIcons + sta delta + jsr GetKeyboardSelectableIcons + FALL_THROUGH_TO common +;;; -------------------------------------------------- ;;; Figure out current selected index, based on selection. common: + ;; First byte is icon count. Rest is a list of selectable icons. + buffer := $1800 + ;; Anything selectable? lda buffer - RTS_IF_EQ + beq ret lda selected_icon_count - beq pick_first + beq fallback ;; Try to find actual selection in our list lda selected_icon_list ; Only consider first, otherwise N^2 @@ -3420,50 +3431,32 @@ common: dex bpl :- - ;; No selection; pick the first volume icon. -pick_first: - copy #0, selected_index - beq select_next ; always - - ;; There was a selection; clear it, and pick prev/next - ;; based on keypress. -pick_next_prev: - stx selected_index - jsr ClearSelection - - flag := *+1 - lda #SELF_MODIFIED_BYTE - bmi select_prev - FALL_THROUGH_TO select_next - -select_next: - selected_index := *+1 - ldx #SELF_MODIFIED_BYTE - inx - cpx buffer - bne :+ + ;; If not in our list, use a fallback. +fallback: ldx #0 -: stx selected_index - jmp HighlightIcon - -select_prev: - ldx selected_index - dex - bpl :+ + ldy delta + IF_NEG ldx buffer dex -: stx selected_index - FALL_THROUGH_TO HighlightIcon + END_IF + bpl select_index ; always -;;; Highlight the icon in the list at `selected_index` -HighlightIcon: - ldx selected_index + ;; There was a selection; pick prev/next based on keypress. +pick_next_prev: + txa + clc + adc delta ; +1 or -1 + cmp buffer + bcs ret ; handles >= max or < 0 + tax + FALL_THROUGH_TO select_index + +select_index: lda buffer+1,x - pha - jsr GetIconWindow - jsr ActivateWindow ; no-op if already active, or 0 - pla - jmp SelectIcon + jmp ClearSelectionActivateWindowAndSelectIcon + +ret: rts + .endproc ; CmdHighlightImpl CmdHighlightPrev := CmdHighlightImpl::prev CmdHighlightNext := CmdHighlightImpl::next @@ -3471,6 +3464,161 @@ CmdHighlightAlpha := CmdHighlightImpl::alpha CmdHighlightAlphaPrev := CmdHighlightImpl::a_prev CmdHighlightAlphaNext := CmdHighlightImpl::a_next +;;; ============================================================ + +.proc CmdHighlightSpatialImpl + +;;; Local variables on ZP +PARAM_BLOCK, $50 +delta_x .word +delta_y .word +tmpw .word +iter_count .byte +index .byte +END_PARAM_BLOCK + + ;; Values that work in Icon and Small Icon views + kDeltaX = 40 + kDeltaY = 10 + +left: ldx #AS_BYTE(-kDeltaX) + ldy #0 + beq common ; always + +right: ldx #kDeltaX + ldy #0 + beq common ; always + +up: ldy #AS_BYTE(-kDeltaY) + ldx #0 + beq common ; always + +down: ldy #kDeltaY + ldx #0 + FALL_THROUGH_TO common + +;;; -------------------------------------------------- +;;; Compute 16-bit deltas + +common: + stx delta_x + ;; sign-extend + txa + and #$80 + bpl :+ + lda #$FF +: sta delta_x+1 + + sty delta_y + ;; sign-extend + tya + and #$80 + bpl :+ + lda #$FF +: sta delta_y+1 + +;;; -------------------------------------------------- +;;; If a list view, use index-based logic + + jsr GetActiveWindowViewBy ; N=0 is icon view, N=1 is list view + IF_NEG + lda delta_x + RTS_IF_NOT_ZERO ; ignore + bit delta_y + jpl CmdHighlightNext + jmp CmdHighlightPrev + END_IF + +;;; -------------------------------------------------- +;;; Identify a starting icon + + jsr LoadActiveWindowEntryTable + + lda selected_icon_count + jeq fallback + + lda active_window_id + cmp selected_window_id + jne fallback + + lda selected_icon_list ; use first + sta icon_param + +;;; -------------------------------------------------- +;;; Get bounds, walk rect until a different icon is contained + + ITK_CALL IconTK::GetIconBounds, icon_param ; inits `tmp_rect` + + ;; Constrain to icon bitmap width (long names tend to overlap) + add16 tmp_rect+MGTK::Rect::x1, tmp_rect+MGTK::Rect::x2, tmpw + lsr16 tmpw + sub16 tmpw, #kIconBitmapWidth/2, tmp_rect+MGTK::Rect::x1 + add16 tmpw, #kIconBitmapWidth/2, tmp_rect+MGTK::Rect::x2 + + copy #(560 / kDeltaX), iter_count + +rect_loop: + add16 delta_x, tmp_rect+MGTK::Rect::x1, tmp_rect+MGTK::Rect::x1 + add16 delta_x, tmp_rect+MGTK::Rect::x2, tmp_rect+MGTK::Rect::x2 + add16 delta_y, tmp_rect+MGTK::Rect::y1, tmp_rect+MGTK::Rect::y1 + add16 delta_y, tmp_rect+MGTK::Rect::y2, tmp_rect+MGTK::Rect::y2 + + copy #0, index +icon_loop: + ldx index + cpx cached_window_entry_count + beq next_rect + + lda cached_window_entry_list,x + sta icon_param + jsr IsIconSelected + beq next_icon + + ITK_CALL IconTK::IconInRect, icon_param + IF_NOT_ZERO + lda icon_param + bne select ; always + END_IF + +next_icon: + inc index + bne icon_loop ; always + +next_rect: + dec iter_count + bne rect_loop + +ret: rts + +;;; -------------------------------------------------- +;;; If there was no (usable) selection, pick icon from active window. + +fallback: + lda cached_window_entry_count + beq ret + + ;; Default to first icon + ldx #0 + lda active_window_id + IF_ZERO + ;; ...except on desktop, since that's trash. + inx + cpx cached_window_entry_count + bne :+ + dex +: + END_IF + lda cached_window_entry_list,x + +select: + jmp ClearSelectionActivateWindowAndSelectIcon + +.endproc ; CmdHighlightSpatialImpl +CmdHighlightLeft := CmdHighlightSpatialImpl::left +CmdHighlightRight := CmdHighlightSpatialImpl::right +CmdHighlightDown := CmdHighlightSpatialImpl::down +CmdHighlightUp := CmdHighlightSpatialImpl::up + ;;; ============================================================ ;;; Type Down Selection @@ -3510,7 +3658,7 @@ file_char: sta typedown_buf,x ;; Collect and sort the potential type-down matches - jsr GetNameSelectableIconsSorted + jsr GetKeyboardSelectableIconsSorted lda num_filenames beq done @@ -3528,13 +3676,9 @@ file_char: beq done ; yes, nothing to do ;; Update the selection. - jsr ClearSelection icon := *+1 lda #SELF_MODIFIED_BYTE - jsr GetIconWindow - jsr ActivateWindow ; no-op if already active, or 0 - lda icon - jsr SelectIcon + jsr ClearSelectionActivateWindowAndSelectIcon done: lda #0 rts @@ -3592,37 +3736,14 @@ typedown_buf: .res 16, 0 ;;; ============================================================ -;;; Build list of selectable icons. -;;; This is all icons, so order is stable as windows are activated -;;; while the arrow keys are held down and selection cycles. +;;; Build list of keyboard-selectable icons. +;;; This is all icons in active window. ;;; Output: Buffer at $1800 (length prefixed) +;;; X = number of icons -.proc GetSelectableIcons +.proc GetKeyboardSelectableIcons buffer := $1800 - window_id := findwindow_params::window_id - - ldx icon_count - stx buffer - beq ret - -: lda window_entry_table,x - sta buffer+1,x - dex - bpl :- - -ret: rts -.endproc ; GetSelectableIcons - -;;; Gather the name-selectable icons - those in the active window or -;;; desktop - into buffer at $1800, and sort them by name. -;;; Output: Buffer at $1800 (length prefixed) - -.proc GetNameSelectableIconsSorted - buffer := $1800 - ptr1 := $06 - ptr2 := $08 - jsr LoadActiveWindowEntryTable ldx #0 : @@ -3631,9 +3752,22 @@ ret: rts lda cached_window_entry_list,x sta buffer+1,x inx - bne :- + bne :- ; always : stx buffer + rts +.endproc ; GetKeyboardSelectableIcons + +;;; Gather the keyboard-selectable icons into buffer at $1800, and +;;; sort them by name. +;;; Output: Buffer at $1800 (length prefixed) + +.proc GetKeyboardSelectableIconsSorted + buffer := $1800 + ptr1 := $06 + ptr2 := $08 + + jsr GetKeyboardSelectableIcons cpx #2 RTS_IF_CC @@ -3682,7 +3816,7 @@ next: inc inner bne oloop rts -.endproc ; GetNameSelectableIconsSorted +.endproc ; GetKeyboardSelectableIconsSorted ;;; Assuming selectable icon buffer at $1800 is populated by the ;;; above functions, return ptr to nth icon's name in A,X @@ -3736,6 +3870,23 @@ gt: lda #$FF ; Z=0 ret: rts .endproc ; CompareStrings +;;; ============================================================ +;;; Replace selection with the specified icon. The icon's +;;; window is activated if necessary. +;;; Inputs: A = icon id + +.proc ClearSelectionActivateWindowAndSelectIcon + pha + jsr ClearSelection + pla + pha + jsr GetIconWindow + jsr ActivateWindow ; no-op if already active, or 0 + pla + + FALL_THROUGH_TO SelectIcon +.endproc ; ClearSelectionActivateWindowAndSelectIcon + ;;; ============================================================ ;;; Select an arbitrary icon. If windowed, it is scrolled into view. ;;; Inputs: A = icon id diff --git a/res/notes/testplan.md b/res/notes/testplan.md index b3f3acbd..546396ed 100644 --- a/res/notes/testplan.md +++ b/res/notes/testplan.md @@ -238,19 +238,33 @@ * Open a window containing no file icons. * Open a window containing file icons. * Run these steps: - * Clear selection. Press Tab repeatedly. Verify that icons in the active window (or desktop if no window is open) are selected in lexicographic order. + * Clear selection. Press Tab repeatedly. Verify that icons in the active window (or desktop if no window is open) are selected in lexicographic order, starting with first in lexicographic order. * Select an icon. Press Tab. Verify that the next icon in the active window (or desktop if no window is open) in lexicographic order is selected. - * Clear selection. Press \` repeatedly. Verify that icons in the active window (or desktop if no window is open) are selected in lexicographic order. + * Clear selection. Press \` repeatedly. Verify that icons in the active window (or desktop if no window is open) are selected in lexicographic order, starting with first in lexicographic order. * Select an icon. Press \`. Verify that the next icon in the active window (or desktop if no window is open) in lexicographic order is selected. - * Clear selection. Press Shift+\` repeatedly. Verify that icons in the active window (or desktop if no window is open) are selected in reverse lexicographic order. + * Clear selection. Press Shift+\` repeatedly. Verify that icons in the active window (or desktop if no window is open) are selected in reverse lexicographic order, starting with last in lexicographic order. * Select an icon. Press Shift+\`. Verify that the previous icon in the active window (or desktop if no window is open) in lexicographic order is selected. * On a IIgs and a Platinum IIe: - * Clear selection. Press Shift+Tab repeatedly. Verify that icons in the active window (or desktop if no window is open) are selected in reverse lexicographic order. + * Clear selection. Press Shift+Tab repeatedly. Verify that icons in the active window (or desktop if no window is open) are selected in reverse lexicographic order, starting with last in lexicographic order. * Select an icon. Press Shift+Tab. Verify that the previous icon in the active window (or desktop if no window is open) in lexicographic order is selected. * Open a volume window containing no file icons. Clear selection by clicking on the desktop. Press Tab repeatedly. Verify selection does not change, scrollbars do not appear in the window, and DeskTop does not crash. Close the window. Verify that the volume icon is no longer dimmed. * Open a volume window containing no file icons. Clear selection by clicking on the desktop. Type 'Z' repeatedly. Verify selection does not change, scrollbars do not appear in the window, and DeskTop does not crash. Close the window. Verify that the volume icon is no longer dimmed. +* Launch DeskTop. Close all windows. Clear selection by clicking on the desktop. Press an arrow key. Verify that the first (non-Trash) volume icon is selected. +* Launch DeskTop. Close all windows. Select a volume icon. Press an arrow key. Verify that the next icon in the specified direction is selected, if any. If none, verify that selection remains unchanged. +* Launch DeskTop. Close all windows. Eject all disks, and verify that only the Trash icon remains. Clear selection by clicking on the desktop. Press an arrow key. Verify that the Trash icon is selected. + +* Launch DeskTop. Open a window. Select a volume icon. Press an arrow key. Verify that the first icon in the window is selected. +* Launch DeskTop. Open two windows, both containing icons. Select an icon in one window. Activate the other window without changing selection. Press an arrow key. Verify that the first icon in the active window is selected. +* Launch DeskTop. Open a window containing multiple icons. Select an icon in the window. Press an arrow key. Verify that the next icon in the specified direction is selected, if any. If none, verify that selection remains unchanged. + +* Launch DeskTop. Open one window containing icons, the other containing no icons. Select an icon in the first window. Activate the other window without changing selection. Press an arrow key. Verify that selection remains unchanged. +* Launch DeskTop. Open a window containing icons. Select a volume icon. Press an arrow key. Verify that selection remains unchanged. + +* Launch DeskTop. Open a window containing icons. View > by Date. Press the Right Arrow key. Verify that selection remains unchanged. Repeat with the Left Arrow key. +* Launch DeskTop. Open a window containing icons. View > by Date. Clear selection by clicking on the desktop. Press the Down Arrow key. Verify that the first icon in visual order is selected. Press the Down Arrow key again. Verify that the next icon in visual order is selected. Repeat, and verify that selection does not wrap around. +* Launch DeskTop. Open a window containing icons. View > by Date. Clear selection by clicking on the desktop. Press the Up Arrow key. Verify that the last icon in visual order is selected. Press the Up Arrow key again. Verify that the previous icon in visual order is selected. Repeat, and verify that selection does not wrap around. * Launch DeskTop. Open a volume containing no files. Verify that the default minimum window size is used - about 170px by 50px not counting title/scrollbars.