commit 509e82ff4e5729f2fef2cfcd2da0422e0c0632a4
parent f3c807105ab2dba85d4e532e0b22a4a9be870adc
Author: Michael Camilleri <[email protected]>
Date: Sat, 18 Apr 2026 07:26:44 +0900
Add further keyboard shortcuts
On Magic Keyboards for iPads, there are no Page-Up/Page-Down/Home/End
keys. This commit adds alernative keys to support those input methods.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Diffstat:
6 files changed, 87 insertions(+), 32 deletions(-)
diff --git a/Listless.xcodeproj/xcshareddata/xcschemes/Listless macOS.xcscheme b/Listless.xcodeproj/xcshareddata/xcschemes/Listless macOS.xcscheme
@@ -4,8 +4,7 @@
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
- buildImplicitDependencies = "YES"
- runPostActionsOnFailure = "NO">
+ buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
@@ -16,7 +15,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "0FB4F07A37999BBC6DFE4DBB"
- BuildableName = "Listless macOS.app"
+ BuildableName = "Listless.app"
BlueprintName = "Listless macOS"
ReferencedContainer = "container:Listless.xcodeproj">
</BuildableReference>
@@ -34,7 +33,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "0FB4F07A37999BBC6DFE4DBB"
- BuildableName = "Listless macOS.app"
+ BuildableName = "Listless.app"
BlueprintName = "Listless macOS"
ReferencedContainer = "container:Listless.xcodeproj">
</BuildableReference>
@@ -45,13 +44,12 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
- shouldUseLaunchSchemeArgsEnv = "YES"
- onlyGenerateCoverageForSpecifiedTargets = "NO">
+ shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "0FB4F07A37999BBC6DFE4DBB"
- BuildableName = "Listless macOS.app"
+ BuildableName = "Listless.app"
BlueprintName = "Listless macOS"
ReferencedContainer = "container:Listless.xcodeproj">
</BuildableReference>
@@ -82,8 +80,6 @@
</BuildableReference>
</TestableReference>
</Testables>
- <CommandLineArguments>
- </CommandLineArguments>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
@@ -100,13 +96,11 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "0FB4F07A37999BBC6DFE4DBB"
- BuildableName = "Listless macOS.app"
+ BuildableName = "Listless.app"
BlueprintName = "Listless macOS"
ReferencedContainer = "container:Listless.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
- <CommandLineArguments>
- </CommandLineArguments>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
@@ -119,13 +113,11 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "0FB4F07A37999BBC6DFE4DBB"
- BuildableName = "Listless macOS.app"
+ BuildableName = "Listless.app"
BlueprintName = "Listless macOS"
ReferencedContainer = "container:Listless.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
- <CommandLineArguments>
- </CommandLineArguments>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
diff --git a/Listless.xcodeproj/xcshareddata/xcschemes/Listless watchOS.xcscheme b/Listless.xcodeproj/xcshareddata/xcschemes/Listless watchOS.xcscheme
@@ -4,8 +4,7 @@
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
- buildImplicitDependencies = "YES"
- runPostActionsOnFailure = "NO">
+ buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
@@ -16,7 +15,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "9BDC1B2175AB9CE26790448D"
- BuildableName = "Listless watchOS.app"
+ BuildableName = "Listless.app"
BlueprintName = "Listless watchOS"
ReferencedContainer = "container:Listless.xcodeproj">
</BuildableReference>
@@ -34,7 +33,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "9BDC1B2175AB9CE26790448D"
- BuildableName = "Listless watchOS.app"
+ BuildableName = "Listless.app"
BlueprintName = "Listless watchOS"
ReferencedContainer = "container:Listless.xcodeproj">
</BuildableReference>
@@ -45,21 +44,19 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
- shouldUseLaunchSchemeArgsEnv = "YES"
- onlyGenerateCoverageForSpecifiedTargets = "NO">
+ shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "9BDC1B2175AB9CE26790448D"
- BuildableName = "Listless watchOS.app"
+ BuildableName = "Listless.app"
BlueprintName = "Listless watchOS"
ReferencedContainer = "container:Listless.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
- skipped = "NO"
- parallelizable = "NO">
+ skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "FAC694EFB69F0D9F12557137"
@@ -69,8 +66,6 @@
</BuildableReference>
</TestableReference>
</Testables>
- <CommandLineArguments>
- </CommandLineArguments>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
@@ -87,13 +82,11 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "9BDC1B2175AB9CE26790448D"
- BuildableName = "Listless watchOS.app"
+ BuildableName = "Listless.app"
BlueprintName = "Listless watchOS"
ReferencedContainer = "container:Listless.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
- <CommandLineArguments>
- </CommandLineArguments>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
@@ -106,13 +99,11 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "9BDC1B2175AB9CE26790448D"
- BuildableName = "Listless watchOS.app"
+ BuildableName = "Listless.app"
BlueprintName = "Listless watchOS"
ReferencedContainer = "container:Listless.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
- <CommandLineArguments>
- </CommandLineArguments>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
diff --git a/ListlessiOS/Helpers/AppCommands.swift b/ListlessiOS/Helpers/AppCommands.swift
@@ -17,6 +17,10 @@ final class IOSMenuCoordinator {
var moveUp: (() -> Void)?
var moveDown: (() -> Void)?
var markCompleted: (() -> Void)?
+ var navigatePageUp: (() -> Void)?
+ var navigatePageDown: (() -> Void)?
+ var navigateToFirst: (() -> Void)?
+ var navigateToLast: (() -> Void)?
// Enabled state — read by KeyCaptureView in validate(_:).
var canDelete = false
@@ -38,6 +42,10 @@ enum IOSMenuSelectors {
static let moveUp = #selector(IOSMenuActions.handleMoveUp)
static let moveDown = #selector(IOSMenuActions.handleMoveDown)
static let markCompleted = #selector(IOSMenuActions.handleMarkCompleted)
+ static let navigatePageUp = #selector(IOSMenuActions.handleNavigatePageUp)
+ static let navigatePageDown = #selector(IOSMenuActions.handleNavigatePageDown)
+ static let navigateToFirst = #selector(IOSMenuActions.handleNavigateToFirst)
+ static let navigateToLast = #selector(IOSMenuActions.handleNavigateToLast)
}
/// Protocol declaring the `@objc` action methods so selectors can be
@@ -48,4 +56,8 @@ enum IOSMenuSelectors {
func handleMoveUp()
func handleMoveDown()
func handleMarkCompleted()
+ func handleNavigatePageUp()
+ func handleNavigatePageDown()
+ func handleNavigateToFirst()
+ func handleNavigateToLast()
}
diff --git a/ListlessiOS/Helpers/KeyCommandBridge.swift b/ListlessiOS/Helpers/KeyCommandBridge.swift
@@ -144,6 +144,22 @@ struct KeyCommandBridge: UIViewRepresentable {
IOSMenuCoordinator.shared.markCompleted?()
}
+ @objc func handleNavigatePageUp() {
+ IOSMenuCoordinator.shared.navigatePageUp?()
+ }
+
+ @objc func handleNavigatePageDown() {
+ IOSMenuCoordinator.shared.navigatePageDown?()
+ }
+
+ @objc func handleNavigateToFirst() {
+ IOSMenuCoordinator.shared.navigateToFirst?()
+ }
+
+ @objc func handleNavigateToLast() {
+ IOSMenuCoordinator.shared.navigateToLast?()
+ }
+
// MARK: - Menu validation
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
@@ -158,6 +174,11 @@ struct KeyCommandBridge: UIViewRepresentable {
return isActive && IOSMenuCoordinator.shared.canMoveDown
case IOSMenuSelectors.markCompleted:
return isActive && IOSMenuCoordinator.shared.canMarkCompleted
+ case IOSMenuSelectors.navigatePageUp,
+ IOSMenuSelectors.navigatePageDown,
+ IOSMenuSelectors.navigateToFirst,
+ IOSMenuSelectors.navigateToLast:
+ return isActive
default:
return super.canPerformAction(action, withSender: sender)
}
diff --git a/ListlessiOS/ListlessiOSApp.swift b/ListlessiOS/ListlessiOSApp.swift
@@ -69,6 +69,41 @@ class IOSAppDelegate: UIResponder, UIApplicationDelegate {
]),
atEndOfMenu: .edit
)
+
+ // Edit menu — Page Up (⌥↑), Page Down (⌥↓),
+ // Jump to Top (⌘⌥↑), Jump to Bottom (⌘⌥↓).
+ // Provides Magic Keyboard users an alternative to the absent
+ // Page/Home/End keys.
+ let pageUp = UIKeyCommand(
+ title: "Page Up",
+ action: IOSMenuSelectors.navigatePageUp,
+ input: UIKeyCommand.inputUpArrow,
+ modifierFlags: .alternate
+ )
+ let pageDown = UIKeyCommand(
+ title: "Page Down",
+ action: IOSMenuSelectors.navigatePageDown,
+ input: UIKeyCommand.inputDownArrow,
+ modifierFlags: .alternate
+ )
+ let jumpToTop = UIKeyCommand(
+ title: "Jump to Top",
+ action: IOSMenuSelectors.navigateToFirst,
+ input: UIKeyCommand.inputUpArrow,
+ modifierFlags: [.command, .alternate]
+ )
+ let jumpToBottom = UIKeyCommand(
+ title: "Jump to Bottom",
+ action: IOSMenuSelectors.navigateToLast,
+ input: UIKeyCommand.inputDownArrow,
+ modifierFlags: [.command, .alternate]
+ )
+ builder.insertChild(
+ UIMenu(title: "", options: .displayInline, children: [
+ pageUp, pageDown, jumpToTop, jumpToBottom,
+ ]),
+ atEndOfMenu: .edit
+ )
}
}
diff --git a/ListlessiOS/Views/ItemListView.swift b/ListlessiOS/Views/ItemListView.swift
@@ -139,6 +139,10 @@ struct ItemListView: View, ItemListViewProtocol {
coord.moveUp = { moveSelectedItemUp() }
coord.moveDown = { moveSelectedItemDown() }
coord.markCompleted = { markSelectedItemCompleted() }
+ coord.navigatePageUp = { _ = navigatePageUp() }
+ coord.navigatePageDown = { _ = navigatePageDown() }
+ coord.navigateToFirst = { _ = navigateToFirst() }
+ coord.navigateToLast = { _ = navigateToLast() }
let inNavMode = focusedField == .scrollView
coord.canDelete = fState.selectedItemID != nil && inNavMode
coord.canMoveUp = canMoveSelectionUp