Skip to content

Commit

Permalink
Implement cursor lock (#23)
Browse files Browse the repository at this point in the history
* Implement cursor lock

* Bump version
  • Loading branch information
rohitsangwan01 authored Aug 18, 2024
1 parent e980397 commit d2816b9
Show file tree
Hide file tree
Showing 14 changed files with 671 additions and 5 deletions.
4 changes: 3 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"Cntrl",
"dylib",
"ffigen",
"Henkan",
"hotplug",
"intf",
"libblkid",
Expand All @@ -31,7 +32,8 @@
"Uhid",
"upgrader",
"Xinerama",
"xtest"
"xtest",
"Zenkaku"
],
"search.exclude": {
"**/lib/generated/": true,
Expand Down
13 changes: 13 additions & 0 deletions lib/app/client/client_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,19 @@ class ClientScreen extends ScreenInterface {
onMouseMove?.call(x, y);
}

@override
void mouseRelativeMove(int x, int y) {
var reportData = Uint8List(5);
reportData[0] = 0x02; // Report ID
reportData[1] = buttonPressed ?? 0; // Button state
reportData[2] = x; // X movement
reportData[3] = y; // Y movement
reportData[4] = 0; // Wheel movement
_addInputReport(reportData);
relativeX = x;
relativeY = y;
}

@override
void mouseWheel(int x, int y) {
int wheel = x != 0 ? x : y;
Expand Down
20 changes: 20 additions & 0 deletions lib/app/data/dialog_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,26 @@ class DialogHandler {
);
}

static void showSnackbar(String message) {
BuildContext? context = AppService.to.overlayContext;
if (context == null) {
logError("Navigator context is null: $message");
return;
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
message,
style: const TextStyle(color: Colors.white),
),
closeIconColor: Colors.white,
backgroundColor: Colors.orange,
duration: const Duration(seconds: 2),
showCloseIcon: true,
),
);
}

static void showInfoDialog({
required BuildContext context,
required String title,
Expand Down
6 changes: 6 additions & 0 deletions lib/app/data/info_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,9 @@ This mode requires ADB, and can also work over wireless ADB
Note: Both devices must be under same network
''';

String get lockMouseTileInfo => '''
Assign a hotkey to confine the mouse cursor within the bounds of a specific device.
Once locked, the mouse will only move relative to that device's screen.
Pressing the hotkey again will release the lock, allowing the mouse to move freely between devices.
''';
4 changes: 4 additions & 0 deletions lib/app/models/screen_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ class ScreenOptions {
bool? win32KeepForeground;
bool? clipboardSharing;
int? switchCornerSize;
String? toggleKeyStroke;

ScreenOptions({
this.relativeMouseMoves = false,
this.toggleKeyStroke,
this.win32KeepForeground,
this.clipboardSharing,
this.switchCornerSize,
Expand All @@ -18,6 +20,8 @@ class ScreenOptions {
'win32KeepForeground': win32KeepForeground,
if (clipboardSharing != null) 'clipboardSharing': clipboardSharing,
if (switchCornerSize != null) 'switchCornerSize': switchCornerSize,
if (toggleKeyStroke != null)
'keystroke($toggleKeyStroke)': '; lockCursorToScreen(toggle)'
};
}
}
29 changes: 28 additions & 1 deletion lib/app/modules/dashboard/dashboard_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ class _DashboardViewState extends State<DashboardView> with WindowListener {
SizedBox(
width: min(450, MediaQuery.sizeOf(context).width),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 20),
Watch((_) => _appService.userInternalServer.value
Expand All @@ -123,6 +124,7 @@ class _DashboardViewState extends State<DashboardView> with WindowListener {
: const SizedBox.shrink()),
const SizedBox(height: 20),
const _ClientTitleWidget(),
const _CursorLockedWidget(),
const SizedBox(height: 10),
const _ClientsListWidget(),
],
Expand All @@ -136,6 +138,31 @@ class _DashboardViewState extends State<DashboardView> with WindowListener {
}
}

class _CursorLockedWidget extends StatelessWidget {
const _CursorLockedWidget();

@override
Widget build(BuildContext context) {
final SynergyService synergyService = SynergyService.to;
return Watch((_) => synergyService.cursorLocked.value
? Row(
children: [
const SizedBox(width: 10),
Text(
'Cursor locked to current Screen',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(width: 10),
const Icon(
Icons.lock,
size: 14,
),
],
)
: const SizedBox.shrink());
}
}

class _ClientsListWidget extends StatelessWidget {
const _ClientsListWidget();

Expand Down Expand Up @@ -205,7 +232,7 @@ class _ClientTitleWidget extends StatelessWidget {
Icons.info_outline,
size: 18,
),
)
),
],
),
Row(
Expand Down
148 changes: 148 additions & 0 deletions lib/app/modules/setting/choose_hotkey.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import 'package:flutter/material.dart';
import 'package:uni_control_hub/app/synergy/synergy_key_types.dart';

class ChooseHotkey extends StatefulWidget {
const ChooseHotkey({super.key});

@override
State<ChooseHotkey> createState() => _ChooseHotkeyState();
}

class _ChooseHotkeyState extends State<ChooseHotkey> {
String? selectedKey;
String? selectedModifier;

String? get result {
if (selectedModifier != null && selectedKey == null) {
return '$selectedModifier+__';
} else if (selectedKey != null && selectedModifier == null) {
return selectedKey;
} else if (selectedKey == null && selectedModifier == null) {
return null;
} else {
return '$selectedModifier+$selectedKey';
}
}

void returnResult() {
String? finalResult;
// only use result if Key is not empty
if (selectedKey != null) {
finalResult = result;
}
Navigator.pop(context, finalResult);
}

FocusNode focusNode = FocusNode();

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Choose Hotkey'),
leading: IconButton(
onPressed: returnResult,
icon: const Icon(Icons.arrow_back_ios),
),
),
body: Column(
children: [
const SizedBox(height: 10),
Text('Result: ${result ?? '__'}'),
const SizedBox(height: 10),
const Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text('Modifier (Optional) '),
Text('Key (Required)'),
],
),
const Divider(),
Expanded(
child: Row(
children: [
Expanded(
child: ListView.builder(
itemCount: synergyKeyModifierList.length,
itemBuilder: (BuildContext context, int index) {
String key = synergyKeyModifierList[index];
return Card(
color: selectedModifier == key
? Theme.of(context).colorScheme.primaryContainer
: null,
child: ListTile(
title: Text(key),
onTap: () {
setState(() {
if (key == selectedModifier) {
selectedModifier = null;
} else {
selectedModifier = key;
}
});
},
),
);
},
),
),
const VerticalDivider(),
Expanded(
child: ListView.builder(
itemCount: synergyKeyList.length,
itemBuilder: (BuildContext context, int index) {
String key = synergyKeyList[index];
return Card(
color: selectedKey == key
? Theme.of(context).colorScheme.primaryContainer
: null,
child: ListTile(
title: Text(key),
onTap: () {
setState(() {
if (key == selectedKey) {
selectedKey = null;
} else {
selectedKey = key;
}
});
},
),
);
},
),
),
],
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () {
setState(() {
selectedKey = null;
selectedModifier = null;
});
returnResult();
},
child: const Text('Cancel'),
),
),
const SizedBox(width: 10),
Expanded(
child: ElevatedButton(
onPressed: returnResult,
child: const Text('Set'),
),
),
],
),
)
],
),
);
}
}
57 changes: 57 additions & 0 deletions lib/app/modules/setting/lock_mouse_tile.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
import 'package:flutter_settings_ui/flutter_settings_ui.dart';
import 'package:signals_flutter/signals_flutter.dart';
import 'package:uni_control_hub/app/data/dialog_handler.dart';
import 'package:uni_control_hub/app/data/info_data.dart';
import 'package:uni_control_hub/app/data/logger.dart';
import 'package:uni_control_hub/app/modules/setting/choose_hotkey.dart';
import 'package:uni_control_hub/app/services/synergy_service.dart';

class LockMouseTile extends StatelessWidget {
const LockMouseTile({super.key});

void selectHotkey(BuildContext context, SynergyService synergyService) async {
String? result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ChooseHotkey(),
),
);
logInfo('Result $result');
synergyService.toggleKeyStroke.value = result;
DialogHandler.showSuccess(
"Changes will take effect after you restart the server",
);
}

@override
Widget build(BuildContext context) {
final synergyService = SynergyService.to;

return Watch(
(_) => SettingsTile(
title: Row(
children: [
const Text('Lock Mouse Hotkey'),
const SizedBox(width: 10),
InkWell(
onTap: () {
DialogHandler.showInfoDialog(
context: context,
title: 'Lock Mouse to Device',
text: lockMouseTileInfo,
);
},
child: const Icon(Icons.info_outline, size: 19),
)
],
),
trailing: Text(
synergyService.toggleKeyStroke.value ?? "Select Hotkey",
),
onPressed: (context) => selectHotkey(context, synergyService),
leading: const Icon(Icons.ads_click),
),
);
}
}
6 changes: 5 additions & 1 deletion lib/app/modules/setting/settings_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_settings_ui/flutter_settings_ui.dart';
import 'package:signals_flutter/signals_flutter.dart';
import 'package:uni_control_hub/app/modules/setting/android_connection_mode_tile.dart';
import 'package:uni_control_hub/app/modules/setting/lock_mouse_tile.dart';
import 'package:uni_control_hub/app/modules/setting/uhid_port_tile.dart';
import 'package:uni_control_hub/app/services/app_service.dart';
import 'package:uni_control_hub/app/data/capabilities.dart';
Expand Down Expand Up @@ -46,7 +47,7 @@ class SettingsView extends StatelessWidget {
onToggle: (value) {
_appService.autoStartServer.value = value;
},
leading: const Icon(Icons.mouse),
leading: const Icon(Icons.dns),
)),
),
if (Capabilities.supportsBleConnection)
Expand All @@ -65,6 +66,9 @@ class SettingsView extends StatelessWidget {
SettingsSection(
title: const Text('Client'),
tiles: [
const CustomSettingsTile(
child: LockMouseTile(),
),
const CustomSettingsTile(
child: AndroidConnectionModeTile(),
),
Expand Down
5 changes: 5 additions & 0 deletions lib/app/services/storage_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ class StorageService {
set testStatus(String? value) =>
value == null ? _removeDb('testStatus') : _writeDb('testStatus', value);

String? get toggleKeyStroke => _readDb('toggleKeyStroke');
set toggleKeyStroke(String? value) => value == null
? _removeDb('toggleKeyStroke')
: _writeDb('toggleKeyStroke', value);

int get uhidPort => _readDb('uhidPort') ?? 9945;
set uhidPort(int value) => _writeDb('uhidPort', value);

Expand Down
Loading

0 comments on commit d2816b9

Please sign in to comment.