Skip to content

Commit 668c118

Browse files
committed
Add direct drag scroll settings
1 parent 4e042da commit 668c118

7 files changed

Lines changed: 207 additions & 43 deletions

File tree

OpenParsec/ParsecSDKBridge.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ enum CursorMode: Int {
1717
case direct
1818
}
1919

20+
enum DirectDragMode: Int {
21+
case scroll
22+
case drag
23+
}
24+
2025
enum RightClickPosition: Int {
2126
case firstFinger
2227
case middle

OpenParsec/ParsecViewController.swift

Lines changed: 143 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ class ParsecViewController: UIViewController, UIScrollViewDelegate {
2323
var lastLongPressPoint: CGPoint = CGPoint()
2424
var accumulatedDeltaX: Float = 0.0
2525
var accumulatedDeltaY: Float = 0.0
26+
var accumulatedWheelY: Float = 0.0
27+
let directScrollDivisor: Float = 3.0
28+
let directLongPressDragThreshold: CGFloat = 8.0
29+
var directLongPressActive = false
30+
var directLongPressStartPoint: CGPoint = .zero
31+
var directLongPressDidMove = false
32+
var directLongPressDragging = false
2633
var lastPanLocation: CGPoint = .zero
2734
var lastPanTranslation: CGPoint = .zero
2835

@@ -468,16 +475,8 @@ extension ParsecViewController: UIGestureRecognizerDelegate {
468475
// lock activatedPanFingerNumber in case user not releasing both finger at the same time
469476
if gestureRecognizer.numberOfTouches == 0 {
470477
if gestureRecognizer.state == .ended || gestureRecognizer.state == .cancelled {
471-
activatedPanFingerNumber = 0
472-
// Reset accumulators
473-
accumulatedDeltaX = 0.0
474-
accumulatedDeltaY = 0.0
475-
lastPanTranslation = .zero
476-
477-
if SettingsHandler.cursorMode == .direct {
478-
let button = ParsecMouseButton.init(rawValue: 1)
479-
CParsec.sendMouseClickMessage(button, false)
480-
}
478+
resetPanTracking()
479+
releaseDirectDragIfNeeded()
481480
}
482481
} else if activatedPanFingerNumber == 2 || (gestureRecognizer.numberOfTouches == 2 && activatedPanFingerNumber == 0) {
483482
// Native UIScrollView handles 2-finger pan for scrolling.
@@ -500,52 +499,105 @@ extension ParsecViewController: UIGestureRecognizerDelegate {
500499
}
501500
} else if activatedPanFingerNumber == 1 || (gestureRecognizer.numberOfTouches == 1 && activatedPanFingerNumber == 0) {
502501
activatedPanFingerNumber = 1
503-
// move mouse
504502
if SettingsHandler.cursorMode == .direct {
505-
// Map screen tap to content coordinates
506-
let position = gestureRecognizer.location(in: gestureRecognizer.view)
507-
// Convert to content coordinates
508-
let adjustedPosition = contentView.convert(position, from: view)
509-
CParsec.sendMousePosition(Int32(adjustedPosition.x), Int32(adjustedPosition.y))
503+
if SettingsHandler.directDragMode == .scroll {
504+
handleDirectPanAsScroll(gestureRecognizer)
505+
} else {
506+
handleDirectPanAsDrag(gestureRecognizer)
507+
}
510508
} else {
511-
// Simple translation-based movement with sub-pixel accumulation
512-
let currentTranslation = gestureRecognizer.translation(in: gestureRecognizer.view)
509+
handleTouchpadPan(gestureRecognizer)
510+
}
513511

514-
if gestureRecognizer.state == .began {
515-
lastPanTranslation = .zero
516-
accumulatedDeltaX = 0.0
517-
accumulatedDeltaY = 0.0
518-
}
512+
}
513+
}
519514

520-
// Calculate delta since last update
521-
let deltaX = Float(currentTranslation.x - lastPanTranslation.x) * mouseSensitivity
522-
let deltaY = Float(currentTranslation.y - lastPanTranslation.y) * mouseSensitivity
515+
func resetPanTracking() {
516+
activatedPanFingerNumber = 0
517+
accumulatedDeltaX = 0.0
518+
accumulatedDeltaY = 0.0
519+
accumulatedWheelY = 0.0
520+
lastPanTranslation = .zero
521+
}
523522

524-
lastPanTranslation = currentTranslation
523+
func releaseDirectDragIfNeeded() {
524+
guard SettingsHandler.cursorMode == .direct else { return }
525+
if SettingsHandler.directDragMode == .drag || directLongPressDragging {
526+
let button = ParsecMouseButton.init(rawValue: 1)
527+
CParsec.sendMouseClickMessage(button, false)
528+
directLongPressDragging = false
529+
}
530+
}
525531

526-
// Accumulate for sub-pixel precision
527-
accumulatedDeltaX += deltaX
528-
accumulatedDeltaY += deltaY
532+
func handleDirectPanAsDrag(_ gestureRecognizer: UIPanGestureRecognizer) {
533+
moveDirectMouse(to: gestureRecognizer.location(in: gestureRecognizer.view))
529534

530-
// Send movement when we have at least 1 pixel
531-
let intDeltaX = Int32(accumulatedDeltaX)
532-
let intDeltaY = Int32(accumulatedDeltaY)
535+
if gestureRecognizer.state == .began {
536+
let button = ParsecMouseButton.init(rawValue: 1)
537+
CParsec.sendMouseClickMessage(button, true)
538+
}
539+
}
533540

534-
if intDeltaX != 0 || intDeltaY != 0 {
535-
CParsec.sendMouseDelta(intDeltaX, intDeltaY)
536-
accumulatedDeltaX -= Float(intDeltaX)
537-
accumulatedDeltaY -= Float(intDeltaY)
538-
}
539-
}
541+
func handleDirectPanAsScroll(_ gestureRecognizer: UIPanGestureRecognizer) {
542+
if directLongPressActive || directLongPressDragging {
543+
return
544+
}
540545

541-
if gestureRecognizer.state == .began && SettingsHandler.cursorMode == .direct {
542-
let button = ParsecMouseButton.init(rawValue: 1)
543-
CParsec.sendMouseClickMessage(button, true)
544-
}
546+
if gestureRecognizer.state == .began {
547+
moveDirectMouse(to: gestureRecognizer.location(in: gestureRecognizer.view))
548+
lastPanTranslation = .zero
549+
accumulatedWheelY = 0.0
550+
}
551+
552+
let currentTranslation = gestureRecognizer.translation(in: gestureRecognizer.view)
553+
let deltaY = Float(currentTranslation.y - lastPanTranslation.y) * mouseSensitivity
554+
lastPanTranslation = currentTranslation
555+
556+
accumulatedWheelY += deltaY / directScrollDivisor
557+
let wheelY = Int32(accumulatedWheelY)
558+
559+
if wheelY != 0 {
560+
CParsec.sendWheelMsg(x: 0, y: wheelY)
561+
accumulatedWheelY -= Float(wheelY)
562+
}
563+
}
564+
565+
func handleTouchpadPan(_ gestureRecognizer: UIPanGestureRecognizer) {
566+
// Simple translation-based movement with sub-pixel accumulation
567+
let currentTranslation = gestureRecognizer.translation(in: gestureRecognizer.view)
568+
569+
if gestureRecognizer.state == .began {
570+
lastPanTranslation = .zero
571+
accumulatedDeltaX = 0.0
572+
accumulatedDeltaY = 0.0
573+
}
574+
575+
// Calculate delta since last update
576+
let deltaX = Float(currentTranslation.x - lastPanTranslation.x) * mouseSensitivity
577+
let deltaY = Float(currentTranslation.y - lastPanTranslation.y) * mouseSensitivity
578+
579+
lastPanTranslation = currentTranslation
545580

581+
// Accumulate for sub-pixel precision
582+
accumulatedDeltaX += deltaX
583+
accumulatedDeltaY += deltaY
584+
585+
// Send movement when we have at least 1 pixel
586+
let intDeltaX = Int32(accumulatedDeltaX)
587+
let intDeltaY = Int32(accumulatedDeltaY)
588+
589+
if intDeltaX != 0 || intDeltaY != 0 {
590+
CParsec.sendMouseDelta(intDeltaX, intDeltaY)
591+
accumulatedDeltaX -= Float(intDeltaX)
592+
accumulatedDeltaY -= Float(intDeltaY)
546593
}
547594
}
548595

596+
func moveDirectMouse(to position: CGPoint) {
597+
let adjustedPosition = contentView.convert(position, from: view)
598+
CParsec.sendMousePosition(Int32(adjustedPosition.x), Int32(adjustedPosition.y))
599+
}
600+
549601
@objc func handleSingleFingerTap(_ gestureRecognizer: UITapGestureRecognizer) {
550602

551603
let location = gestureRecognizer.location(in: gestureRecognizer.view)
@@ -574,6 +626,11 @@ extension ParsecViewController: UIGestureRecognizerDelegate {
574626
}
575627

576628
@objc func handleLongPress(_ gestureRecognizer: UIGestureRecognizer) {
629+
if SettingsHandler.cursorMode == .direct && SettingsHandler.directDragMode == .scroll {
630+
handleDirectLongPressDrag(gestureRecognizer)
631+
return
632+
}
633+
577634
if SettingsHandler.cursorMode != .touchpad {
578635
return
579636
}
@@ -596,6 +653,49 @@ extension ParsecViewController: UIGestureRecognizerDelegate {
596653
}
597654
}
598655

656+
func handleDirectLongPressDrag(_ gestureRecognizer: UIGestureRecognizer) {
657+
let button = ParsecMouseButton.init(rawValue: 1)
658+
let location = gestureRecognizer.location(in: gestureRecognizer.view)
659+
660+
if gestureRecognizer.state == .began {
661+
directLongPressActive = true
662+
directLongPressStartPoint = location
663+
directLongPressDidMove = false
664+
directLongPressDragging = false
665+
moveDirectMouse(to: location)
666+
} else if gestureRecognizer.state == .changed {
667+
let deltaX = location.x - directLongPressStartPoint.x
668+
let deltaY = location.y - directLongPressStartPoint.y
669+
let distance = hypot(deltaX, deltaY)
670+
671+
if !directLongPressDragging && distance >= directLongPressDragThreshold {
672+
directLongPressDidMove = true
673+
directLongPressDragging = true
674+
moveDirectMouse(to: directLongPressStartPoint)
675+
CParsec.sendMouseClickMessage(button, true)
676+
}
677+
678+
if directLongPressDragging {
679+
moveDirectMouse(to: location)
680+
}
681+
} else if gestureRecognizer.state == .ended || gestureRecognizer.state == .cancelled || gestureRecognizer.state == .failed {
682+
if directLongPressDragging {
683+
CParsec.sendMouseClickMessage(button, false)
684+
} else if gestureRecognizer.state == .ended && !directLongPressDidMove {
685+
handleDirectLongPressRightClick(at: location)
686+
}
687+
688+
directLongPressActive = false
689+
directLongPressDragging = false
690+
directLongPressDidMove = false
691+
}
692+
}
693+
694+
func handleDirectLongPressRightClick(at location: CGPoint) {
695+
let adjustedLocation = contentView.convert(location, from: view)
696+
touchController.onTap(typeOfTap: 3, location: adjustedLocation)
697+
}
698+
599699
// UIScrollViewDelegate
600700
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
601701
return contentView

OpenParsec/SettingsHandler.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ struct SettingsHandler {
77
@AppStorage("bitrate") public static var bitrate: Int = 0
88
@AppStorage("decoder") public static var decoder: DecoderPref = .h264
99
@AppStorage("cursorMode") public static var cursorMode: CursorMode = .touchpad
10+
@AppStorage("directDragMode") public static var directDragMode: DirectDragMode = .scroll
1011
@AppStorage("cursorScale") public static var cursorScale: Double = 0.5
1112
@AppStorage("mouseSensitivity") public static var mouseSensitivity: Double = 1.0
1213
@AppStorage("noOverlay") public static var noOverlay: Bool = false

OpenParsec/SettingsView.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ struct SettingsView: View {
88
@AppStorage("bitrate") var bitrate: Int = 0
99
@AppStorage("decoder") var decoder: DecoderPref = .h264
1010
@AppStorage("cursorMode") var cursorMode: CursorMode = .touchpad
11+
@AppStorage("directDragMode") var directDragMode: DirectDragMode = .scroll
1112
@AppStorage("cursorScale") var cursorScale: Double = 0.5
1213
@AppStorage("mouseSensitivity") var mouseSensitivity: Double = 1.0
1314
@AppStorage("noOverlay") var noOverlay: Bool = false
@@ -76,6 +77,13 @@ struct SettingsView: View {
7677
Choice("Direct", CursorMode.direct)
7778
])
7879
}
80+
CatItem("Direct Drag") {
81+
MultiPicker(selection: $directDragMode, options:
82+
[
83+
Choice("Scroll", DirectDragMode.scroll),
84+
Choice("Drag", DirectDragMode.drag)
85+
])
86+
}
7987
CatItem("Right Click Position") {
8088
MultiPicker(selection: $rightClickPosition, options:
8189
[

OpenParsec/zh-Hans.lproj/Localizable.strings

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
"Mouse Movement" = "鼠标移动";
3434
"Touchpad" = "触控板";
3535
"Direct" = "直接";
36+
"Direct Drag" = "直接模式拖动";
37+
"Scroll" = "滚动";
38+
"Drag" = "拖动";
3639
"Right Click Position" = "右键位置";
3740
"First Finger" = "第一根手指";
3841
"Middle" = "中间";

scripts/check_input_preferences.sh

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#!/bin/sh
2+
set -eu
3+
4+
missing=0
5+
6+
require_in_file() {
7+
pattern="$1"
8+
file="$2"
9+
if ! /usr/bin/grep -Fq "$pattern" "$file"; then
10+
echo "Missing '$pattern' in $file"
11+
missing=1
12+
fi
13+
}
14+
15+
forbid_in_file() {
16+
pattern="$1"
17+
file="$2"
18+
if /usr/bin/grep -Fq "$pattern" "$file"; then
19+
echo "Unexpected '$pattern' in $file"
20+
missing=1
21+
fi
22+
}
23+
24+
require_in_file "enum DirectDragMode" "OpenParsec/ParsecSDKBridge.swift"
25+
require_in_file "@AppStorage(\"directDragMode\") public static var directDragMode: DirectDragMode = .scroll" "OpenParsec/SettingsHandler.swift"
26+
require_in_file "CatItem(\"Direct Drag\")" "OpenParsec/SettingsView.swift"
27+
require_in_file "handleDirectPanAsScroll" "OpenParsec/ParsecViewController.swift"
28+
require_in_file "directScrollDivisor" "OpenParsec/ParsecViewController.swift"
29+
require_in_file "directLongPressActive" "OpenParsec/ParsecViewController.swift"
30+
require_in_file "directLongPressStartPoint" "OpenParsec/ParsecViewController.swift"
31+
require_in_file "directLongPressDidMove" "OpenParsec/ParsecViewController.swift"
32+
require_in_file "handleDirectLongPressRightClick" "OpenParsec/ParsecViewController.swift"
33+
require_in_file "\"Direct Drag\"" "OpenParsec/zh-Hans.lproj/Localizable.strings"
34+
35+
forbid_in_file "autoShowKeyboardOnTap" "OpenParsec/SettingsHandler.swift"
36+
forbid_in_file "autoShowKeyboardOnTap" "OpenParsec/SettingsView.swift"
37+
forbid_in_file "showKeyboardIfEnabledAfterTap" "OpenParsec/ParsecViewController.swift"
38+
forbid_in_file "\"Auto Show Keyboard\"" "OpenParsec/zh-Hans.lproj/Localizable.strings"
39+
40+
if [ "$missing" -ne 0 ]; then
41+
exit 1
42+
fi
43+
44+
echo "Input preference implementation contains required hooks."

scripts/check_localizations.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ Interactivity
5050
Mouse Movement
5151
Touchpad
5252
Direct
53+
Direct Drag
54+
Scroll
55+
Drag
5356
Right Click Position
5457
First Finger
5558
Middle

0 commit comments

Comments
 (0)