PadXcode-iPad/Editor/HardwareKeyboardCommandHandler.swift

156 lines
7.8 KiB
Swift
Raw Normal View History

2026-04-12 00:46:30 -07:00
import UIKit
import SwiftUI
import Runestone
final class EditorCommandHandler: UIViewController {
weak var textView: Runestone.TextView?
var onSave: (() -> Void)?
var onBuild: (() -> Void)?
var onRun: (() -> Void)?
var onToggleNavigator: (() -> Void)?
var onToggleConsole: (() -> Void)?
var onFind: (() -> Void)?
var onJumpToLine: (() -> Void)?
var onFindInProject: (() -> Void)?
override var canBecomeFirstResponder: Bool { true }
override var keyCommands: [UIKeyCommand]? {
let cmds: [(String, String, UIKeyModifierFlags)] = [
("Save", "s", .command),
("Undo", "z", .command),
("Redo", "z", [.command, .shift]),
("Indent", "]", .command),
("Unindent", "[", .command),
("Toggle Comment", "/", .command),
("Move Line Up", UIKeyCommand.inputUpArrow, [.command, .alternate]),
("Move Line Down", UIKeyCommand.inputDownArrow, [.command, .alternate]),
("Duplicate Line", "d", [.command, .shift]),
("Delete Line", "k", [.command, .shift]),
("Find", "f", .command),
("Find in Project", "f", [.command, .shift]),
("Find Next", "g", .command),
("Find Prev", "g", [.command, .shift]),
("Jump to Line", "l", .command),
("Top", UIKeyCommand.inputUpArrow, .command),
("Bottom", UIKeyCommand.inputDownArrow, .command),
("Build", "b", .command),
("Run", "r", .command),
("Navigator", "0", .command),
("Console", "y", [.command, .shift]),
("Font+", "+", .command),
("Font-", "-", .command),
]
return cmds.map { title, input, mods in
let sel = selectorFor(title)
let kc = UIKeyCommand(title: title, action: sel, input: input, modifierFlags: mods, discoverabilityTitle: title)
if [UIKeyCommand.inputUpArrow, UIKeyCommand.inputDownArrow].contains(input) {
kc.wantsPriorityOverSystemBehavior = true
}
return kc
}
}
private func selectorFor(_ title: String) -> Selector {
switch title {
case "Save": return #selector(cmdSave)
case "Undo": return #selector(cmdUndo)
case "Redo": return #selector(cmdRedo)
case "Indent": return #selector(cmdIndent)
case "Unindent": return #selector(cmdUnindent)
case "Toggle Comment": return #selector(cmdComment)
case "Move Line Up": return #selector(cmdMoveUp)
case "Move Line Down": return #selector(cmdMoveDown)
case "Duplicate Line": return #selector(cmdDuplicate)
case "Delete Line": return #selector(cmdDeleteLine)
case "Find": return #selector(cmdFind)
case "Find in Project": return #selector(cmdFindProject)
case "Find Next": return #selector(cmdFindNext)
case "Find Prev": return #selector(cmdFindPrev)
case "Jump to Line": return #selector(cmdJumpToLine)
case "Top": return #selector(cmdTop)
case "Bottom": return #selector(cmdBottom)
case "Build": return #selector(cmdBuild)
case "Run": return #selector(cmdRun)
case "Navigator": return #selector(cmdNavigator)
case "Console": return #selector(cmdConsole)
case "Font+": return #selector(cmdFontBigger)
default: return #selector(cmdFontSmaller)
}
}
@objc func cmdSave() { onSave?() }
@objc func cmdUndo() { textView?.undoManager?.undo() }
@objc func cmdRedo() { textView?.undoManager?.redo() }
@objc func cmdIndent() { textView?.shiftRight() }
@objc func cmdUnindent() { textView?.shiftLeft() }
@objc func cmdFind() { onFind?() }
@objc func cmdFindProject(){ onFindInProject?() }
@objc func cmdFindNext() {}
@objc func cmdFindPrev() {}
@objc func cmdJumpToLine() { onJumpToLine?() }
@objc func cmdBuild() { onBuild?() }
@objc func cmdRun() { onRun?() }
@objc func cmdNavigator() { onToggleNavigator?() }
@objc func cmdConsole() { onToggleConsole?() }
@objc func cmdFontBigger() {}
@objc func cmdFontSmaller(){}
@objc func cmdComment() {
guard let tv = textView, let sel = tv.selectedRange else { return }
let text = tv.text as NSString; let lr = text.lineRange(for: sel)
let line = text.substring(with: lr); let trimmed = line.trimmingCharacters(in: .whitespaces)
let indent = String(line.prefix(while: { $0 == " " || $0 == "\t" }))
let new: String
if trimmed.hasPrefix("// ") { new = line.replacingFirstOccurrence(of: "// ", with: "") }
else if trimmed.hasPrefix("//") { new = line.replacingFirstOccurrence(of: "//", with: "") }
else { new = indent + "// " + line.dropFirst(indent.count) }
tv.replace(lr, withText: new)
}
@objc func cmdMoveUp() {
guard let tv = textView, let sel = tv.selectedRange else { return }
let text = tv.text as NSString; let cur = text.lineRange(for: sel)
guard cur.location > 0 else { return }
let prev = text.lineRange(for: NSRange(location: cur.location - 1, length: 0))
let curLine = text.substring(with: cur); let prevLine = text.substring(with: prev)
tv.replace(NSRange(location: prev.location, length: prev.length + cur.length), withText: curLine + prevLine)
tv.selectedRange = NSRange(location: prev.location + curLine.count - 1, length: 0)
}
@objc func cmdMoveDown() {
guard let tv = textView, let sel = tv.selectedRange else { return }
let text = tv.text as NSString; let cur = text.lineRange(for: sel)
let end = cur.location + cur.length; guard end < text.length else { return }
let next = text.lineRange(for: NSRange(location: end, length: 0))
let curLine = text.substring(with: cur); let nextLine = text.substring(with: next)
tv.replace(NSRange(location: cur.location, length: cur.length + next.length), withText: nextLine + curLine)
tv.selectedRange = NSRange(location: cur.location + next.length, length: 0)
}
@objc func cmdDuplicate() {
guard let tv = textView, let sel = tv.selectedRange else { return }
let text = tv.text as NSString; let lr = text.lineRange(for: sel)
let line = text.substring(with: lr)
tv.replace(NSRange(location: lr.location + lr.length, length: 0), withText: line)
}
@objc func cmdDeleteLine() {
guard let tv = textView, let sel = tv.selectedRange else { return }
let text = tv.text as NSString
tv.replace(text.lineRange(for: sel), withText: "")
}
@objc func cmdTop() {
textView?.selectedRange = NSRange(location: 0, length: 0)
textView?.scrollRangeToVisible(NSRange(location: 0, length: 0))
}
@objc func cmdBottom() {
guard let tv = textView else { return }
let end = NSRange(location: (tv.text as NSString).length, length: 0)
tv.selectedRange = end; tv.scrollRangeToVisible(end)
}
}