Merge pull request #1273 from fufesou/flutter_desktop_remote_menus_rebase
Flutter desktop remote menus rebase
This commit is contained in:
commit
f812adedff
@ -4,3 +4,6 @@ const String kAppTypeDesktopRemote = "remote";
|
||||
const String kAppTypeDesktopFileTransfer = "file transfer";
|
||||
const String kTabLabelHomePage = "Home";
|
||||
const String kTabLabelSettingPage = "Settings";
|
||||
|
||||
const int kDefaultDisplayWidth = 1280;
|
||||
const int kDefaultDisplayHeight = 720;
|
||||
|
@ -41,6 +41,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
String _value = '';
|
||||
double _scale = 1;
|
||||
double _mouseScrollIntegral = 0; // mouse scroll speed controller
|
||||
var _cursorOverImage = false.obs;
|
||||
|
||||
var _more = true;
|
||||
var _fn = false;
|
||||
@ -256,7 +257,8 @@ class _RemotePageState extends State<RemotePage>
|
||||
}
|
||||
});
|
||||
}),
|
||||
bottomNavigationBar: _showBar && hasDisplays ? getBottomAppBar() : null,
|
||||
bottomNavigationBar:
|
||||
_showBar && hasDisplays ? getBottomAppBar(ffiModel) : null,
|
||||
body: Overlay(
|
||||
initialEntries: [
|
||||
OverlayEntry(builder: (context) {
|
||||
@ -274,7 +276,6 @@ class _RemotePageState extends State<RemotePage>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
_ffi.canvasModel.tabBarHeight = super.widget.tabBarHeight;
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
clientClose(_ffi.dialogManager);
|
||||
@ -292,201 +293,148 @@ class _RemotePageState extends State<RemotePage>
|
||||
}
|
||||
|
||||
Widget getRawPointerAndKeyBody(Widget child) {
|
||||
return Listener(
|
||||
onPointerHover: (e) {
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||
if (!_isPhysicalMouse) {
|
||||
setState(() {
|
||||
_isPhysicalMouse = true;
|
||||
});
|
||||
}
|
||||
if (_isPhysicalMouse) {
|
||||
_ffi.handleMouse(getEvent(e, 'mousemove'),
|
||||
tabBarHeight: super.widget.tabBarHeight);
|
||||
}
|
||||
},
|
||||
onPointerDown: (e) {
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) {
|
||||
if (_isPhysicalMouse) {
|
||||
setState(() {
|
||||
_isPhysicalMouse = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
if (_isPhysicalMouse) {
|
||||
_ffi.handleMouse(getEvent(e, 'mousedown'),
|
||||
tabBarHeight: super.widget.tabBarHeight);
|
||||
}
|
||||
},
|
||||
onPointerUp: (e) {
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||
if (_isPhysicalMouse) {
|
||||
_ffi.handleMouse(getEvent(e, 'mouseup'),
|
||||
tabBarHeight: super.widget.tabBarHeight);
|
||||
}
|
||||
},
|
||||
onPointerMove: (e) {
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||
if (_isPhysicalMouse) {
|
||||
_ffi.handleMouse(getEvent(e, 'mousemove'),
|
||||
tabBarHeight: super.widget.tabBarHeight);
|
||||
}
|
||||
},
|
||||
onPointerSignal: (e) {
|
||||
if (e is PointerScrollEvent) {
|
||||
var dx = e.scrollDelta.dx;
|
||||
var dy = e.scrollDelta.dy;
|
||||
if (dx > 0)
|
||||
dx = -1;
|
||||
else if (dx < 0) dx = 1;
|
||||
if (dy > 0)
|
||||
dy = -1;
|
||||
else if (dy < 0) dy = 1;
|
||||
bind.sessionSendMouse(
|
||||
id: widget.id,
|
||||
msg: '{"type": "wheel", "x": "$dx", "y": "$dy"}');
|
||||
}
|
||||
},
|
||||
child: Consumer<FfiModel>(
|
||||
builder: (context, FfiModel, _child) => MouseRegion(
|
||||
cursor: FfiModel.permissions['keyboard'] != false
|
||||
? SystemMouseCursors.none
|
||||
: MouseCursor.defer,
|
||||
child: FocusScope(
|
||||
return Consumer<FfiModel>(
|
||||
builder: (context, FfiModel, _child) => MouseRegion(
|
||||
cursor: FfiModel.permissions['keyboard'] != false
|
||||
? SystemMouseCursors.none
|
||||
: MouseCursor.defer,
|
||||
child: FocusScope(
|
||||
autofocus: true,
|
||||
child: Focus(
|
||||
autofocus: true,
|
||||
child: Focus(
|
||||
autofocus: true,
|
||||
canRequestFocus: true,
|
||||
focusNode: _physicalFocusNode,
|
||||
onKey: (data, e) {
|
||||
final key = e.logicalKey;
|
||||
if (e is RawKeyDownEvent) {
|
||||
if (e.repeat) {
|
||||
sendRawKey(e, press: true);
|
||||
} else {
|
||||
if (e.isAltPressed && !_ffi.alt) {
|
||||
_ffi.alt = true;
|
||||
} else if (e.isControlPressed && !_ffi.ctrl) {
|
||||
_ffi.ctrl = true;
|
||||
} else if (e.isShiftPressed && !_ffi.shift) {
|
||||
_ffi.shift = true;
|
||||
} else if (e.isMetaPressed && !_ffi.command) {
|
||||
_ffi.command = true;
|
||||
}
|
||||
sendRawKey(e, down: true);
|
||||
}
|
||||
canRequestFocus: true,
|
||||
focusNode: _physicalFocusNode,
|
||||
onKey: (data, e) {
|
||||
final key = e.logicalKey;
|
||||
if (e is RawKeyDownEvent) {
|
||||
if (e.repeat) {
|
||||
sendRawKey(e, press: true);
|
||||
} else {
|
||||
if (e.isAltPressed && !_ffi.alt) {
|
||||
_ffi.alt = true;
|
||||
} else if (e.isControlPressed && !_ffi.ctrl) {
|
||||
_ffi.ctrl = true;
|
||||
} else if (e.isShiftPressed && !_ffi.shift) {
|
||||
_ffi.shift = true;
|
||||
} else if (e.isMetaPressed && !_ffi.command) {
|
||||
_ffi.command = true;
|
||||
}
|
||||
// [!_showEdit] workaround for soft-keyboard's control_key like Backspace / Enter
|
||||
if (!_showEdit && e is RawKeyUpEvent) {
|
||||
if (key == LogicalKeyboardKey.altLeft ||
|
||||
key == LogicalKeyboardKey.altRight) {
|
||||
_ffi.alt = false;
|
||||
} else if (key == LogicalKeyboardKey.controlLeft ||
|
||||
key == LogicalKeyboardKey.controlRight) {
|
||||
_ffi.ctrl = false;
|
||||
} else if (key == LogicalKeyboardKey.shiftRight ||
|
||||
key == LogicalKeyboardKey.shiftLeft) {
|
||||
_ffi.shift = false;
|
||||
} else if (key == LogicalKeyboardKey.metaLeft ||
|
||||
key == LogicalKeyboardKey.metaRight) {
|
||||
_ffi.command = false;
|
||||
}
|
||||
sendRawKey(e);
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
},
|
||||
child: child)))));
|
||||
sendRawKey(e, down: true);
|
||||
}
|
||||
}
|
||||
// [!_showEdit] workaround for soft-keyboard's control_key like Backspace / Enter
|
||||
if (!_showEdit && e is RawKeyUpEvent) {
|
||||
if (key == LogicalKeyboardKey.altLeft ||
|
||||
key == LogicalKeyboardKey.altRight) {
|
||||
_ffi.alt = false;
|
||||
} else if (key == LogicalKeyboardKey.controlLeft ||
|
||||
key == LogicalKeyboardKey.controlRight) {
|
||||
_ffi.ctrl = false;
|
||||
} else if (key == LogicalKeyboardKey.shiftRight ||
|
||||
key == LogicalKeyboardKey.shiftLeft) {
|
||||
_ffi.shift = false;
|
||||
} else if (key == LogicalKeyboardKey.metaLeft ||
|
||||
key == LogicalKeyboardKey.metaRight) {
|
||||
_ffi.command = false;
|
||||
}
|
||||
sendRawKey(e);
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
},
|
||||
child: child))));
|
||||
}
|
||||
|
||||
Widget? getBottomAppBar() {
|
||||
return BottomAppBar(
|
||||
elevation: 10,
|
||||
color: MyTheme.accent,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
clientClose(_ffi.dialogManager);
|
||||
},
|
||||
)
|
||||
] +
|
||||
<Widget>[
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.tv),
|
||||
onPressed: () {
|
||||
setState(() => _showEdit = false);
|
||||
showOptions(widget.id, _ffi.dialogManager);
|
||||
},
|
||||
)
|
||||
] +
|
||||
(isWebDesktop
|
||||
? []
|
||||
: _ffi.ffiModel.isPeerAndroid
|
||||
? [
|
||||
Widget? getBottomAppBar(FfiModel ffiModel) {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.basic,
|
||||
child: BottomAppBar(
|
||||
elevation: 10,
|
||||
color: MyTheme.accent,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
clientClose(_ffi.dialogManager);
|
||||
},
|
||||
)
|
||||
] +
|
||||
<Widget>[
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.tv),
|
||||
onPressed: () {
|
||||
setState(() => _showEdit = false);
|
||||
showOptions(widget.id, _ffi.dialogManager);
|
||||
},
|
||||
)
|
||||
] +
|
||||
(isWebDesktop
|
||||
? []
|
||||
: _ffi.ffiModel.isPeerAndroid
|
||||
? [
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.build),
|
||||
onPressed: () {
|
||||
if (mobileActionsOverlayEntry == null) {
|
||||
showMobileActionsOverlay();
|
||||
} else {
|
||||
hideMobileActionsOverlay();
|
||||
}
|
||||
},
|
||||
)
|
||||
]
|
||||
: [
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.keyboard),
|
||||
onPressed: openKeyboard),
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(_ffi.ffiModel.touchMode
|
||||
? Icons.touch_app
|
||||
: Icons.mouse),
|
||||
onPressed: changeTouchMode,
|
||||
),
|
||||
]) +
|
||||
(isWeb
|
||||
? []
|
||||
: <Widget>[
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.build),
|
||||
icon: Icon(Icons.message),
|
||||
onPressed: () {
|
||||
if (mobileActionsOverlayEntry == null) {
|
||||
showMobileActionsOverlay();
|
||||
} else {
|
||||
hideMobileActionsOverlay();
|
||||
}
|
||||
_ffi.chatModel
|
||||
.changeCurrentID(ChatModel.clientModeID);
|
||||
_ffi.chatModel.toggleChatOverlay();
|
||||
},
|
||||
)
|
||||
]
|
||||
: [
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.keyboard),
|
||||
onPressed: openKeyboard),
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(_ffi.ffiModel.touchMode
|
||||
? Icons.touch_app
|
||||
: Icons.mouse),
|
||||
onPressed: changeTouchMode,
|
||||
),
|
||||
]) +
|
||||
(isWeb
|
||||
? []
|
||||
: <Widget>[
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.message),
|
||||
onPressed: () {
|
||||
_ffi.chatModel
|
||||
.changeCurrentID(ChatModel.clientModeID);
|
||||
_ffi.chatModel.toggleChatOverlay();
|
||||
},
|
||||
)
|
||||
]) +
|
||||
[
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.more_vert),
|
||||
onPressed: () {
|
||||
setState(() => _showEdit = false);
|
||||
showActions(widget.id);
|
||||
},
|
||||
),
|
||||
]),
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.expand_more),
|
||||
onPressed: () {
|
||||
setState(() => _showBar = !_showBar);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
[
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.more_vert),
|
||||
onPressed: () {
|
||||
setState(() => _showEdit = false);
|
||||
showActions(widget.id, ffiModel);
|
||||
},
|
||||
),
|
||||
]),
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.expand_more),
|
||||
onPressed: () {
|
||||
setState(() => _showBar = !_showBar);
|
||||
}),
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
/// touchMode only:
|
||||
@ -499,6 +447,81 @@ class _RemotePageState extends State<RemotePage>
|
||||
/// DoubleFiner -> right click
|
||||
/// HoldDrag -> left drag
|
||||
|
||||
void _onPointHoverImage(PointerHoverEvent e) {
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||
if (!_isPhysicalMouse) {
|
||||
setState(() {
|
||||
_isPhysicalMouse = true;
|
||||
});
|
||||
}
|
||||
if (_isPhysicalMouse) {
|
||||
_ffi.handleMouse(getEvent(e, 'mousemove'),
|
||||
tabBarHeight: super.widget.tabBarHeight);
|
||||
}
|
||||
}
|
||||
|
||||
void _onPointDownImage(PointerDownEvent e) {
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) {
|
||||
if (_isPhysicalMouse) {
|
||||
setState(() {
|
||||
_isPhysicalMouse = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
if (_isPhysicalMouse) {
|
||||
_ffi.handleMouse(getEvent(e, 'mousedown'),
|
||||
tabBarHeight: super.widget.tabBarHeight);
|
||||
}
|
||||
}
|
||||
|
||||
void _onPointUpImage(PointerUpEvent e) {
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||
if (_isPhysicalMouse) {
|
||||
_ffi.handleMouse(getEvent(e, 'mouseup'),
|
||||
tabBarHeight: super.widget.tabBarHeight);
|
||||
}
|
||||
}
|
||||
|
||||
void _onPointMoveImage(PointerMoveEvent e) {
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||
if (_isPhysicalMouse) {
|
||||
_ffi.handleMouse(getEvent(e, 'mousemove'),
|
||||
tabBarHeight: super.widget.tabBarHeight);
|
||||
}
|
||||
}
|
||||
|
||||
void _onPointerSignalImage(PointerSignalEvent e) {
|
||||
if (e is PointerScrollEvent) {
|
||||
var dx = e.scrollDelta.dx.toInt();
|
||||
var dy = e.scrollDelta.dy.toInt();
|
||||
if (dx > 0)
|
||||
dx = -1;
|
||||
else if (dx < 0) dx = 1;
|
||||
if (dy > 0)
|
||||
dy = -1;
|
||||
else if (dy < 0) dy = 1;
|
||||
bind.sessionSendMouse(
|
||||
id: widget.id, msg: '{"type": "wheel", "x": "$dx", "y": "$dy"}');
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildImageListener(Widget child) {
|
||||
return Listener(
|
||||
onPointerHover: _onPointHoverImage,
|
||||
onPointerDown: _onPointDownImage,
|
||||
onPointerUp: _onPointUpImage,
|
||||
onPointerMove: _onPointMoveImage,
|
||||
onPointerSignal: _onPointerSignalImage,
|
||||
child: MouseRegion(
|
||||
onEnter: (evt) {
|
||||
_cursorOverImage.value = true;
|
||||
},
|
||||
onExit: (evt) {
|
||||
_cursorOverImage.value = false;
|
||||
},
|
||||
child: child));
|
||||
}
|
||||
|
||||
Widget getBodyForDesktop(BuildContext context, bool keyboard) {
|
||||
var paints = <Widget>[
|
||||
MouseRegion(onEnter: (evt) {
|
||||
@ -512,6 +535,8 @@ class _RemotePageState extends State<RemotePage>
|
||||
});
|
||||
return ImagePaint(
|
||||
id: widget.id,
|
||||
cursorOverImage: _cursorOverImage,
|
||||
listenerBuilder: _buildImageListener,
|
||||
);
|
||||
}),
|
||||
))
|
||||
@ -550,7 +575,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
return out;
|
||||
}
|
||||
|
||||
void showActions(String id) async {
|
||||
void showActions(String id, FfiModel ffiModel) async {
|
||||
final size = MediaQuery.of(context).size;
|
||||
final x = 120.0;
|
||||
final y = size.height - super.widget.tabBarHeight;
|
||||
@ -595,12 +620,8 @@ class _RemotePageState extends State<RemotePage>
|
||||
await bind.getSessionToggleOption(id: id, arg: 'privacy-mode') !=
|
||||
true) {
|
||||
more.add(PopupMenuItem<String>(
|
||||
child: Consumer<FfiModel>(
|
||||
builder: (_context, ffiModel, _child) => () {
|
||||
return Text(translate(
|
||||
(ffiModel.inputBlocked ? 'Unb' : 'B') +
|
||||
'lock user input'));
|
||||
}()),
|
||||
child: Text(translate(
|
||||
(ffiModel.inputBlocked ? 'Unb' : 'B') + 'lock user input')),
|
||||
value: 'block-input'));
|
||||
}
|
||||
}
|
||||
@ -630,6 +651,9 @@ class _RemotePageState extends State<RemotePage>
|
||||
}
|
||||
}();
|
||||
} else if (value == 'enter_os_password') {
|
||||
// FIXME:
|
||||
// null means no session of id
|
||||
// empty string means no password
|
||||
var password = await bind.getSessionOption(id: id, arg: "os-password");
|
||||
if (password != null) {
|
||||
bind.sessionInputOsPassword(id: widget.id, value: password);
|
||||
@ -822,18 +846,95 @@ class _RemotePageState extends State<RemotePage>
|
||||
|
||||
class ImagePaint extends StatelessWidget {
|
||||
final String id;
|
||||
final Rx<bool> cursorOverImage;
|
||||
final Widget Function(Widget)? listenerBuilder;
|
||||
final ScrollController _horizontal = ScrollController();
|
||||
final ScrollController _vertical = ScrollController();
|
||||
|
||||
const ImagePaint({Key? key, required this.id}) : super(key: key);
|
||||
ImagePaint(
|
||||
{Key? key,
|
||||
required this.id,
|
||||
required this.cursorOverImage,
|
||||
this.listenerBuilder = null})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final m = Provider.of<ImageModel>(context);
|
||||
final c = Provider.of<CanvasModel>(context);
|
||||
var s = c.scale;
|
||||
return CustomPaint(
|
||||
painter:
|
||||
new ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s),
|
||||
);
|
||||
var c = Provider.of<CanvasModel>(context);
|
||||
final s = c.scale;
|
||||
if (c.scrollStyle == ScrollStyle.scrollbar) {
|
||||
final imageWidget = SizedBox(
|
||||
width: c.getDisplayWidth() * s,
|
||||
height: c.getDisplayHeight() * s,
|
||||
child: CustomPaint(
|
||||
painter: new ImagePainter(image: m.image, x: 0, y: 0, scale: s),
|
||||
));
|
||||
return Center(
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
onNotification: (_notification) {
|
||||
final percentX = _horizontal.position.extentBefore /
|
||||
(_horizontal.position.extentBefore +
|
||||
_horizontal.position.extentInside +
|
||||
_horizontal.position.extentAfter);
|
||||
final percentY = _vertical.position.extentBefore /
|
||||
(_vertical.position.extentBefore +
|
||||
_vertical.position.extentInside +
|
||||
_vertical.position.extentAfter);
|
||||
c.setScrollPercent(percentX, percentY);
|
||||
return false;
|
||||
},
|
||||
child: Obx(() => MouseRegion(
|
||||
cursor: cursorOverImage.value
|
||||
? SystemMouseCursors.none
|
||||
: SystemMouseCursors.basic,
|
||||
child: _buildCrossScrollbar(_buildListener(imageWidget)))),
|
||||
));
|
||||
} else {
|
||||
final imageWidget = SizedBox(
|
||||
width: c.size.width,
|
||||
height: c.size.height,
|
||||
child: CustomPaint(
|
||||
painter: new ImagePainter(
|
||||
image: m.image, x: c.x / s, y: c.y / s, scale: s),
|
||||
));
|
||||
return _buildListener(imageWidget);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildCrossScrollbar(Widget child) {
|
||||
final physicsVertical =
|
||||
cursorOverImage.value ? const NeverScrollableScrollPhysics() : null;
|
||||
final physicsHorizontal =
|
||||
cursorOverImage.value ? const NeverScrollableScrollPhysics() : null;
|
||||
return Scrollbar(
|
||||
controller: _vertical,
|
||||
thumbVisibility: true,
|
||||
trackVisibility: true,
|
||||
child: Scrollbar(
|
||||
controller: _horizontal,
|
||||
thumbVisibility: true,
|
||||
trackVisibility: true,
|
||||
notificationPredicate: (notif) => notif.depth == 1,
|
||||
child: SingleChildScrollView(
|
||||
controller: _vertical,
|
||||
physics: physicsVertical,
|
||||
child: SingleChildScrollView(
|
||||
controller: _horizontal,
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: physicsHorizontal,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
Widget _buildListener(Widget child) {
|
||||
if (listenerBuilder != null) {
|
||||
return listenerBuilder!(child);
|
||||
} else {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -896,6 +997,8 @@ void showOptions(String id, OverlayDialogManager dialogManager) async {
|
||||
if (quality == '') quality = 'balanced';
|
||||
String viewStyle =
|
||||
await bind.getSessionOption(id: id, arg: 'view-style') ?? '';
|
||||
String scrollStyle =
|
||||
await bind.getSessionOption(id: id, arg: 'scroll-style') ?? '';
|
||||
var displays = <Widget>[];
|
||||
final pi = ffi(id).ffiModel.pi;
|
||||
final image = ffi(id).ffiModel.getConnectionImage();
|
||||
@ -968,6 +1071,14 @@ void showOptions(String id, OverlayDialogManager dialogManager) async {
|
||||
ffi(id).canvasModel.updateViewStyle();
|
||||
});
|
||||
};
|
||||
var setScrollStyle = (String? value) {
|
||||
if (value == null) return;
|
||||
setState(() {
|
||||
scrollStyle = value;
|
||||
bind.sessionPeerOption(id: id, name: "scroll-style", value: value);
|
||||
ffi(id).canvasModel.updateScrollStyle();
|
||||
});
|
||||
};
|
||||
return CustomAlertDialog(
|
||||
title: SizedBox.shrink(),
|
||||
content: Column(
|
||||
@ -978,6 +1089,10 @@ void showOptions(String id, OverlayDialogManager dialogManager) async {
|
||||
getRadio('Shrink', 'shrink', viewStyle, setViewStyle),
|
||||
getRadio('Stretch', 'stretch', viewStyle, setViewStyle),
|
||||
Divider(color: MyTheme.border),
|
||||
getRadio(
|
||||
'ScrollAuto', 'scrollauto', scrollStyle, setScrollStyle),
|
||||
getRadio('Scrollbar', 'scrollbar', scrollStyle, setScrollStyle),
|
||||
Divider(color: MyTheme.border),
|
||||
getRadio('Good image quality', 'best', quality, setQuality),
|
||||
getRadio('Balanced', 'balanced', quality, setQuality),
|
||||
getRadio('Optimize reaction time', 'low', quality, setQuality),
|
||||
|
@ -735,6 +735,9 @@ class _RemotePageState extends State<RemotePage> {
|
||||
}
|
||||
}();
|
||||
} else if (value == 'enter_os_password') {
|
||||
// FIXME:
|
||||
// null means no session of id
|
||||
// empty string means no password
|
||||
var password = await bind.getSessionOption(id: id, arg: "os-password");
|
||||
if (password != null) {
|
||||
bind.sessionInputOsPassword(id: widget.id, value: password);
|
||||
|
@ -387,8 +387,9 @@ class ImageModel with ChangeNotifier {
|
||||
|
||||
void update(ui.Image? image, double tabBarHeight) {
|
||||
if (_image == null && image != null) {
|
||||
if (isWebDesktop) {
|
||||
if (isWebDesktop || isDesktop) {
|
||||
parent.target?.canvasModel.updateViewStyle();
|
||||
parent.target?.canvasModel.updateScrollStyle();
|
||||
} else {
|
||||
final size = MediaQueryData.fromWindow(ui.window).size;
|
||||
final canvasWidth = size.width;
|
||||
@ -432,36 +433,52 @@ class ImageModel with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
enum ScrollStyle {
|
||||
scrollbar,
|
||||
scrollauto,
|
||||
}
|
||||
|
||||
class CanvasModel with ChangeNotifier {
|
||||
// scroll offset x percent
|
||||
double _scrollX = 0.0;
|
||||
// scroll offset y percent
|
||||
double _scrollY = 0.0;
|
||||
double _x = 0;
|
||||
double _y = 0;
|
||||
double _scale = 1.0;
|
||||
double _tabBarHeight = 0.0;
|
||||
String id = ""; // TODO multi canvas model
|
||||
ScrollStyle _scrollStyle = ScrollStyle.scrollauto;
|
||||
|
||||
WeakReference<FFI> parent;
|
||||
|
||||
CanvasModel(this.parent);
|
||||
|
||||
double get x => _x;
|
||||
|
||||
double get y => _y;
|
||||
|
||||
double get scale => _scale;
|
||||
ScrollStyle get scrollStyle => _scrollStyle;
|
||||
|
||||
setScrollPercent(double x, double y) {
|
||||
_scrollX = x;
|
||||
_scrollY = y;
|
||||
}
|
||||
|
||||
double get scrollX => _scrollX;
|
||||
double get scrollY => _scrollY;
|
||||
|
||||
set tabBarHeight(double h) => _tabBarHeight = h;
|
||||
double get tabBarHeight => _tabBarHeight;
|
||||
|
||||
void updateViewStyle() async {
|
||||
final s = await bind.getSessionOption(id: id, arg: 'view-style');
|
||||
if (s == null) {
|
||||
final style = await bind.getSessionOption(id: id, arg: 'view-style');
|
||||
if (style == null) {
|
||||
return;
|
||||
}
|
||||
final size = MediaQueryData.fromWindow(ui.window).size;
|
||||
final canvasWidth = size.width;
|
||||
final canvasHeight = size.height - _tabBarHeight;
|
||||
final s1 = canvasWidth / (parent.target?.ffiModel.display.width ?? 720);
|
||||
final s2 = canvasHeight / (parent.target?.ffiModel.display.height ?? 1280);
|
||||
|
||||
final s1 = size.width / (parent.target?.ffiModel.display.width ?? 720);
|
||||
final s2 = size.height / (parent.target?.ffiModel.display.height ?? 1280);
|
||||
|
||||
// Closure to perform shrink operation.
|
||||
final shrinkOp = () {
|
||||
final s = s1 < s2 ? s1 : s2;
|
||||
@ -471,7 +488,7 @@ class CanvasModel with ChangeNotifier {
|
||||
};
|
||||
// Closure to perform stretch operation.
|
||||
final stretchOp = () {
|
||||
final s = s1 > s2 ? s1 : s2;
|
||||
final s = s1 < s2 ? s1 : s2;
|
||||
if (s > 1) {
|
||||
_scale = s;
|
||||
}
|
||||
@ -480,20 +497,34 @@ class CanvasModel with ChangeNotifier {
|
||||
final defaultOp = () {
|
||||
_scale = 1.0;
|
||||
};
|
||||
if (s == 'shrink') {
|
||||
|
||||
// // On desktop, shrink is the default behavior.
|
||||
// if (isDesktop) {
|
||||
// shrinkOp();
|
||||
// } else {
|
||||
defaultOp();
|
||||
// }
|
||||
|
||||
if (style == 'shrink') {
|
||||
shrinkOp();
|
||||
} else if (s == 'stretch') {
|
||||
} else if (style == 'stretch') {
|
||||
stretchOp();
|
||||
} else {
|
||||
// On desktop, shrink is the default behavior.
|
||||
if (isDesktop) {
|
||||
shrinkOp();
|
||||
} else {
|
||||
defaultOp();
|
||||
}
|
||||
}
|
||||
_x = (canvasWidth - getDisplayWidth() * _scale) / 2;
|
||||
_y = (canvasHeight - getDisplayHeight() * _scale) / 2;
|
||||
|
||||
_x = (size.width - getDisplayWidth() * _scale) / 2;
|
||||
_y = (size.height - getDisplayHeight() * _scale) / 2;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
updateScrollStyle() async {
|
||||
final style = await bind.getSessionOption(id: id, arg: 'scroll-style');
|
||||
if (style == 'scrollbar') {
|
||||
_scrollStyle = ScrollStyle.scrollbar;
|
||||
_scrollX = 0.0;
|
||||
_scrollY = 0.0;
|
||||
} else {
|
||||
_scrollStyle = ScrollStyle.scrollauto;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@ -512,28 +543,30 @@ class CanvasModel with ChangeNotifier {
|
||||
return parent.target?.ffiModel.display.height ?? 720;
|
||||
}
|
||||
|
||||
Size get size {
|
||||
final size = MediaQueryData.fromWindow(ui.window).size;
|
||||
return Size(size.width, size.height - _tabBarHeight);
|
||||
}
|
||||
|
||||
void moveDesktopMouse(double x, double y) {
|
||||
// On mobile platforms, move the canvas with the cursor.
|
||||
if (!isDesktop) {
|
||||
final size = MediaQueryData.fromWindow(ui.window).size;
|
||||
final canvasWidth = size.width;
|
||||
final canvasHeight = size.height - _tabBarHeight;
|
||||
final dw = getDisplayWidth() * _scale;
|
||||
final dh = getDisplayHeight() * _scale;
|
||||
var dxOffset = 0;
|
||||
var dyOffset = 0;
|
||||
if (dw > canvasWidth) {
|
||||
dxOffset = (x - dw * (x / canvasWidth) - _x).toInt();
|
||||
}
|
||||
if (dh > canvasHeight) {
|
||||
dyOffset = (y - dh * (y / canvasHeight) - _y).toInt();
|
||||
}
|
||||
_x += dxOffset;
|
||||
_y += dyOffset;
|
||||
if (dxOffset != 0 || dyOffset != 0) {
|
||||
notifyListeners();
|
||||
}
|
||||
//if (!isDesktop) {
|
||||
final dw = getDisplayWidth() * _scale;
|
||||
final dh = getDisplayHeight() * _scale;
|
||||
var dxOffset = 0;
|
||||
var dyOffset = 0;
|
||||
if (dw > size.width) {
|
||||
dxOffset = (x - dw * (x / size.width) - _x).toInt();
|
||||
}
|
||||
if (dh > size.height) {
|
||||
dyOffset = (y - dh * (y / size.height) - _y).toInt();
|
||||
}
|
||||
_x += dxOffset;
|
||||
_y += dyOffset;
|
||||
if (dxOffset != 0 || dyOffset != 0) {
|
||||
notifyListeners();
|
||||
}
|
||||
//}
|
||||
parent.target?.cursorModel.moveLocal(x, y);
|
||||
}
|
||||
|
||||
@ -1091,8 +1124,24 @@ class FFI {
|
||||
canvasModel.moveDesktopMouse(x, y);
|
||||
}
|
||||
final d = ffiModel.display;
|
||||
x -= canvasModel.x;
|
||||
y -= canvasModel.y;
|
||||
if (canvasModel.scrollStyle == ScrollStyle.scrollbar) {
|
||||
final imageWidth = d.width * canvasModel.scale;
|
||||
final imageHeight = d.height * canvasModel.scale;
|
||||
x += imageWidth * canvasModel.scrollX;
|
||||
y += imageHeight * canvasModel.scrollY;
|
||||
|
||||
// boxed size is a center widget
|
||||
if (canvasModel.size.width > imageWidth) {
|
||||
x -= ((canvasModel.size.width - imageWidth) / 2);
|
||||
}
|
||||
if (canvasModel.size.height > imageHeight) {
|
||||
y -= ((canvasModel.size.height - imageHeight) / 2);
|
||||
}
|
||||
} else {
|
||||
x -= canvasModel.x;
|
||||
y -= canvasModel.y;
|
||||
}
|
||||
|
||||
if (!isMove && (x < 0 || x > d.width || y < 0 || y > d.height)) {
|
||||
return;
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Original", "原始比例"),
|
||||
("Shrink", "收缩"),
|
||||
("Stretch", "伸展"),
|
||||
("Scrollbar", "滚动条"),
|
||||
("ScrollAuto", "自动滚动"),
|
||||
("Good image quality", "好画质"),
|
||||
("Balanced", "一般画质"),
|
||||
("Optimize reaction time", "优化反应时间"),
|
||||
|
@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Original", "Původní"),
|
||||
("Shrink", "Oříznout"),
|
||||
("Stretch", "Roztáhnout"),
|
||||
("Scrollbar", "Posuvník"),
|
||||
("ScrollAuto", "Rolovať Auto"),
|
||||
("Good image quality", "Dobrá kvalita obrazu"),
|
||||
("Balanced", "Vyvážené"),
|
||||
("Optimize reaction time", "Optimalizovat pro co nejnižší prodlevu odezvy"),
|
||||
|
@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Original", "Original"),
|
||||
("Shrink", "Krymp"),
|
||||
("Stretch", "Strak"),
|
||||
("Scrollbar", "Rullebar"),
|
||||
("ScrollAuto", "Rul Auto"),
|
||||
("Good image quality", "God billedkvalitet"),
|
||||
("Balanced", "Afbalanceret"),
|
||||
("Optimize reaction time", "Optimeret responstid"),
|
||||
|
@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Original", "Original"),
|
||||
("Shrink", "Verkleinern"),
|
||||
("Stretch", "Strecken"),
|
||||
("Scrollbar", "Scrollleiste"),
|
||||
("ScrollAuto", "Automatisch scrollen"),
|
||||
("Good image quality", "Schöner"),
|
||||
("Balanced", "Ausgeglichen"),
|
||||
("Optimize reaction time", "Schneller"),
|
||||
|
@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Original", "Originala rilatumo"),
|
||||
("Shrink", "Ŝrumpi"),
|
||||
("Stretch", "Streĉi"),
|
||||
("Scrollbar", "Rulumbreto"),
|
||||
("ScrollAuto", "Rulumu Aŭtomate"),
|
||||
("Good image quality", "Bona bilda kvalito"),
|
||||
("Balanced", "Normala bilda kvalito"),
|
||||
("Optimize reaction time", "Optimigi reakcia tempo"),
|
||||
|
@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Original", "Original"),
|
||||
("Shrink", "Encogerse"),
|
||||
("Stretch", "Estirar"),
|
||||
("Scrollbar", "Barra de desplazamiento"),
|
||||
("ScrollAuto", "Desplazamiento automático"),
|
||||
("Good image quality", "Buena calidad de imagen"),
|
||||
("Balanced", "Equilibrado"),
|
||||
("Optimize reaction time", "Optimizar el tiempo de reacción"),
|
||||
|
@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Original", "Ratio d'origine"),
|
||||
("Shrink", "Rétrécir"),
|
||||
("Stretch", "Étirer"),
|
||||
("Scrollbar", "Barre de défilement"),
|
||||
("ScrollAuto", "Défilement automatique"),
|
||||
("Good image quality", "Bonne qualité d'image"),
|
||||
("Balanced", "Qualité d'image normale"),
|
||||
("Optimize reaction time", "Optimiser le temps de réaction"),
|
||||
|
@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Original", "Eredeti"),
|
||||
("Shrink", "Zsugorított"),
|
||||
("Stretch", "Nyújtott"),
|
||||
("Scrollbar", "Görgetősáv"),
|
||||
("ScrollAuto", "Görgessen Auto"),
|
||||
("Good image quality", "Jó képminőség"),
|
||||
("Balanced", "Balanszolt"),
|
||||
("Optimize reaction time", "Válaszidő optimializálása"),
|
||||
|
@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Original", "Original"),
|
||||
("Shrink", "Susutkan"),
|
||||
("Stretch", "Regangkan"),
|
||||
("Scrollbar", "Scroll bar"),
|
||||
("ScrollAuto", "Gulir Otomatis"),
|
||||
("Good image quality", "Kualitas Gambar Baik"),
|
||||
("Balanced", "Seimbang"),
|
||||
("Optimize reaction time", "Optimalkan waktu reaksi"),
|
||||
|
@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Original", "Originale"),
|
||||
("Shrink", "Restringi"),
|
||||
("Stretch", "Allarga"),
|
||||
("Scrollbar", "Barra di scorrimento"),
|
||||
("ScrollAuto", "Scorri automaticamente"),
|
||||
("Good image quality", "Buona qualità immagine"),
|
||||
("Balanced", "Bilanciato"),
|
||||
("Optimize reaction time", "Ottimizza il tempo di reazione"),
|
||||
|
@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Original", "オリジナル"),
|
||||
("Shrink", "縮小"),
|
||||
("Stretch", "伸縮"),
|
||||
("Scrollbar", "スクロール・バー"),
|
||||
("ScrollAuto", "自動スクロール"),
|
||||
("Good image quality", "画質優先"),
|
||||
("Balanced", "バランス"),
|
||||
("Optimize reaction time", "速度優先"),
|
||||
|
@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Original", "Oryginał"),
|
||||
("Shrink", "Zmniejsz"),
|
||||
("Stretch", "Zwiększ"),
|
||||
("Scrollbar", "Pasek przewijania"),
|
||||
("ScrollAuto", "Przewijanie automatyczne"),
|
||||
("Good image quality", "Dobra jakość obrazu"),
|
||||
("Balanced", "Zrównoważony"),
|
||||
("Optimize reaction time", "Zoptymalizuj czas reakcji"),
|
||||
|
@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Original", "Original"),
|
||||
("Shrink", "Reduzir"),
|
||||
("Stretch", "Aumentar"),
|
||||
("Scrollbar", "Barra de rolagem"),
|
||||
("ScrollAuto", "Rolagem automática"),
|
||||
("Good image quality", "Qualidade visual boa"),
|
||||
("Balanced", "Balanceada"),
|
||||
("Optimize reaction time", "Otimizar tempo de reação"),
|
||||
|
@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Original", "Оригинал"),
|
||||
("Shrink", "Уменьшить"),
|
||||
("Stretch", "Растянуть"),
|
||||
("Scrollbar", "Полоса прокрутки"),
|
||||
("ScrollAuto", "Прокрутка Авто"),
|
||||
("Good image quality", "Хорошее качество изображения"),
|
||||
("Balanced", "Сбалансированный"),
|
||||
("Optimize reaction time", "Оптимизировать время реакции"),
|
||||
|
@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Original", "Pôvodný"),
|
||||
("Shrink", "Zmenšené"),
|
||||
("Stretch", "Roztiahnuté"),
|
||||
("Scrollbar", "Posuvník"),
|
||||
("ScrollAuto", "Rolovať Auto"),
|
||||
("Good image quality", "Dobrá kvalita obrazu"),
|
||||
("Balanced", "Vyvážené"),
|
||||
("Optimize reaction time", "Optimalizované pre čas odozvy"),
|
||||
|
@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Original", ""),
|
||||
("Shrink", ""),
|
||||
("Stretch", ""),
|
||||
("Scrollbar", ""),
|
||||
("ScrollAuto", ""),
|
||||
("Good image quality", ""),
|
||||
("Balanced", ""),
|
||||
("Optimize reaction time", ""),
|
||||
|
@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Original", "Orjinal"),
|
||||
("Shrink", "Küçült"),
|
||||
("Stretch", "Uzat"),
|
||||
("Scrollbar", "Kaydırma çubuğu"),
|
||||
("ScrollAuto", "Otomatik Kaydır"),
|
||||
("Good image quality", "İyi görüntü kalitesi"),
|
||||
("Balanced", "Dengelenmiş"),
|
||||
("Optimize reaction time", "Tepki süresini optimize et"),
|
||||
|
@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Original", "原始"),
|
||||
("Shrink", "縮減"),
|
||||
("Stretch", "延展"),
|
||||
("Scrollbar", "滾動條"),
|
||||
("ScrollAuto", "自動滾動"),
|
||||
("Good image quality", "畫面品質良好"),
|
||||
("Balanced", "平衡"),
|
||||
("Optimize reaction time", "回應速度最佳化"),
|
||||
|
@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Original", "Gốc"),
|
||||
("Shrink", "Thu nhỏ"),
|
||||
("Stretch", "Kéo dãn"),
|
||||
("Scrollbar", "Thanh cuộn"),
|
||||
("ScrollAuto", "Tự động cuộn"),
|
||||
("Good image quality", "Chất lượng hình ảnh tốt"),
|
||||
("Balanced", "Cân bằng"),
|
||||
("Optimize reaction time", "Thời gian phản ứng tối ưu"),
|
||||
|
Loading…
x
Reference in New Issue
Block a user