Compare commits
1 Commits
copilot/fi
...
hdie-tray
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a054de744a |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 969 B After Width: | Height: | Size: 511 B |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 989 B |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 755 B |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 989 B |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 4.4 KiB |
@@ -1124,23 +1124,18 @@ class CustomAlertDialog extends StatelessWidget {
|
||||
|
||||
Widget createDialogContent(String text) {
|
||||
final RegExp linkRegExp = RegExp(r'(https?://[^\s]+)');
|
||||
bool hasLink = linkRegExp.hasMatch(text);
|
||||
|
||||
// Early return: no link, use default theme color
|
||||
if (!hasLink) {
|
||||
return SelectableText(text, style: const TextStyle(fontSize: 15));
|
||||
}
|
||||
|
||||
final List<TextSpan> spans = [];
|
||||
int start = 0;
|
||||
bool hasLink = false;
|
||||
|
||||
linkRegExp.allMatches(text).forEach((match) {
|
||||
hasLink = true;
|
||||
if (match.start > start) {
|
||||
spans.add(TextSpan(text: text.substring(start, match.start)));
|
||||
}
|
||||
spans.add(TextSpan(
|
||||
text: match.group(0) ?? '',
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
color: Colors.blue,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
@@ -1158,9 +1153,13 @@ Widget createDialogContent(String text) {
|
||||
spans.add(TextSpan(text: text.substring(start)));
|
||||
}
|
||||
|
||||
if (!hasLink) {
|
||||
return SelectableText(text, style: const TextStyle(fontSize: 15));
|
||||
}
|
||||
|
||||
return SelectableText.rich(
|
||||
TextSpan(
|
||||
style: const TextStyle(fontSize: 15),
|
||||
style: TextStyle(color: Colors.black, fontSize: 15),
|
||||
children: spans,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -25,7 +25,6 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
|
||||
GestureDragStartCallback? onOneFingerPanStart;
|
||||
GestureDragUpdateCallback? onOneFingerPanUpdate;
|
||||
GestureDragEndCallback? onOneFingerPanEnd;
|
||||
GestureDragCancelCallback? onOneFingerPanCancel;
|
||||
|
||||
// twoFingerScale : scale + pan event
|
||||
GestureScaleStartCallback? onTwoFingerScaleStart;
|
||||
@@ -170,27 +169,6 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
|
||||
|
||||
DragEndDetails _getDragEndDetails(ScaleEndDetails d) =>
|
||||
DragEndDetails(velocity: d.velocity);
|
||||
|
||||
@override
|
||||
void rejectGesture(int pointer) {
|
||||
super.rejectGesture(pointer);
|
||||
switch (_currentState) {
|
||||
case GestureState.oneFingerPan:
|
||||
if (onOneFingerPanCancel != null) {
|
||||
onOneFingerPanCancel!();
|
||||
}
|
||||
break;
|
||||
case GestureState.twoFingerScale:
|
||||
// Reset scale state if needed, currently self-contained
|
||||
break;
|
||||
case GestureState.threeFingerVerticalDrag:
|
||||
// Reset drag state if needed, currently self-contained
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
_currentState = GestureState.none;
|
||||
}
|
||||
}
|
||||
|
||||
class HoldTapMoveGestureRecognizer extends GestureRecognizer {
|
||||
@@ -739,7 +717,6 @@ RawGestureDetector getMixinGestureDetector({
|
||||
GestureDragStartCallback? onOneFingerPanStart,
|
||||
GestureDragUpdateCallback? onOneFingerPanUpdate,
|
||||
GestureDragEndCallback? onOneFingerPanEnd,
|
||||
GestureDragCancelCallback? onOneFingerPanCancel,
|
||||
GestureScaleUpdateCallback? onTwoFingerScaleUpdate,
|
||||
GestureScaleEndCallback? onTwoFingerScaleEnd,
|
||||
GestureDragUpdateCallback? onThreeFingerVerticalDragUpdate,
|
||||
@@ -788,7 +765,6 @@ RawGestureDetector getMixinGestureDetector({
|
||||
..onOneFingerPanStart = onOneFingerPanStart
|
||||
..onOneFingerPanUpdate = onOneFingerPanUpdate
|
||||
..onOneFingerPanEnd = onOneFingerPanEnd
|
||||
..onOneFingerPanCancel = onOneFingerPanCancel
|
||||
..onTwoFingerScaleUpdate = onTwoFingerScaleUpdate
|
||||
..onTwoFingerScaleEnd = onTwoFingerScaleEnd
|
||||
..onThreeFingerVerticalDragUpdate = onThreeFingerVerticalDragUpdate;
|
||||
|
||||
@@ -107,8 +107,6 @@ class _RawTouchGestureDetectorRegionState
|
||||
// For mouse mode, we need to block the events when the cursor is in a blocked area.
|
||||
// So we need to cache the last tap down position.
|
||||
Offset? _lastTapDownPositionForMouseMode;
|
||||
// Cache global position for onTap (which lacks position info).
|
||||
Offset? _lastTapDownGlobalPosition;
|
||||
|
||||
FFI get ffi => widget.ffi;
|
||||
FfiModel get ffiModel => widget.ffiModel;
|
||||
@@ -138,7 +136,6 @@ class _RawTouchGestureDetectorRegionState
|
||||
|
||||
onTapDown(TapDownDetails d) async {
|
||||
lastDeviceKind = d.kind;
|
||||
_lastTapDownGlobalPosition = d.globalPosition;
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
@@ -157,16 +154,11 @@ class _RawTouchGestureDetectorRegionState
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
// Filter duplicate touch tap events on iOS (Magic Mouse issue).
|
||||
if (inputModel.shouldIgnoreTouchTap(d.globalPosition)) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
final isMoved =
|
||||
await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||
if (isMoved) {
|
||||
// If pan already handled 'down', don't send it again.
|
||||
if (lastTapDownDetails != null && !_touchModePanStarted) {
|
||||
if (lastTapDownDetails != null) {
|
||||
await inputModel.tapDown(MouseButtons.left);
|
||||
}
|
||||
await inputModel.tapUp(MouseButtons.left);
|
||||
@@ -178,11 +170,6 @@ class _RawTouchGestureDetectorRegionState
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
// Filter duplicate touch tap events on iOS (Magic Mouse issue).
|
||||
final lastPos = _lastTapDownGlobalPosition;
|
||||
if (lastPos != null && inputModel.shouldIgnoreTouchTap(lastPos)) {
|
||||
return;
|
||||
}
|
||||
if (!handleTouch) {
|
||||
// Cannot use `_lastTapDownDetails` because Flutter calls `onTapUp` before `onTap`, clearing the cached details.
|
||||
// Using `_lastTapDownPositionForMouseMode` instead.
|
||||
@@ -437,14 +424,6 @@ class _RawTouchGestureDetectorRegionState
|
||||
}
|
||||
}
|
||||
|
||||
// Reset `_touchModePanStarted` if the one-finger pan gesture is cancelled
|
||||
// or rejected by the gesture arena. Without this, the flag can remain
|
||||
// stuck in the "started" state and cause issues such as the Magic Mouse
|
||||
// double-click problem on iPad with magic mouse.
|
||||
onOneFingerPanCancel() {
|
||||
_touchModePanStarted = false;
|
||||
}
|
||||
|
||||
// scale + pan event
|
||||
onTwoFingerScaleStart(ScaleStartDetails d) {
|
||||
_lastTapDownDetails = null;
|
||||
@@ -578,7 +557,6 @@ class _RawTouchGestureDetectorRegionState
|
||||
instance
|
||||
..onOneFingerPanUpdate = onOneFingerPanUpdate
|
||||
..onOneFingerPanEnd = onOneFingerPanEnd
|
||||
..onOneFingerPanCancel = onOneFingerPanCancel
|
||||
..onTwoFingerScaleStart = onTwoFingerScaleStart
|
||||
..onTwoFingerScaleUpdate = onTwoFingerScaleUpdate
|
||||
..onTwoFingerScaleEnd = onTwoFingerScaleEnd
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
@@ -42,9 +41,6 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
final GlobalKey _keyboardKey = GlobalKey();
|
||||
double _keyboardHeight = 0;
|
||||
late bool _showTerminalExtraKeys;
|
||||
// For iOS edge swipe gesture
|
||||
double _swipeStartX = 0;
|
||||
double _swipeCurrentX = 0;
|
||||
|
||||
// For web only.
|
||||
// 'monospace' does not work on web, use Google Fonts, `??` is only for null safety.
|
||||
@@ -151,7 +147,7 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
}
|
||||
|
||||
Widget buildBody() {
|
||||
final scaffold = Scaffold(
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false, // Disable automatic layout adjustment; manually control UI updates to prevent flickering when the keyboard shows/hides
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
body: Stack(
|
||||
@@ -168,13 +164,6 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
autofocus: true,
|
||||
textStyle: _getTerminalStyle(),
|
||||
backgroundOpacity: 0.7,
|
||||
// The following comment is from xterm.dart source code:
|
||||
// Workaround to detect delete key for platforms and IMEs that do not
|
||||
// emit a hardware delete event. Preferred on mobile platforms. [false] by
|
||||
// default.
|
||||
//
|
||||
// Android works fine without this workaround.
|
||||
deleteDetection: isIOS,
|
||||
padding: _calculatePadding(heightPx),
|
||||
onSecondaryTapDown: (details, offset) async {
|
||||
final selection = _terminalModel.terminalController.selection;
|
||||
@@ -196,108 +185,9 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
),
|
||||
),
|
||||
if (_showTerminalExtraKeys) _buildFloatingKeyboard(),
|
||||
// iOS-style circular close button in top-right corner
|
||||
if (isIOS) _buildCloseButton(),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
// Add iOS edge swipe gesture to exit (similar to Android back button)
|
||||
if (isIOS) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final screenWidth = constraints.maxWidth;
|
||||
// Base thresholds on screen width but clamp to reasonable logical pixel ranges
|
||||
// Edge detection region: ~10% of width, clamped between 20 and 80 logical pixels
|
||||
final edgeThreshold = (screenWidth * 0.1).clamp(20.0, 80.0);
|
||||
// Required horizontal movement: ~25% of width, clamped between 80 and 300 logical pixels
|
||||
final swipeThreshold = (screenWidth * 0.25).clamp(80.0, 300.0);
|
||||
|
||||
return RawGestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
gestures: <Type, GestureRecognizerFactory>{
|
||||
HorizontalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>(
|
||||
() => HorizontalDragGestureRecognizer(
|
||||
debugOwner: this,
|
||||
// Only respond to touch input, exclude mouse/trackpad
|
||||
supportedDevices: kTouchBasedDeviceKinds,
|
||||
),
|
||||
(HorizontalDragGestureRecognizer instance) {
|
||||
instance
|
||||
// Capture initial touch-down position (before touch slop)
|
||||
..onDown = (details) {
|
||||
_swipeStartX = details.localPosition.dx;
|
||||
_swipeCurrentX = details.localPosition.dx;
|
||||
}
|
||||
..onUpdate = (details) {
|
||||
_swipeCurrentX = details.localPosition.dx;
|
||||
}
|
||||
..onEnd = (details) {
|
||||
// Check if swipe started from left edge and moved right
|
||||
if (_swipeStartX < edgeThreshold && (_swipeCurrentX - _swipeStartX) > swipeThreshold) {
|
||||
clientClose(sessionId, _ffi);
|
||||
}
|
||||
_swipeStartX = 0;
|
||||
_swipeCurrentX = 0;
|
||||
}
|
||||
..onCancel = () {
|
||||
_swipeStartX = 0;
|
||||
_swipeCurrentX = 0;
|
||||
};
|
||||
},
|
||||
),
|
||||
},
|
||||
child: scaffold,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return scaffold;
|
||||
}
|
||||
|
||||
Widget _buildCloseButton() {
|
||||
return Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: SafeArea(
|
||||
minimum: const EdgeInsets.only(
|
||||
top: 16, // iOS standard margin
|
||||
right: 16, // iOS standard margin
|
||||
),
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: translate('Close'),
|
||||
child: Container(
|
||||
width: 44, // iOS standard tap target size
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.5), // Half transparency
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
shape: const CircleBorder(),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
customBorder: const CircleBorder(),
|
||||
onTap: () {
|
||||
clientClose(sessionId, _ffi);
|
||||
},
|
||||
child: Tooltip(
|
||||
message: translate('Close'),
|
||||
child: const Icon(
|
||||
Icons.chevron_left, // iOS-style back arrow
|
||||
color: Colors.white,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFloatingKeyboard() {
|
||||
|
||||
@@ -59,8 +59,7 @@ class CanvasCoords {
|
||||
model.scale = json['scale'];
|
||||
model.scrollX = json['scrollX'];
|
||||
model.scrollY = json['scrollY'];
|
||||
model.scrollStyle =
|
||||
ScrollStyle.fromJson(json['scrollStyle'], ScrollStyle.scrollauto);
|
||||
model.scrollStyle = ScrollStyle.fromJson(json['scrollStyle'], ScrollStyle.scrollauto);
|
||||
model.size = Size(json['size']['w'], json['size']['h']);
|
||||
return model;
|
||||
}
|
||||
@@ -419,74 +418,6 @@ class InputModel {
|
||||
});
|
||||
}
|
||||
|
||||
// https://github.com/flutter/flutter/issues/157241
|
||||
// Infer CapsLock state from the character output.
|
||||
// This is needed because Flutter's HardwareKeyboard.lockModesEnabled may report
|
||||
// incorrect CapsLock state on iOS.
|
||||
bool _getIosCapsFromCharacter(KeyEvent e) {
|
||||
if (!isIOS) return false;
|
||||
final ch = e.character;
|
||||
return _getIosCapsFromCharacterImpl(
|
||||
ch, HardwareKeyboard.instance.isShiftPressed);
|
||||
}
|
||||
|
||||
// RawKeyEvent version of _getIosCapsFromCharacter.
|
||||
bool _getIosCapsFromRawCharacter(RawKeyEvent e) {
|
||||
if (!isIOS) return false;
|
||||
final ch = e.character;
|
||||
return _getIosCapsFromCharacterImpl(ch, e.isShiftPressed);
|
||||
}
|
||||
|
||||
// Shared implementation for inferring CapsLock state from character.
|
||||
// Uses Unicode-aware case detection to support non-ASCII letters (e.g., ü/Ü, é/É).
|
||||
//
|
||||
// Limitations:
|
||||
// 1. This inference assumes the client and server use the same keyboard layout.
|
||||
// If layouts differ (e.g., client uses EN, server uses DE), the character output
|
||||
// may not match expectations. For example, ';' on EN layout maps to 'ö' on DE
|
||||
// layout, making it impossible to correctly infer CapsLock state from the
|
||||
// character alone.
|
||||
// 2. On iOS, CapsLock+Shift produces uppercase letters (unlike desktop where it
|
||||
// produces lowercase). This method cannot handle that case correctly.
|
||||
bool _getIosCapsFromCharacterImpl(String? ch, bool shiftPressed) {
|
||||
if (ch == null || ch.length != 1) return false;
|
||||
// Use Dart's built-in Unicode-aware case detection
|
||||
final upper = ch.toUpperCase();
|
||||
final lower = ch.toLowerCase();
|
||||
final isUpper = upper == ch && lower != ch;
|
||||
final isLower = lower == ch && upper != ch;
|
||||
// Skip non-letter characters (e.g., numbers, symbols, CJK characters without case)
|
||||
if (!isUpper && !isLower) return false;
|
||||
return isUpper != shiftPressed;
|
||||
}
|
||||
|
||||
int _buildLockModes(bool iosCapsLock) {
|
||||
const capslock = 1;
|
||||
const numlock = 2;
|
||||
const scrolllock = 3;
|
||||
int lockModes = 0;
|
||||
if (isIOS) {
|
||||
if (iosCapsLock) {
|
||||
lockModes |= (1 << capslock);
|
||||
}
|
||||
// Ignore "NumLock/ScrollLock" on iOS for now.
|
||||
} else {
|
||||
if (HardwareKeyboard.instance.lockModesEnabled
|
||||
.contains(KeyboardLockMode.capsLock)) {
|
||||
lockModes |= (1 << capslock);
|
||||
}
|
||||
if (HardwareKeyboard.instance.lockModesEnabled
|
||||
.contains(KeyboardLockMode.numLock)) {
|
||||
lockModes |= (1 << numlock);
|
||||
}
|
||||
if (HardwareKeyboard.instance.lockModesEnabled
|
||||
.contains(KeyboardLockMode.scrollLock)) {
|
||||
lockModes |= (1 << scrolllock);
|
||||
}
|
||||
}
|
||||
return lockModes;
|
||||
}
|
||||
|
||||
// This function must be called after the peer info is received.
|
||||
// Because `sessionGetKeyboardMode` relies on the peer version.
|
||||
updateKeyboardMode() async {
|
||||
@@ -619,11 +550,6 @@ class InputModel {
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
bool iosCapsLock = false;
|
||||
if (isIOS && e is RawKeyDownEvent) {
|
||||
iosCapsLock = _getIosCapsFromRawCharacter(e);
|
||||
}
|
||||
|
||||
final key = e.logicalKey;
|
||||
if (e is RawKeyDownEvent) {
|
||||
if (!e.repeat) {
|
||||
@@ -660,7 +586,7 @@ class InputModel {
|
||||
|
||||
// * Currently mobile does not enable map mode
|
||||
if ((isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) {
|
||||
mapKeyboardModeRaw(e, iosCapsLock);
|
||||
mapKeyboardModeRaw(e);
|
||||
} else {
|
||||
legacyKeyboardModeRaw(e);
|
||||
}
|
||||
@@ -696,11 +622,6 @@ class InputModel {
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
bool iosCapsLock = false;
|
||||
if (isIOS && (e is KeyDownEvent || e is KeyRepeatEvent)) {
|
||||
iosCapsLock = _getIosCapsFromCharacter(e);
|
||||
}
|
||||
|
||||
if (e is KeyUpEvent) {
|
||||
handleKeyUpEventModifiers(e);
|
||||
} else if (e is KeyDownEvent) {
|
||||
@@ -746,8 +667,7 @@ class InputModel {
|
||||
e.character ?? '',
|
||||
e.physicalKey.usbHidUsage & 0xFFFF,
|
||||
// Show repeat event be converted to "release+press" events?
|
||||
e is KeyDownEvent || e is KeyRepeatEvent,
|
||||
iosCapsLock);
|
||||
e is KeyDownEvent || e is KeyRepeatEvent);
|
||||
} else {
|
||||
legacyKeyboardMode(e);
|
||||
}
|
||||
@@ -756,9 +676,23 @@ class InputModel {
|
||||
}
|
||||
|
||||
/// Send Key Event
|
||||
void newKeyboardMode(
|
||||
String character, int usbHid, bool down, bool iosCapsLock) {
|
||||
final lockModes = _buildLockModes(iosCapsLock);
|
||||
void newKeyboardMode(String character, int usbHid, bool down) {
|
||||
const capslock = 1;
|
||||
const numlock = 2;
|
||||
const scrolllock = 3;
|
||||
int lockModes = 0;
|
||||
if (HardwareKeyboard.instance.lockModesEnabled
|
||||
.contains(KeyboardLockMode.capsLock)) {
|
||||
lockModes |= (1 << capslock);
|
||||
}
|
||||
if (HardwareKeyboard.instance.lockModesEnabled
|
||||
.contains(KeyboardLockMode.numLock)) {
|
||||
lockModes |= (1 << numlock);
|
||||
}
|
||||
if (HardwareKeyboard.instance.lockModesEnabled
|
||||
.contains(KeyboardLockMode.scrollLock)) {
|
||||
lockModes |= (1 << scrolllock);
|
||||
}
|
||||
bind.sessionHandleFlutterKeyEvent(
|
||||
sessionId: sessionId,
|
||||
character: character,
|
||||
@@ -767,7 +701,7 @@ class InputModel {
|
||||
downOrUp: down);
|
||||
}
|
||||
|
||||
void mapKeyboardModeRaw(RawKeyEvent e, bool iosCapsLock) {
|
||||
void mapKeyboardModeRaw(RawKeyEvent e) {
|
||||
int positionCode = -1;
|
||||
int platformCode = -1;
|
||||
bool down;
|
||||
@@ -798,14 +732,27 @@ class InputModel {
|
||||
} else {
|
||||
down = false;
|
||||
}
|
||||
inputRawKey(
|
||||
e.character ?? '', platformCode, positionCode, down, iosCapsLock);
|
||||
inputRawKey(e.character ?? '', platformCode, positionCode, down);
|
||||
}
|
||||
|
||||
/// Send raw Key Event
|
||||
void inputRawKey(String name, int platformCode, int positionCode, bool down,
|
||||
bool iosCapsLock) {
|
||||
final lockModes = _buildLockModes(iosCapsLock);
|
||||
void inputRawKey(String name, int platformCode, int positionCode, bool down) {
|
||||
const capslock = 1;
|
||||
const numlock = 2;
|
||||
const scrolllock = 3;
|
||||
int lockModes = 0;
|
||||
if (HardwareKeyboard.instance.lockModesEnabled
|
||||
.contains(KeyboardLockMode.capsLock)) {
|
||||
lockModes |= (1 << capslock);
|
||||
}
|
||||
if (HardwareKeyboard.instance.lockModesEnabled
|
||||
.contains(KeyboardLockMode.numLock)) {
|
||||
lockModes |= (1 << numlock);
|
||||
}
|
||||
if (HardwareKeyboard.instance.lockModesEnabled
|
||||
.contains(KeyboardLockMode.scrollLock)) {
|
||||
lockModes |= (1 << scrolllock);
|
||||
}
|
||||
bind.sessionHandleFlutterRawKeyEvent(
|
||||
sessionId: sessionId,
|
||||
name: name,
|
||||
@@ -879,9 +826,6 @@ class InputModel {
|
||||
Map<String, dynamic> _getMouseEvent(PointerEvent evt, String type) {
|
||||
final Map<String, dynamic> out = {};
|
||||
|
||||
bool hasStaleButtonsOnMouseUp =
|
||||
type == _kMouseEventUp && evt.buttons == _lastButtons;
|
||||
|
||||
// Check update event type and set buttons to be sent.
|
||||
int buttons = _lastButtons;
|
||||
if (type == _kMouseEventMove) {
|
||||
@@ -906,7 +850,7 @@ class InputModel {
|
||||
buttons = evt.buttons;
|
||||
}
|
||||
}
|
||||
_lastButtons = hasStaleButtonsOnMouseUp ? 0 : evt.buttons;
|
||||
_lastButtons = evt.buttons;
|
||||
|
||||
out['buttons'] = buttons;
|
||||
out['type'] = type;
|
||||
@@ -1104,14 +1048,6 @@ class InputModel {
|
||||
if (isViewOnly && !showMyCursor) return;
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||
|
||||
// May fix https://github.com/rustdesk/rustdesk/issues/13009
|
||||
if (isIOS && e.synthesized && e.position == Offset.zero && e.buttons == 0) {
|
||||
// iOS may emit a synthesized hover event at (0,0) when the mouse is disconnected.
|
||||
// Ignore this event to prevent cursor jumping.
|
||||
debugPrint('Ignored synthesized hover at (0,0) on iOS');
|
||||
return;
|
||||
}
|
||||
|
||||
// Only update pointer region when relative mouse mode is enabled.
|
||||
// This avoids unnecessary tracking when not in relative mode.
|
||||
if (_relativeMouse.enabled.value) {
|
||||
@@ -1274,28 +1210,6 @@ class InputModel {
|
||||
_trackpadLastDelta = Offset.zero;
|
||||
}
|
||||
|
||||
// iOS Magic Mouse duplicate event detection.
|
||||
// When using Magic Mouse on iPad, iOS may emit both mouse and touch events
|
||||
// for the same click in certain areas (like top-left corner).
|
||||
int _lastMouseDownTimeMs = 0;
|
||||
ui.Offset _lastMouseDownPos = ui.Offset.zero;
|
||||
|
||||
/// Check if a touch tap event should be ignored because it's a duplicate
|
||||
/// of a recent mouse event (iOS Magic Mouse issue).
|
||||
bool shouldIgnoreTouchTap(ui.Offset pos) {
|
||||
if (!isIOS) return false;
|
||||
final nowMs = DateTime.now().millisecondsSinceEpoch;
|
||||
final dt = nowMs - _lastMouseDownTimeMs;
|
||||
final distance = (_lastMouseDownPos - pos).distance;
|
||||
// If touch tap is within 2000ms and 80px of the last mouse down,
|
||||
// it's likely a duplicate event from the same Magic Mouse click.
|
||||
if (dt >= 0 && dt < 2000 && distance < 80.0) {
|
||||
debugPrint("shouldIgnoreTouchTap: IGNORED (dt=$dt, dist=$distance)");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void onPointDownImage(PointerDownEvent e) {
|
||||
debugPrint("onPointDownImage ${e.kind}");
|
||||
_stopFling = true;
|
||||
@@ -1305,13 +1219,6 @@ class InputModel {
|
||||
if (isViewOnly && !showMyCursor) return;
|
||||
if (isViewCamera) return;
|
||||
|
||||
// Track mouse down events for duplicate detection on iOS.
|
||||
final nowMs = DateTime.now().millisecondsSinceEpoch;
|
||||
if (e.kind == ui.PointerDeviceKind.mouse) {
|
||||
_lastMouseDownTimeMs = nowMs;
|
||||
_lastMouseDownPos = e.position;
|
||||
}
|
||||
|
||||
if (_relativeMouse.enabled.value) {
|
||||
_relativeMouse.updatePointerRegionTopLeftGlobal(e);
|
||||
}
|
||||
@@ -1853,9 +1760,9 @@ class InputModel {
|
||||
// Simulate a key press event.
|
||||
// `usbHidUsage` is the USB HID usage code of the key.
|
||||
Future<void> tapHidKey(int usbHidUsage) async {
|
||||
newKeyboardMode(kKeyFlutterKey, usbHidUsage, true, false);
|
||||
newKeyboardMode(kKeyFlutterKey, usbHidUsage, true);
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
newKeyboardMode(kKeyFlutterKey, usbHidUsage, false, false);
|
||||
newKeyboardMode(kKeyFlutterKey, usbHidUsage, false);
|
||||
}
|
||||
|
||||
Future<void> onMobileVolumeUp() async =>
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
# Icon Regeneration Guide
|
||||
|
||||
This document describes how to regenerate app icons from the source icon files.
|
||||
|
||||
## Source Icons
|
||||
|
||||
- **Primary source**: `res/icon.png` (2048x2048 PNG) - Used for iOS and Android
|
||||
- **SVG source**: `flutter/assets/icon.svg` - Vector format, can be used to generate any resolution
|
||||
- **macOS source**: `res/mac-icon.png` - Specific macOS variant
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Install required tools:
|
||||
```bash
|
||||
sudo apt-get install librsvg2-bin imagemagick
|
||||
```
|
||||
|
||||
## Regenerating iOS Icons
|
||||
|
||||
### Method 1: Using ImageMagick (Manual)
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
SOURCE_ICON="res/icon.png"
|
||||
OUTPUT_DIR="flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset"
|
||||
|
||||
# Function to generate icon with alpha removal for iOS
|
||||
generate_icon() {
|
||||
local size=$1
|
||||
local filename=$2
|
||||
convert "$SOURCE_ICON" -resize ${size}x${size} -background white -alpha remove -alpha off "$OUTPUT_DIR/$filename"
|
||||
}
|
||||
|
||||
# iPhone icons
|
||||
generate_icon 40 "Icon-App-20x20@2x.png"
|
||||
generate_icon 60 "Icon-App-20x20@3x.png"
|
||||
generate_icon 29 "Icon-App-29x29@1x.png"
|
||||
generate_icon 58 "Icon-App-29x29@2x.png"
|
||||
generate_icon 87 "Icon-App-29x29@3x.png"
|
||||
generate_icon 80 "Icon-App-40x40@2x.png"
|
||||
generate_icon 120 "Icon-App-40x40@3x.png"
|
||||
generate_icon 120 "Icon-App-60x60@2x.png"
|
||||
generate_icon 180 "Icon-App-60x60@3x.png"
|
||||
|
||||
# iPad icons
|
||||
generate_icon 20 "Icon-App-20x20@1x.png"
|
||||
generate_icon 40 "Icon-App-40x40@1x.png"
|
||||
generate_icon 76 "Icon-App-76x76@1x.png"
|
||||
generate_icon 152 "Icon-App-76x76@2x.png"
|
||||
generate_icon 167 "Icon-App-83.5x83.5@2x.png"
|
||||
|
||||
# App Store icon
|
||||
generate_icon 1024 "Icon-App-1024x1024@1x.png"
|
||||
```
|
||||
|
||||
### Method 2: Using flutter_launcher_icons (Automated)
|
||||
|
||||
```bash
|
||||
cd flutter
|
||||
flutter pub run flutter_launcher_icons
|
||||
```
|
||||
|
||||
This uses the configuration in `flutter/pubspec.yaml`:
|
||||
```yaml
|
||||
flutter_icons:
|
||||
image_path: "../res/icon.png"
|
||||
remove_alpha_ios: true
|
||||
ios: true
|
||||
android: true
|
||||
```
|
||||
|
||||
## Regenerating Android Icons
|
||||
|
||||
```bash
|
||||
SOURCE_ICON="res/icon.png"
|
||||
BASE_DIR="flutter/android/app/src/main/res"
|
||||
|
||||
convert "$SOURCE_ICON" -resize 48x48 "$BASE_DIR/mipmap-mdpi/ic_launcher.png"
|
||||
convert "$SOURCE_ICON" -resize 72x72 "$BASE_DIR/mipmap-hdpi/ic_launcher.png"
|
||||
convert "$SOURCE_ICON" -resize 96x96 "$BASE_DIR/mipmap-xhdpi/ic_launcher.png"
|
||||
convert "$SOURCE_ICON" -resize 144x144 "$BASE_DIR/mipmap-xxhdpi/ic_launcher.png"
|
||||
convert "$SOURCE_ICON" -resize 192x192 "$BASE_DIR/mipmap-xxxhdpi/ic_launcher.png"
|
||||
```
|
||||
|
||||
Or use `flutter_launcher_icons` as shown above.
|
||||
|
||||
## Updating Source Icon from SVG
|
||||
|
||||
If you need to regenerate the source PNG from the SVG at a different resolution:
|
||||
|
||||
```bash
|
||||
# Generate 2048x2048 PNG from SVG
|
||||
rsvg-convert -w 2048 -h 2048 flutter/assets/icon.svg -o res/icon.png
|
||||
|
||||
# Or for even higher resolution (4096x4096)
|
||||
rsvg-convert -w 4096 -h 4096 flutter/assets/icon.svg -o res/icon.png
|
||||
```
|
||||
|
||||
## iOS Icon Requirements
|
||||
|
||||
- All iOS icons must have alpha channel removed (opaque)
|
||||
- White background is applied to maintain appearance
|
||||
- Icons are automatically rounded by iOS system
|
||||
- Retina displays require @2x and @3x variants
|
||||
|
||||
## Icon Sizes
|
||||
|
||||
### iOS
|
||||
- 20pt (20x20, 40x40, 60x60)
|
||||
- 29pt (29x29, 58x58, 87x87)
|
||||
- 40pt (40x40, 80x80, 120x120)
|
||||
- 60pt (120x120, 180x180)
|
||||
- 76pt (76x76, 152x152) - iPad
|
||||
- 83.5pt (167x167) - iPad Pro
|
||||
- 1024x1024 - App Store
|
||||
|
||||
### Android
|
||||
- mdpi: 48x48
|
||||
- hdpi: 72x72
|
||||
- xhdpi: 96x96
|
||||
- xxhdpi: 144x144
|
||||
- xxxhdpi: 192x192
|
||||
|
||||
## Quality Tips
|
||||
|
||||
1. Always start from the highest resolution source available (SVG preferred)
|
||||
2. Use at least 2048x2048 PNG as intermediate source
|
||||
3. Let ImageMagick/flutter_launcher_icons handle downscaling
|
||||
4. Never upscale low-resolution images - regenerate from vector source
|
||||
5. Test on actual devices with Retina displays to verify quality
|
||||
|
||||
## References
|
||||
|
||||
- [Apple Human Interface Guidelines - App Icons](https://developer.apple.com/design/human-interface-guidelines/app-icons)
|
||||
- [Android App Icon Guidelines](https://developer.android.com/guide/practices/ui_guidelines/icon_design_launcher)
|
||||
BIN
res/icon.png
|
Before Width: | Height: | Size: 188 KiB After Width: | Height: | Size: 39 KiB |
@@ -562,8 +562,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("id_input_tip", "Sie können eine ID, eine direkte IP oder eine Domäne mit einem Port (<domain>:<port>) eingeben.\nWenn Sie auf ein Gerät auf einem anderen Server zugreifen wollen, fügen Sie bitte die Serveradresse (<id>@<server_address>?key=<key_value>) hinzu, zum Beispiel\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nWenn Sie auf ein Gerät auf einem öffentlichen Server zugreifen wollen, geben Sie bitte \"<id>@public\" ein. Der Schlüssel wird für öffentliche Server nicht benötigt.\n\nWenn Sie bei der ersten Verbindung die Verwendung einer Relay-Verbindung erzwingen wollen, fügen Sie \"/r\" am Ende der ID hinzu, zum Beispiel \"9123456234/r\"."),
|
||||
("privacy_mode_impl_mag_tip", "Modus 1"),
|
||||
("privacy_mode_impl_virtual_display_tip", "Modus 2"),
|
||||
("Enter privacy mode", "Datenschutzmodus aktiviert"),
|
||||
("Exit privacy mode", "Datenschutzmodus beendet"),
|
||||
("Enter privacy mode", "Datenschutzmodus aktivieren"),
|
||||
("Exit privacy mode", "Datenschutzmodus beenden"),
|
||||
("idd_not_support_under_win10_2004_tip", "Indirekter Grafiktreiber wird nicht unterstützt. Windows 10, Version 2004 oder neuer ist erforderlich."),
|
||||
("input_source_1_tip", "Eingangsquelle 1"),
|
||||
("input_source_2_tip", "Eingangsquelle 2"),
|
||||
@@ -737,7 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("rel-mouse-exit-{}-tip", "Drücken Sie {} zum Beenden."),
|
||||
("rel-mouse-permission-lost-tip", "Die Tastaturberechtigung wurde widerrufen. Der relative Mausmodus wurde deaktiviert."),
|
||||
("Changelog", "Änderungsprotokoll"),
|
||||
("keep-awake-during-outgoing-sessions-label", "Bildschirm während ausgehender Sitzungen aktiv halten"),
|
||||
("keep-awake-during-incoming-sessions-label", "Bildschirm während eingehender Sitzungen aktiv halten"),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Are you sure you want to delete this empty directory?", "Voulez-vous vraiment supprimer ce répertoire vide ?"),
|
||||
("Are you sure you want to delete the file of this directory?", "Voulez-vous vraiment supprimer le fichier de ce répertoire ?"),
|
||||
("Do this for all conflicts", "Appliquer à tous les conflits"),
|
||||
("This is irreversible!", "Cette action est irréversible !"),
|
||||
("This is irreversible!", "Ceci est irréversible !"),
|
||||
("Deleting", "Suppression"),
|
||||
("files", "fichiers"),
|
||||
("Waiting", "En attente"),
|
||||
@@ -737,7 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("rel-mouse-exit-{}-tip", "Appuyez sur {} pour quitter."),
|
||||
("rel-mouse-permission-lost-tip", "L’autorisation de contrôle du clavier a été révoquée. Le mode souris relative a été désactivé."),
|
||||
("Changelog", "Journal des modifications"),
|
||||
("keep-awake-during-outgoing-sessions-label", "Maintenir l’écran allumé lors des sessions sortantes"),
|
||||
("keep-awake-during-incoming-sessions-label", "Maintenir l’écran allumé lors des sessions entrantes"),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Click to upgrade", "Kattintson ide a frissítés telepítéséhez"),
|
||||
("Configure", "Beállítás"),
|
||||
("config_acc", "A számítógép távoli vezérléséhez a RustDesknek hozzáférési jogokat kell adnia."),
|
||||
("config_screen", "Ahhoz, hogy távolról hozzáférhessen a számítógépéhez, meg kell adnia a RustDesknek a \"Képernyőfelvétel\" jogosultságot."),
|
||||
("config_screen", "Ahhoz, hogy távolról hozzáférhessen a számítógépéhez, meg kell adnia a RustDesknek a „Képernyőfelvétel” jogosultságot."),
|
||||
("Installing ...", "Telepítés…"),
|
||||
("Install", "Telepítés"),
|
||||
("Installation", "Telepítés"),
|
||||
@@ -276,13 +276,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Do you accept?", "Elfogadás?"),
|
||||
("Open System Setting", "Rendszerbeállítások megnyitása"),
|
||||
("How to get Android input permission?", "Hogyan állítható be az Androidos beviteli engedély?"),
|
||||
("android_input_permission_tip1", "Ahhoz, hogy egy távoli eszköz vezérelhesse Android készülékét, engedélyeznie kell a RustDesk számára a \"Hozzáférhetőség\" szolgáltatás használatát."),
|
||||
("android_input_permission_tip1", "Ahhoz, hogy egy távoli eszköz vezérelhesse Android készülékét, engedélyeznie kell a RustDesk számára a „Hozzáférhetőség” szolgáltatás használatát."),
|
||||
("android_input_permission_tip2", "A következő rendszerbeállítások oldalon a letöltött alkalmazások menüponton belül, kapcsolja be a [RustDesk Input] szolgáltatást."),
|
||||
("android_new_connection_tip", "Új kérés érkezett, mely vezérelni szeretné az eszközét"),
|
||||
("android_service_will_start_tip", "A képernyőmegosztás aktiválása automatikusan elindítja a szolgáltatást, így más eszközök is vezérelhetik ezt az Android-eszközt."),
|
||||
("android_stop_service_tip", "A szolgáltatás leállítása automatikusan szétkapcsol minden létező kapcsolatot."),
|
||||
("android_version_audio_tip", "A jelenlegi Android verzió nem támogatja a hangrögzítést, frissítsen legalább Android 10-re, vagy egy újabb verzióra."),
|
||||
("android_start_service_tip", "A képernyőmegosztó szolgáltatás elindításához koppintson a \"Kapcsolási szolgáltatás indítása\" gombra, vagy aktiválja a \"Képernyőfelvétel\" engedélyt."),
|
||||
("android_start_service_tip", "A képernyőmegosztó szolgáltatás elindításához koppintson a „Kapcsolási szolgáltatás indítása” gombra, vagy aktiválja a „Képernyőfelvétel” engedélyt."),
|
||||
("android_permission_may_not_change_tip", "A meglévő kapcsolatok engedélyei csak új kapcsolódás után módosulnak."),
|
||||
("Account", "Fiók"),
|
||||
("Overwrite", "Felülírás"),
|
||||
@@ -408,15 +408,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Select local keyboard type", "Helyi billentyűzet típusának kiválasztása"),
|
||||
("software_render_tip", "Ha Nvidia grafikus kártyát használ Linux alatt, és a távoli ablak a kapcsolat létrehozása után azonnal bezáródik, akkor a Nouveau nyílt forráskódú illesztőprogramra való váltás és a szoftveres leképezés alkalmazása segíthet. A szoftvert újra kell indítani."),
|
||||
("Always use software rendering", "Mindig szoftveres leképezést használjon"),
|
||||
("config_input", "Ahhoz, hogy a távoli asztalt a billentyűzettel vezérelhesse, a RustDesknek meg kell adnia a \"Bemenet figyelése\" jogosultságot."),
|
||||
("config_microphone", "Ahhoz, hogy távolról beszélhessen, meg kell adnia a RustDesknek a \"Hangfelvétel\" jogosultságot."),
|
||||
("config_input", "Ahhoz, hogy a távoli asztalt a billentyűzettel vezérelhesse, a RustDesknek meg kell adnia a „Bemenet figyelése” jogosultságot."),
|
||||
("config_microphone", "Ahhoz, hogy távolról beszélhessen, meg kell adnia a RustDesknek a „Hangfelvétel” jogosultságot."),
|
||||
("request_elevation_tip", "Akkor is kérhet megnövelt jogokat, ha valaki a partneroldalon van."),
|
||||
("Wait", "Várjon"),
|
||||
("Elevation Error", "Emelt szintű hozzáférési hiba"),
|
||||
("Ask the remote user for authentication", "Hitelesítés kérése a távoli felhasználótól"),
|
||||
("Choose this if the remote account is administrator", "Akkor válassza ezt, ha a távoli fiók rendszergazda"),
|
||||
("Transmit the username and password of administrator", "Küldje el a rendszergazda felhasználónevét és jelszavát"),
|
||||
("still_click_uac_tip", "A távoli felhasználónak továbbra is az \"Igen\" gombra kell kattintania a RustDesk UAC ablakában. Kattintson!"),
|
||||
("still_click_uac_tip", "A távoli felhasználónak továbbra is az „Igen” gombra kell kattintania a RustDesk UAC ablakában. Kattintson!"),
|
||||
("Request Elevation", "Emelt szintű jogok igénylése"),
|
||||
("wait_accept_uac_tip", "Várjon, amíg a távoli felhasználó elfogadja az UAC párbeszédet."),
|
||||
("Elevate successfully", "Emelt szintű jogok megadva"),
|
||||
@@ -442,7 +442,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Voice call", "Hanghívás"),
|
||||
("Text chat", "Szöveges csevegés"),
|
||||
("Stop voice call", "Hanghívás leállítása"),
|
||||
("relay_hint_tip", "Ha a közvetlen kapcsolat nem lehetséges, megpróbálhat kapcsolatot létesíteni egy továbbító-kiszolgálón keresztül.\nHa az első próbálkozáskor továbbító-kiszolgálón keresztüli kapcsolatot szeretne létrehozni, használhatja az \"/r\" utótagot. Az azonosítóhoz vagy a \"Mindig továbbító-kiszolgálón keresztül kapcsolódom\" opcióhoz a legutóbbi munkamenetek listájában, ha van ilyen."),
|
||||
("relay_hint_tip", "Ha a közvetlen kapcsolat nem lehetséges, megpróbálhat kapcsolatot létesíteni egy továbbító-kiszolgálón keresztül.\nHa az első próbálkozáskor továbbító-kiszolgálón keresztüli kapcsolatot szeretne létrehozni, használhatja az „/r” utótagot. Az azonosítóhoz vagy a „Mindig továbbító-kiszolgálón keresztül kapcsolódom” opcióhoz a legutóbbi munkamenetek listájában, ha van ilyen."),
|
||||
("Reconnect", "Újrakapcsolódás"),
|
||||
("Codec", "Kodek"),
|
||||
("Resolution", "Felbontás"),
|
||||
@@ -490,7 +490,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Update", "Frissítés"),
|
||||
("Enable", "Engedélyezés"),
|
||||
("Disable", "Letiltás"),
|
||||
("Options", "Opciók"),
|
||||
("Options", "Beállítások"),
|
||||
("resolution_original_tip", "Eredeti felbontás"),
|
||||
("resolution_fit_local_tip", "Helyi felbontás beállítása"),
|
||||
("resolution_custom_tip", "Testre szabható felbontás"),
|
||||
@@ -559,7 +559,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Plug out all", "Kapcsolja ki az összeset"),
|
||||
("True color (4:4:4)", "Valódi szín (4:4:4)"),
|
||||
("Enable blocking user input", "Engedélyezze a felhasználói bevitel blokkolását"),
|
||||
("id_input_tip", "Megadhat egy azonosítót, egy közvetlen IP-címet vagy egy tartományt egy porttal (<domain>:<port>).\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (<id>@<kiszolgáló_cím>?key=<key_value>), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a \"<id>@public\" lehetőséget. A kulcsra nincs szükség nyilvános kiszolgálók esetén.\n\nHa az első kapcsolathoz továbbító-kiszolgálón keresztüli kapcsolatot akar kényszeríteni, adja hozzá az \"/r\" az azonosítót a végén, például \"9123456234/r\"."),
|
||||
("id_input_tip", "Megadhat egy azonosítót, egy közvetlen IP-címet vagy egy tartományt egy porttal (<domain>:<port>).\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (<id>@<kiszolgáló_cím>?key=<key_value>), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a „<id>@public” lehetőséget. A kulcsra nincs szükség nyilvános kiszolgálók esetén.\n\nHa az első kapcsolathoz továbbító-kiszolgálón keresztüli kapcsolatot akar kényszeríteni, adja hozzá az „/r” az azonosítót a végén, például „9123456234/r”."),
|
||||
("privacy_mode_impl_mag_tip", "1. mód"),
|
||||
("privacy_mode_impl_virtual_display_tip", "2. mód"),
|
||||
("Enter privacy mode", "Lépjen be az adatvédelmi módba"),
|
||||
@@ -622,7 +622,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Power", "Főkapcsoló"),
|
||||
("Telegram bot", "Telegram bot"),
|
||||
("enable-bot-tip", "Ha aktiválja ezt a funkciót, akkor a 2FA-kódot a botjától kaphatja meg. Kapcsolati értesítésként is használható."),
|
||||
("enable-bot-desc", "1. Nyisson csevegést @BotFather.\n2. Küldje el a \"/newbot\" parancsot. Miután ezt a lépést elvégezte, kap egy tokent.\n3. Indítson csevegést az újonnan létrehozott botjával. Küldjön egy olyan üzenetet, amely egy perjel (\"/\") kezdetű, pl. \"/hello\" az aktiváláshoz.\n"),
|
||||
("enable-bot-desc", "1. Nyisson csevegést @BotFather.\n2. Küldje el a „/newbot” parancsot. Miután ezt a lépést elvégezte, kap egy tokent.\n3. Indítson csevegést az újonnan létrehozott botjával. Küldjön egy olyan üzenetet, amely egy perjel („/”) kezdetű, pl. „/hello” az aktiváláshoz.\n"),
|
||||
("cancel-2fa-confirm-tip", "Biztosan vissza akarja vonni a 2FA-hitelesítést?"),
|
||||
("cancel-bot-confirm-tip", "Biztosan le akarja mondani a Telegram botot?"),
|
||||
("About RustDesk", "A RustDesk névjegye"),
|
||||
@@ -643,7 +643,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("one-way-file-transfer-tip", "Az egyirányú fájlátvitel engedélyezve van a vezérelt oldalon."),
|
||||
("Authentication Required", "Hitelesítés szükséges"),
|
||||
("Authenticate", "Hitelesítés"),
|
||||
("web_id_input_tip", "Azonos kiszolgálón lévő azonosítót adhat meg, a közvetlen IP elérés nem támogatott a webkliensben.\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (<id>@<kiszolgáló_cím>?key=<key_value>), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a \"<id>@public\" betűt. A kulcsra nincs szükség a nyilvános kiszolgálók esetében."),
|
||||
("web_id_input_tip", "Azonos kiszolgálón lévő azonosítót adhat meg, a közvetlen IP elérés nem támogatott a webkliensben.\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (<id>@<kiszolgáló_cím>?key=<key_value>), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a „<id>@public” betűt. A kulcsra nincs szükség a nyilvános kiszolgálók esetében."),
|
||||
("Download", "Letöltés"),
|
||||
("Upload folder", "Mappa feltöltése"),
|
||||
("Upload files", "Fájlok feltöltése"),
|
||||
@@ -682,9 +682,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Downloading {}", "{} letöltése"),
|
||||
("{} Update", "{} frissítés"),
|
||||
("{}-to-update-tip", "A(z) {} bezárása és az új verzió telepítése."),
|
||||
("download-new-version-failed-tip", "Ha a letöltés sikertelen, akkor vagy újrapróbálkozhat, vagy a \"Letöltés\" gombra kattintva letöltheti a kiadási oldalról, és manuálisan frissíthet."),
|
||||
("download-new-version-failed-tip", "Ha a letöltés sikertelen, akkor vagy újrapróbálkozhat, vagy a „Letöltés” gombra kattintva letöltheti a kiadási oldalról, és manuálisan frissíthet."),
|
||||
("Auto update", "Automatikus frissítés"),
|
||||
("update-failed-check-msi-tip", "A telepítési módszer felismerése nem sikerült. Kattintson a \"Letöltés\" gombra, hogy letöltse a kiadási oldalról, és manuálisan frissítse."),
|
||||
("update-failed-check-msi-tip", "A telepítési módszer felismerése nem sikerült. Kattintson a „Letöltés” gombra, hogy letöltse a kiadási oldalról, és manuálisan frissítse."),
|
||||
("websocket_tip", "WebSocket használatakor csak a relé-kapcsolatok támogatottak."),
|
||||
("Use WebSocket", "WebSocket használata"),
|
||||
("Trackpad speed", "Érintőpad sebessége"),
|
||||
@@ -730,14 +730,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("input note here", "Megjegyzés beírása"),
|
||||
("note-at-conn-end-tip", "Kérjen megjegyzést a kapcsolat végén"),
|
||||
("Show terminal extra keys", "További terminálgombok megjelenítése"),
|
||||
("Relative mouse mode", "Relatív egér mód"),
|
||||
("rel-mouse-not-supported-peer-tip", "A kapcsolódott partner nem támogatja a relatív egér módot."),
|
||||
("rel-mouse-not-ready-tip", "A relatív egér mód még nem elérhető. Próbálja meg újra."),
|
||||
("rel-mouse-lock-failed-tip", "Nem sikerült zárolni a kurzort. A relatív egér mód le lett tiltva."),
|
||||
("Relative mouse mode", "Relatív egérmód"),
|
||||
("rel-mouse-not-supported-peer-tip", "A kapcsolódott partner nem támogatja a relatív egérmódot."),
|
||||
("rel-mouse-not-ready-tip", "A relatív egérmód még nem elérhető. Próbálja meg újra."),
|
||||
("rel-mouse-lock-failed-tip", "Nem sikerült zárolni a kurzort. A relatív egérmód le lett tiltva."),
|
||||
("rel-mouse-exit-{}-tip", "A kilépéshez nyomja meg a(z) {} gombot."),
|
||||
("rel-mouse-permission-lost-tip", "A billentyűzet-hozzáférés vissza lett vonva. A relatív egér mód le lett tilva."),
|
||||
("rel-mouse-permission-lost-tip", "A billentyűzet-hozzáférés vissza lett vonva. A relatív egérmód le lett tilva."),
|
||||
("Changelog", "Változáslista"),
|
||||
("keep-awake-during-outgoing-sessions-label", "Képernyő aktív állapotban tartása a kimenő munkamenetek során"),
|
||||
("keep-awake-during-incoming-sessions-label", "Képernyő aktív állapotban tartása a bejövő munkamenetek során"),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -737,7 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("rel-mouse-exit-{}-tip", "Premi {} per uscire."),
|
||||
("rel-mouse-permission-lost-tip", "È stata revocato l'accesso alla tastiera. La modalità mouse relativa è stata disabilitata."),
|
||||
("Changelog", "Novità programma"),
|
||||
("keep-awake-during-outgoing-sessions-label", "Mantieni lo schermo attivo durante le sessioni in uscita"),
|
||||
("keep-awake-during-incoming-sessions-label", "Mantieni lo schermo attivo durante le sessioni in ingresso"),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -737,7 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("rel-mouse-exit-{}-tip", "종료하려면 {}을(를) 누르세요."),
|
||||
("rel-mouse-permission-lost-tip", "키보드 권한이 취소되었습니다. 상대 마우스 모드가 비활성화되었습니다."),
|
||||
("Changelog", "변경 기록"),
|
||||
("keep-awake-during-outgoing-sessions-label", "발신 세션 중 화면 켜짐 유지"),
|
||||
("keep-awake-during-incoming-sessions-label", "수신 세션 중 화면 켜짐 유지"),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -737,7 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("rel-mouse-exit-{}-tip", "Druk op {} om af te sluiten."),
|
||||
("rel-mouse-permission-lost-tip", "De toetsenbordcontrole is uitgeschakeld. De relatieve muismodus is uitgeschakeld."),
|
||||
("Changelog", "Wijzigingenlogboek"),
|
||||
("keep-awake-during-outgoing-sessions-label", "Houd het scherm open tijdens de uitgaande sessies."),
|
||||
("keep-awake-during-incoming-sessions-label", "Houd het scherm open tijdens de inkomende sessies."),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -737,7 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("rel-mouse-exit-{}-tip", "Aby wyłączyć tryb przechwytywania myszy, naciśnij {}"),
|
||||
("rel-mouse-permission-lost-tip", "Utracono uprawnienia do trybu przechwytywania myszy"),
|
||||
("Changelog", "Dziennik zmian"),
|
||||
("keep-awake-during-outgoing-sessions-label", "Utrzymuj urządzenie w stanie aktywnym podczas sesji wychodzących"),
|
||||
("keep-awake-during-incoming-sessions-label", "Utrzymuj urządzenie w stanie aktywnym podczas sesji przychodzących"),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
102
src/lang/ptbr.rs
@@ -672,72 +672,72 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("remote-printing-disallowed-text-tip", "As configurações do dispositivo controlado não permitem impressão remota."),
|
||||
("save-settings-tip", "Salvar configurações"),
|
||||
("dont-show-again-tip", "Não mostrar novamente"),
|
||||
("Take screenshot", "Capturar de tela"),
|
||||
("Taking screenshot", "Capturando tela"),
|
||||
("Take screenshot", ""),
|
||||
("Taking screenshot", ""),
|
||||
("screenshot-merged-screen-not-supported-tip", ""),
|
||||
("screenshot-action-tip", ""),
|
||||
("Save as", "Salvar como"),
|
||||
("Copy to clipboard", "Copiar para área de transferência"),
|
||||
("Enable remote printer", "Habilitar impressora remota"),
|
||||
("Save as", ""),
|
||||
("Copy to clipboard", ""),
|
||||
("Enable remote printer", ""),
|
||||
("Downloading {}", ""),
|
||||
("{} Update", ""),
|
||||
("{}-to-update-tip", ""),
|
||||
("download-new-version-failed-tip", "Falha no download. Você pode tentar novamente ou clicar no botão \"Download\" para baixar da página releases e atualizar manualmente."),
|
||||
("Auto update", "Atualização automática"),
|
||||
("update-failed-check-msi-tip", "Falha na verificação do método de instalação. Clique no botão \"Download\" para baixar da página releases e atualizar manualmente."),
|
||||
("websocket_tip", "Usando WebSocket, apenas conexões via relay são suportadas."),
|
||||
("Use WebSocket", "Usar WebSocket"),
|
||||
("Trackpad speed", "Velocidade do trackpad"),
|
||||
("download-new-version-failed-tip", ""),
|
||||
("Auto update", ""),
|
||||
("update-failed-check-msi-tip", ""),
|
||||
("websocket_tip", ""),
|
||||
("Use WebSocket", ""),
|
||||
("Trackpad speed", ""),
|
||||
("Default trackpad speed", ""),
|
||||
("Numeric one-time password", "Senha numérica de uso único"),
|
||||
("Enable IPv6 P2P connection", "Habilitar conexão IPv6 P2P"),
|
||||
("Enable UDP hole punching", "Habilitar UDP hole punching"),
|
||||
("Numeric one-time password", ""),
|
||||
("Enable IPv6 P2P connection", ""),
|
||||
("Enable UDP hole punching", ""),
|
||||
("View camera", "Visualizar câmera"),
|
||||
("Enable camera", "Ativar câmera"),
|
||||
("No cameras", "Sem câmeras"),
|
||||
("view_camera_unsupported_tip", "O dispositivo remoto não suporta visualização da câmera."),
|
||||
("Terminal", "Terminal"),
|
||||
("Enable terminal", "Habilitar Terminal"),
|
||||
("New tab", "Nova aba"),
|
||||
("Keep terminal sessions on disconnect", "Manter sessões de terminal ao desconectar"),
|
||||
("Terminal (Run as administrator)", "Terminal (Executar como administrador)"),
|
||||
("terminal-admin-login-tip", "Insira o nome do usuário e senha de administrador do dispositivo controlado."),
|
||||
("Failed to get user token.", "Falha ao obter token do usuário."),
|
||||
("Incorrect username or password.", "Usuário ou senha incorretos"),
|
||||
("The user is not an administrator.", "O usuário não é administrador"),
|
||||
("Failed to check if the user is an administrator.", "Falha ao verificar se o usuário é administrador"),
|
||||
("Supported only in the installed version.", "Funciona somente na versão instalada"),
|
||||
("elevation_username_tip", "Insira o nome do usuário ou domínio\\usuário"),
|
||||
("Preparing for installation ...", "Preparando para instalação ..."),
|
||||
("Show my cursor", "Mostrar meu cursor"),
|
||||
("Terminal", ""),
|
||||
("Enable terminal", ""),
|
||||
("New tab", ""),
|
||||
("Keep terminal sessions on disconnect", ""),
|
||||
("Terminal (Run as administrator)", ""),
|
||||
("terminal-admin-login-tip", ""),
|
||||
("Failed to get user token.", ""),
|
||||
("Incorrect username or password.", ""),
|
||||
("The user is not an administrator.", ""),
|
||||
("Failed to check if the user is an administrator.", ""),
|
||||
("Supported only in the installed version.", ""),
|
||||
("elevation_username_tip", ""),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
("Scale custom", "Escala personalizada"),
|
||||
("Custom scale slider", "Controle deslizante de escala personalizada"),
|
||||
("Decrease", "Diminuir"),
|
||||
("Increase", "Aumentar"),
|
||||
("Show virtual mouse", "Mostrar mouse virtual"),
|
||||
("Virtual mouse size", "Tamanho do mouse virtual"),
|
||||
("Small", "Pequeno"),
|
||||
("Large", "Grande"),
|
||||
("Show virtual mouse", ""),
|
||||
("Virtual mouse size", ""),
|
||||
("Small", ""),
|
||||
("Large", ""),
|
||||
("Show virtual joystick", ""),
|
||||
("Edit note", "Editar nota"),
|
||||
("Alias", "Apelido"),
|
||||
("ScrollEdge", "Rolagem nas bordas"),
|
||||
("Edit note", ""),
|
||||
("Alias", ""),
|
||||
("ScrollEdge", ""),
|
||||
("Allow insecure TLS fallback", ""),
|
||||
("allow-insecure-tls-fallback-tip", "Por padrão, o RustDesk verifica o certificado do servidor para protocolos que usam TLS.\nCom esta opção habilitada, o RustDesk ignorará a verificação e prosseguirá em caso de falha."),
|
||||
("Disable UDP", "Desabilitar UDP"),
|
||||
("disable-udp-tip", "Controla se deve usar somente TCP.\nCom esta opção habilitada, o RustDesk não usará mais UDP 21116, TCP 21116 será usado no lugar."),
|
||||
("server-oss-not-support-tip", "NOTA: O servidor RustDesk OSS não inclui este recurso."),
|
||||
("input note here", "Insira uma nota aqui"),
|
||||
("note-at-conn-end-tip", "Solicitar nota ao final da conexão"),
|
||||
("Show terminal extra keys", "Mostrar teclas extras do terminal"),
|
||||
("Relative mouse mode", "Modo de Mouse Relativo"),
|
||||
("rel-mouse-not-supported-peer-tip", "O Modo de Mouse Relativo não é suportado pelo parceiro conectado."),
|
||||
("rel-mouse-not-ready-tip", "O Modo de Mouse Relativo ainda não está pronto. Por favor, tente novamente."),
|
||||
("rel-mouse-lock-failed-tip", "Falha ao bloquear o cursor. O Modo de Mouse Relativo foi desabilitado."),
|
||||
("rel-mouse-exit-{}-tip", "Pressione {} para sair."),
|
||||
("rel-mouse-permission-lost-tip", "Permissão de teclado revogada. O Modo Mouse Relativo foi desabilitado."),
|
||||
("Changelog", "Registro de alterações"),
|
||||
("keep-awake-during-outgoing-sessions-label", "Manter tela ativa durante sessões de saída"),
|
||||
("keep-awake-during-incoming-sessions-label", "Manter tela ativa durante sessões de entrada"),
|
||||
("allow-insecure-tls-fallback-tip", ""),
|
||||
("Disable UDP", ""),
|
||||
("disable-udp-tip", ""),
|
||||
("server-oss-not-support-tip", ""),
|
||||
("input note here", ""),
|
||||
("note-at-conn-end-tip", ""),
|
||||
("Show terminal extra keys", ""),
|
||||
("Relative mouse mode", ""),
|
||||
("rel-mouse-not-supported-peer-tip", ""),
|
||||
("rel-mouse-not-ready-tip", ""),
|
||||
("rel-mouse-lock-failed-tip", ""),
|
||||
("rel-mouse-exit-{}-tip", ""),
|
||||
("rel-mouse-permission-lost-tip", ""),
|
||||
("Changelog", ""),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -729,15 +729,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("server-oss-not-support-tip", "注意:RustDesk 開源伺服器 (OSS server) 不包含此功能。"),
|
||||
("input note here", "輸入備註"),
|
||||
("note-at-conn-end-tip", "在連接結束時請求備註"),
|
||||
("Show terminal extra keys", "顯示終端機額外按鍵"),
|
||||
("Relative mouse mode", "相對滑鼠模式"),
|
||||
("rel-mouse-not-supported-peer-tip", "被控端不支援相對滑鼠模式"),
|
||||
("rel-mouse-not-ready-tip", "相對滑鼠模式尚未就緒,請稍候再試"),
|
||||
("rel-mouse-lock-failed-tip", "無法鎖定游標,相對滑鼠模式已停用"),
|
||||
("rel-mouse-exit-{}-tip", "按下 {} 退出"),
|
||||
("rel-mouse-permission-lost-tip", "鍵盤權限被撤銷,相對滑鼠模式已被停用"),
|
||||
("Changelog", "更新日誌"),
|
||||
("keep-awake-during-outgoing-sessions-label", "在連出工作階段期間保持螢幕喚醒"),
|
||||
("keep-awake-during-incoming-sessions-label", "在連入工作階段期間保持螢幕喚醒"),
|
||||
("Show terminal extra keys", ""),
|
||||
("Relative mouse mode", ""),
|
||||
("rel-mouse-not-supported-peer-tip", ""),
|
||||
("rel-mouse-not-ready-tip", ""),
|
||||
("rel-mouse-lock-failed-tip", ""),
|
||||
("rel-mouse-exit-{}-tip", ""),
|
||||
("rel-mouse-permission-lost-tip", ""),
|
||||
("Changelog", ""),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -4,13 +4,6 @@
|
||||
#include <Security/Authorization.h>
|
||||
#include <Security/AuthorizationTags.h>
|
||||
|
||||
#include <CoreGraphics/CoreGraphics.h>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
extern "C" bool CanUseNewApiForScreenCaptureCheck() {
|
||||
#ifdef NO_InputMonitoringAuthStatus
|
||||
return false;
|
||||
@@ -299,611 +292,3 @@ extern "C" bool MacSetMode(CGDirectDisplayID display, uint32_t width, uint32_t h
|
||||
CFRelease(allModes);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static CFMachPortRef g_eventTap = NULL;
|
||||
static CFRunLoopSourceRef g_runLoopSource = NULL;
|
||||
static std::mutex g_privacyModeMutex;
|
||||
static bool g_privacyModeActive = false;
|
||||
|
||||
// Flag to request asynchronous shutdown of privacy mode.
|
||||
// This is set by DisplayReconfigurationCallback when an error occurs, instead of calling
|
||||
// TurnOffPrivacyModeInternal() directly from within the callback. This avoids potential
|
||||
// issues with unregistering a callback from within itself, which is not explicitly
|
||||
// guaranteed to be safe by Apple documentation.
|
||||
static bool g_privacyModeShutdownRequested = false;
|
||||
|
||||
// Timestamp of the last display reconfiguration event (in milliseconds).
|
||||
// Used for debouncing rapid successive changes (e.g., multiple resolution changes).
|
||||
static uint64_t g_lastReconfigTimestamp = 0;
|
||||
|
||||
// Flag indicating whether a delayed blackout reapplication is already scheduled.
|
||||
// Prevents multiple concurrent delayed tasks from being created.
|
||||
static bool g_blackoutReapplicationScheduled = false;
|
||||
|
||||
// Use CFStringRef (UUID) as key instead of CGDirectDisplayID for stability across reconnections
|
||||
// CGDirectDisplayID can change when displays are reconnected, but UUID remains stable
|
||||
static std::map<std::string, std::vector<CGGammaValue>> g_originalGammas;
|
||||
|
||||
// The event source user data value used by enigo library for injected events.
|
||||
// This allows us to distinguish remote input (which should be allowed) from local physical input.
|
||||
// See: libs/enigo/src/macos/macos_impl.rs - ENIGO_INPUT_EXTRA_VALUE
|
||||
static const int64_t ENIGO_INPUT_EXTRA_VALUE = 100;
|
||||
|
||||
// Duration in milliseconds to monitor and enforce blackout after display reconfiguration.
|
||||
// macOS may restore default gamma (via ColorSync) at unpredictable times after display changes,
|
||||
// so we need to actively monitor and reapply blackout during this period.
|
||||
static const int64_t DISPLAY_RECONFIG_MONITOR_DURATION_MS = 5000;
|
||||
|
||||
// Interval in milliseconds between gamma checks during the monitoring period.
|
||||
static const int64_t GAMMA_CHECK_INTERVAL_MS = 200;
|
||||
|
||||
// Helper function to get UUID string from DisplayID
|
||||
static std::string GetDisplayUUID(CGDirectDisplayID displayId) {
|
||||
CFUUIDRef uuid = CGDisplayCreateUUIDFromDisplayID(displayId);
|
||||
if (uuid == NULL) {
|
||||
return "";
|
||||
}
|
||||
CFStringRef uuidStr = CFUUIDCreateString(kCFAllocatorDefault, uuid);
|
||||
CFRelease(uuid);
|
||||
if (uuidStr == NULL) {
|
||||
return "";
|
||||
}
|
||||
char buffer[128];
|
||||
if (CFStringGetCString(uuidStr, buffer, sizeof(buffer), kCFStringEncodingUTF8)) {
|
||||
CFRelease(uuidStr);
|
||||
return std::string(buffer);
|
||||
}
|
||||
CFRelease(uuidStr);
|
||||
return "";
|
||||
}
|
||||
|
||||
// Helper function to find DisplayID by UUID from current online displays
|
||||
static CGDirectDisplayID FindDisplayIdByUUID(const std::string& targetUuid) {
|
||||
uint32_t count = 0;
|
||||
CGGetOnlineDisplayList(0, NULL, &count);
|
||||
if (count == 0) return kCGNullDirectDisplay;
|
||||
|
||||
std::vector<CGDirectDisplayID> displays(count);
|
||||
CGGetOnlineDisplayList(count, displays.data(), &count);
|
||||
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
std::string uuid = GetDisplayUUID(displays[i]);
|
||||
if (uuid == targetUuid) {
|
||||
return displays[i];
|
||||
}
|
||||
}
|
||||
return kCGNullDirectDisplay;
|
||||
}
|
||||
|
||||
// Helper function to restore gamma values for all displays in g_originalGammas.
|
||||
// Returns true if all displays were restored successfully, false if any failed.
|
||||
// Note: This function does NOT clear g_originalGammas - caller should do that if needed.
|
||||
static bool RestoreAllGammas() {
|
||||
bool allSuccess = true;
|
||||
for (auto const& [uuid, gamma] : g_originalGammas) {
|
||||
CGDirectDisplayID d = FindDisplayIdByUUID(uuid);
|
||||
if (d == kCGNullDirectDisplay) {
|
||||
NSLog(@"Display with UUID %s no longer online, skipping gamma restore", uuid.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
uint32_t sampleCount = gamma.size() / 3;
|
||||
if (sampleCount > 0) {
|
||||
const CGGammaValue* red = gamma.data();
|
||||
const CGGammaValue* green = red + sampleCount;
|
||||
const CGGammaValue* blue = green + sampleCount;
|
||||
CGError error = CGSetDisplayTransferByTable(d, sampleCount, red, green, blue);
|
||||
if (error != kCGErrorSuccess) {
|
||||
NSLog(@"Failed to restore gamma for display (ID: %u, UUID: %s, error: %d)", (unsigned)d, uuid.c_str(), error);
|
||||
allSuccess = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return allSuccess;
|
||||
}
|
||||
|
||||
// Helper function to apply blackout to a single display
|
||||
static bool ApplyBlackoutToDisplay(CGDirectDisplayID display) {
|
||||
uint32_t capacity = CGDisplayGammaTableCapacity(display);
|
||||
if (capacity > 0) {
|
||||
std::vector<CGGammaValue> zeros(capacity, 0.0f);
|
||||
CGError error = CGSetDisplayTransferByTable(display, capacity, zeros.data(), zeros.data(), zeros.data());
|
||||
if (error != kCGErrorSuccess) {
|
||||
NSLog(@"ApplyBlackoutToDisplay: Failed to set gamma for display %u (error %d)", (unsigned)display, error);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
NSLog(@"ApplyBlackoutToDisplay: Display %u has zero gamma table capacity, blackout not supported", (unsigned)display);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Forward declaration - defined later in the file
|
||||
// Must be called while holding g_privacyModeMutex
|
||||
static bool TurnOffPrivacyModeInternal();
|
||||
|
||||
// Helper function to schedule asynchronous shutdown of privacy mode.
|
||||
// This is called from DisplayReconfigurationCallback when an error occurs,
|
||||
// instead of calling TurnOffPrivacyModeInternal() directly. This avoids
|
||||
// potential issues with unregistering a callback from within itself.
|
||||
// Note: This function should be called while holding g_privacyModeMutex.
|
||||
static void ScheduleAsyncPrivacyModeShutdown(const char* reason) {
|
||||
if (g_privacyModeShutdownRequested) {
|
||||
// Already requested, no need to schedule again
|
||||
return;
|
||||
}
|
||||
g_privacyModeShutdownRequested = true;
|
||||
NSLog(@"Privacy mode shutdown requested: %s", reason);
|
||||
|
||||
// Schedule the actual shutdown on the main queue asynchronously
|
||||
// This ensures we're outside the callback when we unregister it
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
std::lock_guard<std::mutex> lock(g_privacyModeMutex);
|
||||
if (g_privacyModeShutdownRequested && g_privacyModeActive) {
|
||||
NSLog(@"Executing deferred privacy mode shutdown");
|
||||
TurnOffPrivacyModeInternal();
|
||||
}
|
||||
g_privacyModeShutdownRequested = false;
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to apply blackout to all online displays.
|
||||
// Must be called while holding g_privacyModeMutex.
|
||||
static void ApplyBlackoutToAllDisplays() {
|
||||
uint32_t onlineCount = 0;
|
||||
CGGetOnlineDisplayList(0, NULL, &onlineCount);
|
||||
std::vector<CGDirectDisplayID> onlineDisplays(onlineCount);
|
||||
CGGetOnlineDisplayList(onlineCount, onlineDisplays.data(), &onlineCount);
|
||||
|
||||
for (uint32_t i = 0; i < onlineCount; i++) {
|
||||
ApplyBlackoutToDisplay(onlineDisplays[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get current timestamp in milliseconds
|
||||
static uint64_t GetCurrentTimestampMs() {
|
||||
return (uint64_t)(CFAbsoluteTimeGetCurrent() * 1000.0);
|
||||
}
|
||||
|
||||
// Helper function to check if a display's gamma is currently blacked out (all zeros).
|
||||
// Returns true if gamma appears to be blacked out, false otherwise.
|
||||
static bool IsDisplayBlackedOut(CGDirectDisplayID display) {
|
||||
uint32_t capacity = CGDisplayGammaTableCapacity(display);
|
||||
if (capacity == 0) {
|
||||
return true; // Can't check, assume it's fine
|
||||
}
|
||||
|
||||
std::vector<CGGammaValue> red(capacity), green(capacity), blue(capacity);
|
||||
uint32_t sampleCount = 0;
|
||||
if (CGGetDisplayTransferByTable(display, capacity, red.data(), green.data(), blue.data(), &sampleCount) != kCGErrorSuccess) {
|
||||
return true; // Can't read, assume it's fine
|
||||
}
|
||||
|
||||
// Check if all values are zero (or very close to zero)
|
||||
for (uint32_t i = 0; i < sampleCount; i++) {
|
||||
if (red[i] > 0.01f || green[i] > 0.01f || blue[i] > 0.01f) {
|
||||
return false; // Not blacked out
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Internal function that monitors and enforces blackout for a period after display reconfiguration.
|
||||
// This function checks gamma values periodically and reapplies blackout if needed.
|
||||
// Must NOT be called while holding g_privacyModeMutex (it acquires the lock internally).
|
||||
static void RunBlackoutMonitor() {
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(GAMMA_CHECK_INTERVAL_MS * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{
|
||||
std::lock_guard<std::mutex> lock(g_privacyModeMutex);
|
||||
|
||||
if (!g_privacyModeActive) {
|
||||
g_blackoutReapplicationScheduled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t now = GetCurrentTimestampMs();
|
||||
|
||||
// Calculate effective end time based on the last reconfig event
|
||||
uint64_t effectiveEndTime = g_lastReconfigTimestamp + DISPLAY_RECONFIG_MONITOR_DURATION_MS;
|
||||
|
||||
// Check all displays and reapply blackout if any has been restored
|
||||
uint32_t onlineCount = 0;
|
||||
CGGetOnlineDisplayList(0, NULL, &onlineCount);
|
||||
std::vector<CGDirectDisplayID> onlineDisplays(onlineCount);
|
||||
CGGetOnlineDisplayList(onlineCount, onlineDisplays.data(), &onlineCount);
|
||||
|
||||
bool needsReapply = false;
|
||||
for (uint32_t i = 0; i < onlineCount; i++) {
|
||||
if (!IsDisplayBlackedOut(onlineDisplays[i])) {
|
||||
needsReapply = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (needsReapply) {
|
||||
NSLog(@"Gamma was restored by system, reapplying blackout");
|
||||
ApplyBlackoutToAllDisplays();
|
||||
}
|
||||
|
||||
// Continue monitoring if we haven't reached the end time
|
||||
if (now < effectiveEndTime) {
|
||||
RunBlackoutMonitor();
|
||||
} else {
|
||||
NSLog(@"Blackout monitoring period ended");
|
||||
g_blackoutReapplicationScheduled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to start monitoring and enforcing blackout after display reconfiguration.
|
||||
// This is used after display reconfiguration events because macOS may restore
|
||||
// default gamma (via ColorSync) at unpredictable times after display changes.
|
||||
// Note: This function should be called while holding g_privacyModeMutex.
|
||||
static void ScheduleDelayedBlackoutReapplication(const char* reason) {
|
||||
// Update timestamp to current time
|
||||
g_lastReconfigTimestamp = GetCurrentTimestampMs();
|
||||
|
||||
NSLog(@"Starting blackout monitor: %s", reason);
|
||||
|
||||
// Only schedule if not already scheduled
|
||||
if (!g_blackoutReapplicationScheduled) {
|
||||
g_blackoutReapplicationScheduled = true;
|
||||
RunBlackoutMonitor();
|
||||
}
|
||||
// If already scheduled, the running monitor will see the updated timestamp
|
||||
// and extend its monitoring period
|
||||
}
|
||||
|
||||
// Display reconfiguration callback to handle display connect/disconnect events
|
||||
//
|
||||
// IMPORTANT: When errors occur in this callback, we use ScheduleAsyncPrivacyModeShutdown()
|
||||
// instead of calling TurnOffPrivacyModeInternal() directly. This is because:
|
||||
// 1. TurnOffPrivacyModeInternal() calls CGDisplayRemoveReconfigurationCallback to unregister
|
||||
// this callback, and unregistering a callback from within itself is not explicitly
|
||||
// guaranteed to be safe by Apple documentation.
|
||||
// 2. Using async dispatch ensures we're completely outside the callback context when
|
||||
// performing the cleanup, avoiding any potential undefined behavior.
|
||||
static void DisplayReconfigurationCallback(CGDirectDisplayID display, CGDisplayChangeSummaryFlags flags, void *userInfo) {
|
||||
(void)userInfo;
|
||||
|
||||
// Note: We need to handle the callback carefully because:
|
||||
// 1. macOS may call this callback multiple times during display reconfiguration
|
||||
// 2. The system may restore ColorSync settings after our gamma change
|
||||
// 3. We should not hold the lock for too long in the callback
|
||||
|
||||
// Skip begin configuration flag - wait for the actual change
|
||||
if (flags & kCGDisplayBeginConfigurationFlag) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(g_privacyModeMutex);
|
||||
|
||||
if (!g_privacyModeActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (flags & kCGDisplayAddFlag) {
|
||||
// A display was added - apply blackout to it
|
||||
NSLog(@"Display %u added during privacy mode, applying blackout", (unsigned)display);
|
||||
std::string uuid = GetDisplayUUID(display);
|
||||
if (uuid.empty()) {
|
||||
NSLog(@"Failed to get UUID for newly added display %u, exiting privacy mode", (unsigned)display);
|
||||
ScheduleAsyncPrivacyModeShutdown("Failed to get UUID for newly added display");
|
||||
return;
|
||||
}
|
||||
|
||||
// Save original gamma if not already saved for this UUID
|
||||
if (g_originalGammas.find(uuid) == g_originalGammas.end()) {
|
||||
uint32_t capacity = CGDisplayGammaTableCapacity(display);
|
||||
if (capacity > 0) {
|
||||
std::vector<CGGammaValue> red(capacity), green(capacity), blue(capacity);
|
||||
uint32_t sampleCount = 0;
|
||||
if (CGGetDisplayTransferByTable(display, capacity, red.data(), green.data(), blue.data(), &sampleCount) == kCGErrorSuccess) {
|
||||
std::vector<CGGammaValue> all;
|
||||
all.insert(all.end(), red.begin(), red.begin() + sampleCount);
|
||||
all.insert(all.end(), green.begin(), green.begin() + sampleCount);
|
||||
all.insert(all.end(), blue.begin(), blue.begin() + sampleCount);
|
||||
g_originalGammas[uuid] = all;
|
||||
} else {
|
||||
NSLog(@"DisplayReconfigurationCallback: Failed to get gamma table for display %u (UUID: %s), exiting privacy mode", (unsigned)display, uuid.c_str());
|
||||
ScheduleAsyncPrivacyModeShutdown("Failed to get gamma table for newly added display");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
NSLog(@"DisplayReconfigurationCallback: Display %u (UUID: %s) has zero gamma table capacity, exiting privacy mode", (unsigned)display, uuid.c_str());
|
||||
ScheduleAsyncPrivacyModeShutdown("Newly added display has zero gamma table capacity");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply blackout to the new display immediately
|
||||
if (!ApplyBlackoutToDisplay(display)) {
|
||||
NSLog(@"DisplayReconfigurationCallback: Failed to blackout display %u (UUID: %s), exiting privacy mode", (unsigned)display, uuid.c_str());
|
||||
ScheduleAsyncPrivacyModeShutdown("Failed to blackout newly added display");
|
||||
return;
|
||||
}
|
||||
|
||||
// Schedule a delayed re-application to handle ColorSync restoration
|
||||
// macOS may restore default gamma for ALL displays after a new display is added,
|
||||
// so we need to reapply blackout to all online displays, not just the new one
|
||||
ScheduleDelayedBlackoutReapplication("after new display added");
|
||||
} else if (flags & kCGDisplayRemoveFlag) {
|
||||
// A display was removed - update our mapping and reapply blackout to remaining displays
|
||||
NSLog(@"Display %u removed during privacy mode", (unsigned)display);
|
||||
std::string uuid = GetDisplayUUID(display);
|
||||
(void)uuid; // UUID retrieved for potential future use or logging
|
||||
|
||||
// When a display is removed, macOS may reconfigure other displays and restore their gamma.
|
||||
// Schedule a delayed re-application of blackout to all remaining online displays.
|
||||
ScheduleDelayedBlackoutReapplication("after display removal");
|
||||
} else if (flags & kCGDisplaySetModeFlag) {
|
||||
// Display mode changed (resolution change, ColorSync/Night Shift interference, etc.)
|
||||
// macOS resets gamma to default when display mode changes, so we need to reapply blackout.
|
||||
// Schedule a delayed re-application because ColorSync restoration happens asynchronously.
|
||||
NSLog(@"Display %u mode changed during privacy mode, reapplying blackout", (unsigned)display);
|
||||
ScheduleDelayedBlackoutReapplication("after display mode change");
|
||||
}
|
||||
}
|
||||
|
||||
CGEventRef MyEventTapCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) {
|
||||
(void)proxy;
|
||||
(void)refcon;
|
||||
|
||||
// Handle EventTap being disabled by system timeout
|
||||
if (type == kCGEventTapDisabledByTimeout) {
|
||||
NSLog(@"EventTap was disabled by timeout, re-enabling");
|
||||
if (g_eventTap) {
|
||||
CGEventTapEnable(g_eventTap, true);
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
// Handle EventTap being disabled by user input
|
||||
if (type == kCGEventTapDisabledByUserInput) {
|
||||
NSLog(@"EventTap was disabled by user input, re-enabling");
|
||||
if (g_eventTap) {
|
||||
CGEventTapEnable(g_eventTap, true);
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
// Allow events explicitly injected by enigo (remote input), identified via custom user data.
|
||||
int64_t userData = CGEventGetIntegerValueField(event, kCGEventSourceUserData);
|
||||
if (userData == ENIGO_INPUT_EXTRA_VALUE) {
|
||||
return event;
|
||||
}
|
||||
// Block local physical HID input.
|
||||
if (CGEventGetIntegerValueField(event, kCGEventSourceStateID) == kCGEventSourceStateHIDSystemState) {
|
||||
return NULL;
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
// Helper function to set up EventTap on the main thread
|
||||
// Returns true if EventTap was successfully created and enabled
|
||||
static bool SetupEventTapOnMainThread() {
|
||||
__block bool success = false;
|
||||
|
||||
void (^setupBlock)(void) = ^{
|
||||
if (g_eventTap) {
|
||||
// Already set up
|
||||
success = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: kCGEventTapDisabledByTimeout and kCGEventTapDisabledByUserInput are special
|
||||
// notification types (0xFFFFFFFE and 0xFFFFFFFF) that are delivered via the callback's
|
||||
// type parameter, not through the event mask. They should NOT be included in eventMask
|
||||
// as bit-shifting by these values causes undefined behavior.
|
||||
CGEventMask eventMask = (1 << kCGEventKeyDown) | (1 << kCGEventKeyUp) |
|
||||
(1 << kCGEventLeftMouseDown) | (1 << kCGEventLeftMouseUp) |
|
||||
(1 << kCGEventRightMouseDown) | (1 << kCGEventRightMouseUp) |
|
||||
(1 << kCGEventOtherMouseDown) | (1 << kCGEventOtherMouseUp) |
|
||||
(1 << kCGEventLeftMouseDragged) | (1 << kCGEventRightMouseDragged) |
|
||||
(1 << kCGEventOtherMouseDragged) |
|
||||
(1 << kCGEventMouseMoved) | (1 << kCGEventScrollWheel);
|
||||
|
||||
g_eventTap = CGEventTapCreate(kCGHIDEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault,
|
||||
eventMask, MyEventTapCallback, NULL);
|
||||
if (g_eventTap) {
|
||||
g_runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, g_eventTap, 0);
|
||||
CFRunLoopAddSource(CFRunLoopGetMain(), g_runLoopSource, kCFRunLoopCommonModes);
|
||||
CGEventTapEnable(g_eventTap, true);
|
||||
success = true;
|
||||
} else {
|
||||
NSLog(@"MacSetPrivacyMode: Failed to create CGEventTap; input blocking not enabled.");
|
||||
success = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Execute on main thread to ensure CFRunLoop operations are safe.
|
||||
// Use dispatch_sync if not on main thread, otherwise execute directly to avoid deadlock.
|
||||
//
|
||||
// IMPORTANT: Potential deadlock consideration:
|
||||
// Using dispatch_sync while holding g_privacyModeMutex could deadlock if the main thread
|
||||
// tries to acquire g_privacyModeMutex. Currently this is safe because:
|
||||
// 1. MacSetPrivacyMode (which holds the mutex) is only called from background threads
|
||||
// 2. The main thread never directly calls MacSetPrivacyMode
|
||||
// If this assumption changes in the future, consider releasing the mutex before dispatch_sync
|
||||
// or restructuring the locking strategy.
|
||||
if ([NSThread isMainThread]) {
|
||||
setupBlock();
|
||||
} else {
|
||||
dispatch_sync(dispatch_get_main_queue(), setupBlock);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
// Helper function to tear down EventTap on the main thread
|
||||
static void TeardownEventTapOnMainThread() {
|
||||
void (^teardownBlock)(void) = ^{
|
||||
if (g_eventTap) {
|
||||
CGEventTapEnable(g_eventTap, false);
|
||||
CFRunLoopRemoveSource(CFRunLoopGetMain(), g_runLoopSource, kCFRunLoopCommonModes);
|
||||
CFRelease(g_runLoopSource);
|
||||
CFRelease(g_eventTap);
|
||||
g_eventTap = NULL;
|
||||
g_runLoopSource = NULL;
|
||||
}
|
||||
};
|
||||
|
||||
// Execute on main thread to ensure CFRunLoop operations are safe.
|
||||
//
|
||||
// NOTE: We use dispatch_sync here instead of dispatch_async because:
|
||||
// 1. TurnOffPrivacyModeInternal() expects EventTap to be fully torn down before
|
||||
// proceeding with gamma restoration - using async would cause race conditions.
|
||||
// 2. The caller (MacSetPrivacyMode) needs deterministic cleanup order.
|
||||
//
|
||||
// IMPORTANT: Potential deadlock consideration (same as SetupEventTapOnMainThread):
|
||||
// Using dispatch_sync while holding g_privacyModeMutex could deadlock if the main thread
|
||||
// tries to acquire g_privacyModeMutex. Currently this is safe because:
|
||||
// 1. MacSetPrivacyMode (which holds the mutex) is only called from background threads
|
||||
// 2. The main thread never directly calls MacSetPrivacyMode
|
||||
// If this assumption changes in the future, consider releasing the mutex before dispatch_sync
|
||||
// or restructuring the locking strategy.
|
||||
if ([NSThread isMainThread]) {
|
||||
teardownBlock();
|
||||
} else {
|
||||
dispatch_sync(dispatch_get_main_queue(), teardownBlock);
|
||||
}
|
||||
}
|
||||
|
||||
// Internal function to turn off privacy mode without acquiring the mutex
|
||||
// Must be called while holding g_privacyModeMutex
|
||||
static bool TurnOffPrivacyModeInternal() {
|
||||
if (!g_privacyModeActive) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 1. Unregister display reconfiguration callback
|
||||
CGDisplayRemoveReconfigurationCallback(DisplayReconfigurationCallback, NULL);
|
||||
|
||||
// 2. Input - restore (tear down EventTap on main thread)
|
||||
TeardownEventTapOnMainThread();
|
||||
|
||||
// 3. Gamma - restore using UUID to find current DisplayID
|
||||
bool restoreSuccess = RestoreAllGammas();
|
||||
|
||||
// 4. Fallback: Always call CGDisplayRestoreColorSyncSettings as a safety net
|
||||
// This ensures displays return to normal even if our restoration failed or
|
||||
// if the system (ColorSync/Night Shift) modified gamma during privacy mode
|
||||
CGDisplayRestoreColorSyncSettings();
|
||||
|
||||
// Clean up
|
||||
g_originalGammas.clear();
|
||||
g_privacyModeActive = false;
|
||||
g_privacyModeShutdownRequested = false;
|
||||
g_lastReconfigTimestamp = 0;
|
||||
g_blackoutReapplicationScheduled = false;
|
||||
|
||||
return restoreSuccess;
|
||||
}
|
||||
|
||||
extern "C" bool MacSetPrivacyMode(bool on) {
|
||||
std::lock_guard<std::mutex> lock(g_privacyModeMutex);
|
||||
if (on) {
|
||||
// Already in privacy mode
|
||||
if (g_privacyModeActive) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 1. Input Blocking - set up EventTap on main thread
|
||||
if (!SetupEventTapOnMainThread()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. Register display reconfiguration callback to handle hot-plug events
|
||||
CGDisplayRegisterReconfigurationCallback(DisplayReconfigurationCallback, NULL);
|
||||
|
||||
// 3. Gamma Blackout
|
||||
uint32_t count = 0;
|
||||
CGGetOnlineDisplayList(0, NULL, &count);
|
||||
std::vector<CGDirectDisplayID> displays(count);
|
||||
CGGetOnlineDisplayList(count, displays.data(), &count);
|
||||
|
||||
uint32_t blackoutSuccessCount = 0;
|
||||
uint32_t blackoutAttemptCount = 0;
|
||||
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
CGDirectDisplayID d = displays[i];
|
||||
std::string uuid = GetDisplayUUID(d);
|
||||
|
||||
if (uuid.empty()) {
|
||||
NSLog(@"MacSetPrivacyMode: Failed to get UUID for display %u, privacy mode requires all displays", (unsigned)d);
|
||||
// Privacy mode requires ALL connected displays to be successfully blacked out
|
||||
// to ensure user privacy. If we can't identify a display (no UUID),
|
||||
// we can't safely manage its state or restore it later.
|
||||
// Therefore, we must abort the entire operation and clean up any resources
|
||||
// already allocated (like event taps and reconfiguration callbacks).
|
||||
CGDisplayRemoveReconfigurationCallback(DisplayReconfigurationCallback, NULL);
|
||||
TeardownEventTapOnMainThread();
|
||||
// Restore gamma for displays that were already blacked out before this failure
|
||||
if (!RestoreAllGammas()) {
|
||||
// If any display failed to restore, use system reset as fallback
|
||||
CGDisplayRestoreColorSyncSettings();
|
||||
}
|
||||
g_originalGammas.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Save original gamma using UUID as key (stable across reconnections)
|
||||
if (g_originalGammas.find(uuid) == g_originalGammas.end()) {
|
||||
uint32_t capacity = CGDisplayGammaTableCapacity(d);
|
||||
if (capacity > 0) {
|
||||
std::vector<CGGammaValue> red(capacity), green(capacity), blue(capacity);
|
||||
uint32_t sampleCount = 0;
|
||||
if (CGGetDisplayTransferByTable(d, capacity, red.data(), green.data(), blue.data(), &sampleCount) == kCGErrorSuccess) {
|
||||
std::vector<CGGammaValue> all;
|
||||
all.insert(all.end(), red.begin(), red.begin() + sampleCount);
|
||||
all.insert(all.end(), green.begin(), green.begin() + sampleCount);
|
||||
all.insert(all.end(), blue.begin(), blue.begin() + sampleCount);
|
||||
g_originalGammas[uuid] = all;
|
||||
} else {
|
||||
NSLog(@"MacSetPrivacyMode: Failed to get gamma table for display %u (UUID: %s)", (unsigned)d, uuid.c_str());
|
||||
}
|
||||
} else {
|
||||
NSLog(@"MacSetPrivacyMode: Display %u (UUID: %s) has zero gamma table capacity, not supported", (unsigned)d, uuid.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// Set to black only if we have saved original gamma for this display
|
||||
if (g_originalGammas.find(uuid) != g_originalGammas.end()) {
|
||||
uint32_t capacity = CGDisplayGammaTableCapacity(d);
|
||||
if (capacity > 0) {
|
||||
std::vector<CGGammaValue> zeros(capacity, 0.0f);
|
||||
blackoutAttemptCount++;
|
||||
CGError error = CGSetDisplayTransferByTable(d, capacity, zeros.data(), zeros.data(), zeros.data());
|
||||
if (error != kCGErrorSuccess) {
|
||||
NSLog(@"MacSetPrivacyMode: Failed to blackout display (ID: %u, UUID: %s, error: %d)", (unsigned)d, uuid.c_str(), error);
|
||||
} else {
|
||||
blackoutSuccessCount++;
|
||||
}
|
||||
} else {
|
||||
NSLog(@"MacSetPrivacyMode: Display %u (UUID: %s) has zero gamma table capacity for blackout", (unsigned)d, uuid.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return false if any display failed to blackout - privacy mode requires ALL displays to be blacked out
|
||||
if (blackoutAttemptCount > 0 && blackoutSuccessCount < blackoutAttemptCount) {
|
||||
NSLog(@"MacSetPrivacyMode: Failed to blackout all displays (%u/%u succeeded)", blackoutSuccessCount, blackoutAttemptCount);
|
||||
// Clean up: unregister callback and disable event tap since we're failing
|
||||
CGDisplayRemoveReconfigurationCallback(DisplayReconfigurationCallback, NULL);
|
||||
TeardownEventTapOnMainThread();
|
||||
// Restore gamma for displays that were successfully blacked out
|
||||
if (!RestoreAllGammas()) {
|
||||
// If any display failed to restore, use system reset as fallback
|
||||
NSLog(@"Some displays failed to restore gamma during cleanup, using CGDisplayRestoreColorSyncSettings as fallback");
|
||||
CGDisplayRestoreColorSyncSettings();
|
||||
}
|
||||
g_originalGammas.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
g_privacyModeActive = true;
|
||||
return true;
|
||||
|
||||
} else {
|
||||
return TurnOffPrivacyModeInternal();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,9 +23,6 @@ pub mod win_mag;
|
||||
#[cfg(windows)]
|
||||
pub mod win_topmost_window;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod macos;
|
||||
|
||||
#[cfg(windows)]
|
||||
mod win_virtual_display;
|
||||
#[cfg(windows)]
|
||||
@@ -108,14 +105,7 @@ lazy_static::lazy_static! {
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
macos::PRIVACY_MODE_IMPL.to_owned()
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
"".to_owned()
|
||||
}
|
||||
"".to_owned()
|
||||
}
|
||||
};
|
||||
|
||||
@@ -137,13 +127,7 @@ pub type PrivacyModeCreator = fn(impl_key: &str) -> Box<dyn PrivacyMode>;
|
||||
lazy_static::lazy_static! {
|
||||
static ref PRIVACY_MODE_CREATOR: Arc<Mutex<HashMap<&'static str, PrivacyModeCreator>>> = {
|
||||
#[cfg(not(windows))]
|
||||
let mut map: HashMap<&'static str, PrivacyModeCreator> = HashMap::new();
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
map.insert(macos::PRIVACY_MODE_IMPL, |impl_key: &str| {
|
||||
Box::new(macos::PrivacyModeImpl::new(impl_key))
|
||||
});
|
||||
}
|
||||
let map: HashMap<&'static str, PrivacyModeCreator> = HashMap::new();
|
||||
#[cfg(windows)]
|
||||
let mut map: HashMap<&'static str, PrivacyModeCreator> = HashMap::new();
|
||||
#[cfg(windows)]
|
||||
@@ -349,14 +333,7 @@ pub fn get_supported_privacy_mode_impl() -> Vec<(&'static str, &'static str)> {
|
||||
|
||||
vec_impls
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// No translation is intended for privacy_mode_impl_macos_tip as it is a
|
||||
// placeholder for macOS specific privacy mode implementation which currently
|
||||
// doesn't provide multiple modes like Windows does.
|
||||
vec![(macos::PRIVACY_MODE_IMPL, "privacy_mode_impl_macos_tip")]
|
||||
}
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
use super::{PrivacyMode, PrivacyModeState};
|
||||
use hbb_common::{anyhow::anyhow, ResultType};
|
||||
|
||||
extern "C" {
|
||||
fn MacSetPrivacyMode(on: bool) -> bool;
|
||||
}
|
||||
|
||||
pub const PRIVACY_MODE_IMPL: &str = "privacy_mode_impl_macos";
|
||||
|
||||
pub struct PrivacyModeImpl {
|
||||
impl_key: String,
|
||||
conn_id: i32,
|
||||
}
|
||||
|
||||
impl PrivacyModeImpl {
|
||||
pub fn new(impl_key: &str) -> Self {
|
||||
Self {
|
||||
impl_key: impl_key.to_owned(),
|
||||
conn_id: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PrivacyMode for PrivacyModeImpl {
|
||||
fn is_async_privacy_mode(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn init(&self) -> ResultType<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
unsafe {
|
||||
MacSetPrivacyMode(false);
|
||||
}
|
||||
self.conn_id = 0;
|
||||
}
|
||||
|
||||
fn turn_on_privacy(&mut self, conn_id: i32) -> ResultType<bool> {
|
||||
if self.check_on_conn_id(conn_id)? {
|
||||
return Ok(true);
|
||||
}
|
||||
let success = unsafe { MacSetPrivacyMode(true) };
|
||||
if !success {
|
||||
return Err(anyhow!("Failed to turn on privacy mode"));
|
||||
}
|
||||
self.conn_id = conn_id;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn turn_off_privacy(&mut self, conn_id: i32, _state: Option<PrivacyModeState>) -> ResultType<()> {
|
||||
// Note: The `_state` parameter is intentionally ignored on macOS.
|
||||
// On Windows, it's used to notify the connection manager about privacy mode state changes
|
||||
// (see win_topmost_window.rs). macOS currently has a simpler single-mode implementation
|
||||
// without the need for such cross-component state synchronization.
|
||||
self.check_off_conn_id(conn_id)?;
|
||||
let success = unsafe { MacSetPrivacyMode(false) };
|
||||
if !success {
|
||||
return Err(anyhow!("Failed to turn off privacy mode"));
|
||||
}
|
||||
self.conn_id = 0;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pre_conn_id(&self) -> i32 {
|
||||
self.conn_id
|
||||
}
|
||||
|
||||
fn get_impl_key(&self) -> &str {
|
||||
&self.impl_key
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PrivacyModeImpl {
|
||||
fn drop(&mut self) {
|
||||
// Use the same cleanup logic as other code paths to keep conn_id consistent
|
||||
// and ensure all cleanup is centralized in one place.
|
||||
self.clear();
|
||||
}
|
||||
}
|
||||
@@ -1420,7 +1420,7 @@ impl Connection {
|
||||
pi.platform = "Android".into();
|
||||
}
|
||||
#[cfg(all(target_os = "macos", not(feature = "unix-file-copy-paste")))]
|
||||
let mut platform_additions = serde_json::Map::new();
|
||||
let platform_additions = serde_json::Map::new();
|
||||
#[cfg(any(
|
||||
target_os = "windows",
|
||||
target_os = "linux",
|
||||
@@ -1453,13 +1453,6 @@ impl Connection {
|
||||
json!(privacy_mode::get_supported_privacy_mode_impl()),
|
||||
);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
platform_additions.insert(
|
||||
"supported_privacy_mode_impl".into(),
|
||||
json!(privacy_mode::get_supported_privacy_mode_impl()),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
||||
{
|
||||
|
||||