* Add option auto record outgoing session * In the same connection, all displays and all windows share the same recording state. todo: Android check external storage permission Known issue: * Sciter old issue, stop the process directly without stop record, the record file can't play. Signed-off-by: 21pages <sunboeasy@gmail.com>
		
			
				
	
	
		
			2466 lines
		
	
	
		
			73 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			2466 lines
		
	
	
		
			73 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'dart:convert';
 | |
| import 'dart:async';
 | |
| 
 | |
| import 'package:flutter/material.dart';
 | |
| import 'package:flutter/services.dart';
 | |
| import 'package:flutter_hbb/common/widgets/audio_input.dart';
 | |
| import 'package:flutter_hbb/common/widgets/toolbar.dart';
 | |
| import 'package:flutter_hbb/models/chat_model.dart';
 | |
| import 'package:flutter_hbb/models/state_model.dart';
 | |
| import 'package:flutter_hbb/consts.dart';
 | |
| import 'package:flutter_hbb/utils/multi_window_manager.dart';
 | |
| import 'package:flutter_hbb/plugin/widgets/desc_ui.dart';
 | |
| import 'package:flutter_hbb/plugin/common.dart';
 | |
| import 'package:flutter_svg/flutter_svg.dart';
 | |
| import 'package:get/get.dart';
 | |
| import 'package:provider/provider.dart';
 | |
| import 'package:debounce_throttle/debounce_throttle.dart';
 | |
| import 'package:desktop_multi_window/desktop_multi_window.dart';
 | |
| import 'package:window_size/window_size.dart' as window_size;
 | |
| 
 | |
| import '../../common.dart';
 | |
| import '../../models/model.dart';
 | |
| import '../../models/platform_model.dart';
 | |
| import '../../common/shared_state.dart';
 | |
| import './popup_menu.dart';
 | |
| import './kb_layout_type_chooser.dart';
 | |
| 
 | |
| class ToolbarState {
 | |
|   late RxBool _pin;
 | |
| 
 | |
|   bool isShowInited = false;
 | |
|   RxBool show = false.obs;
 | |
| 
 | |
|   ToolbarState() {
 | |
|     _pin = RxBool(false);
 | |
|     final s = bind.getLocalFlutterOption(k: kOptionRemoteMenubarState);
 | |
|     if (s.isEmpty) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     try {
 | |
|       final m = jsonDecode(s);
 | |
|       if (m != null) {
 | |
|         _pin = RxBool(m['pin'] ?? false);
 | |
|       }
 | |
|     } catch (e) {
 | |
|       debugPrint('Failed to decode toolbar state ${e.toString()}');
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   bool get pin => _pin.value;
 | |
| 
 | |
|   switchShow(SessionID sessionId) async {
 | |
|     bind.sessionToggleOption(
 | |
|         sessionId: sessionId, value: kOptionCollapseToolbar);
 | |
|     show.value = !show.value;
 | |
|   }
 | |
| 
 | |
|   initShow(SessionID sessionId) async {
 | |
|     if (!isShowInited) {
 | |
|       show.value = !(await bind.sessionGetToggleOption(
 | |
|               sessionId: sessionId, arg: kOptionCollapseToolbar) ??
 | |
|           false);
 | |
|       isShowInited = true;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   switchPin() async {
 | |
|     _pin.value = !_pin.value;
 | |
|     // Save everytime changed, as this func will not be called frequently
 | |
|     await _savePin();
 | |
|   }
 | |
| 
 | |
|   setPin(bool v) async {
 | |
|     if (_pin.value != v) {
 | |
|       _pin.value = v;
 | |
|       // Save everytime changed, as this func will not be called frequently
 | |
|       await _savePin();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _savePin() async {
 | |
|     bind.setLocalFlutterOption(
 | |
|         k: kOptionRemoteMenubarState, v: jsonEncode({'pin': _pin.value}));
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _ToolbarTheme {
 | |
|   static const Color blueColor = MyTheme.button;
 | |
|   static const Color hoverBlueColor = MyTheme.accent;
 | |
|   static Color inactiveColor = Colors.grey[800]!;
 | |
|   static Color hoverInactiveColor = Colors.grey[850]!;
 | |
| 
 | |
|   static const Color redColor = Colors.redAccent;
 | |
|   static const Color hoverRedColor = Colors.red;
 | |
|   // kMinInteractiveDimension
 | |
|   static const double height = 20.0;
 | |
|   static const double dividerHeight = 12.0;
 | |
| 
 | |
|   static const double buttonSize = 32;
 | |
|   static const double buttonHMargin = 2;
 | |
|   static const double buttonVMargin = 6;
 | |
|   static const double iconRadius = 8;
 | |
|   static const double elevation = 3;
 | |
| 
 | |
|   static double dividerSpaceToAction = isWindows ? 8 : 14;
 | |
| 
 | |
|   static double menuBorderRadius = isWindows ? 5.0 : 7.0;
 | |
|   static EdgeInsets menuPadding = isWindows
 | |
|       ? EdgeInsets.fromLTRB(4, 12, 4, 12)
 | |
|       : EdgeInsets.fromLTRB(6, 14, 6, 14);
 | |
|   static const double menuButtonBorderRadius = 3.0;
 | |
| 
 | |
|   static Color borderColor(BuildContext context) =>
 | |
|       MyTheme.color(context).border3 ?? MyTheme.border;
 | |
| 
 | |
|   static Color? dividerColor(BuildContext context) =>
 | |
|       MyTheme.color(context).divider;
 | |
| 
 | |
|   static MenuStyle defaultMenuStyle(BuildContext context) => MenuStyle(
 | |
|         side: MaterialStateProperty.all(BorderSide(
 | |
|           width: 1,
 | |
|           color: borderColor(context),
 | |
|         )),
 | |
|         shape: MaterialStatePropertyAll(RoundedRectangleBorder(
 | |
|             borderRadius:
 | |
|                 BorderRadius.circular(_ToolbarTheme.menuBorderRadius))),
 | |
|         padding: MaterialStateProperty.all(_ToolbarTheme.menuPadding),
 | |
|       );
 | |
|   static final defaultMenuButtonStyle = ButtonStyle(
 | |
|     backgroundColor: MaterialStatePropertyAll(Colors.transparent),
 | |
|     padding: MaterialStatePropertyAll(EdgeInsets.zero),
 | |
|     overlayColor: MaterialStatePropertyAll(Colors.transparent),
 | |
|   );
 | |
| 
 | |
|   static Widget borderWrapper(
 | |
|       BuildContext context, Widget child, BorderRadius borderRadius) {
 | |
|     return Container(
 | |
|       decoration: BoxDecoration(
 | |
|         border: Border.all(
 | |
|           color: borderColor(context),
 | |
|           width: 1,
 | |
|         ),
 | |
|         borderRadius: borderRadius,
 | |
|       ),
 | |
|       child: child,
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| typedef DismissFunc = void Function();
 | |
| 
 | |
| class RemoteMenuEntry {
 | |
|   static MenuEntryRadios<String> viewStyle(
 | |
|     String remoteId,
 | |
|     FFI ffi,
 | |
|     EdgeInsets padding, {
 | |
|     DismissFunc? dismissFunc,
 | |
|     DismissCallback? dismissCallback,
 | |
|     RxString? rxViewStyle,
 | |
|   }) {
 | |
|     return MenuEntryRadios<String>(
 | |
|       text: translate('Ratio'),
 | |
|       optionsGetter: () => [
 | |
|         MenuEntryRadioOption(
 | |
|           text: translate('Scale original'),
 | |
|           value: kRemoteViewStyleOriginal,
 | |
|           dismissOnClicked: true,
 | |
|           dismissCallback: dismissCallback,
 | |
|         ),
 | |
|         MenuEntryRadioOption(
 | |
|           text: translate('Scale adaptive'),
 | |
|           value: kRemoteViewStyleAdaptive,
 | |
|           dismissOnClicked: true,
 | |
|           dismissCallback: dismissCallback,
 | |
|         ),
 | |
|       ],
 | |
|       curOptionGetter: () async {
 | |
|         // null means peer id is not found, which there's no need to care about
 | |
|         final viewStyle =
 | |
|             await bind.sessionGetViewStyle(sessionId: ffi.sessionId) ?? '';
 | |
|         if (rxViewStyle != null) {
 | |
|           rxViewStyle.value = viewStyle;
 | |
|         }
 | |
|         return viewStyle;
 | |
|       },
 | |
|       optionSetter: (String oldValue, String newValue) async {
 | |
|         await bind.sessionSetViewStyle(
 | |
|             sessionId: ffi.sessionId, value: newValue);
 | |
|         if (rxViewStyle != null) {
 | |
|           rxViewStyle.value = newValue;
 | |
|         }
 | |
|         ffi.canvasModel.updateViewStyle();
 | |
|         if (dismissFunc != null) {
 | |
|           dismissFunc();
 | |
|         }
 | |
|       },
 | |
|       padding: padding,
 | |
|       dismissOnClicked: true,
 | |
|       dismissCallback: dismissCallback,
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   static MenuEntrySwitch2<String> showRemoteCursor(
 | |
|     String remoteId,
 | |
|     SessionID sessionId,
 | |
|     EdgeInsets padding, {
 | |
|     DismissFunc? dismissFunc,
 | |
|     DismissCallback? dismissCallback,
 | |
|   }) {
 | |
|     final state = ShowRemoteCursorState.find(remoteId);
 | |
|     final optKey = 'show-remote-cursor';
 | |
|     return MenuEntrySwitch2<String>(
 | |
|       switchType: SwitchType.scheckbox,
 | |
|       text: translate('Show remote cursor'),
 | |
|       getter: () {
 | |
|         return state;
 | |
|       },
 | |
|       setter: (bool v) async {
 | |
|         await bind.sessionToggleOption(sessionId: sessionId, value: optKey);
 | |
|         state.value =
 | |
|             bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: optKey);
 | |
|         if (dismissFunc != null) {
 | |
|           dismissFunc();
 | |
|         }
 | |
|       },
 | |
|       padding: padding,
 | |
|       dismissOnClicked: true,
 | |
|       dismissCallback: dismissCallback,
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   static MenuEntrySwitch<String> disableClipboard(
 | |
|     SessionID sessionId,
 | |
|     EdgeInsets? padding, {
 | |
|     DismissFunc? dismissFunc,
 | |
|     DismissCallback? dismissCallback,
 | |
|   }) {
 | |
|     return createSwitchMenuEntry(
 | |
|       sessionId,
 | |
|       'Disable clipboard',
 | |
|       'disable-clipboard',
 | |
|       padding,
 | |
|       true,
 | |
|       dismissCallback: dismissCallback,
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   static MenuEntrySwitch<String> createSwitchMenuEntry(
 | |
|     SessionID sessionId,
 | |
|     String text,
 | |
|     String option,
 | |
|     EdgeInsets? padding,
 | |
|     bool dismissOnClicked, {
 | |
|     DismissFunc? dismissFunc,
 | |
|     DismissCallback? dismissCallback,
 | |
|   }) {
 | |
|     return MenuEntrySwitch<String>(
 | |
|       switchType: SwitchType.scheckbox,
 | |
|       text: translate(text),
 | |
|       getter: () async {
 | |
|         return bind.sessionGetToggleOptionSync(
 | |
|             sessionId: sessionId, arg: option);
 | |
|       },
 | |
|       setter: (bool v) async {
 | |
|         await bind.sessionToggleOption(sessionId: sessionId, value: option);
 | |
|         if (dismissFunc != null) {
 | |
|           dismissFunc();
 | |
|         }
 | |
|       },
 | |
|       padding: padding,
 | |
|       dismissOnClicked: dismissOnClicked,
 | |
|       dismissCallback: dismissCallback,
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   static MenuEntryButton<String> insertLock(
 | |
|     SessionID sessionId,
 | |
|     EdgeInsets? padding, {
 | |
|     DismissFunc? dismissFunc,
 | |
|     DismissCallback? dismissCallback,
 | |
|   }) {
 | |
|     return MenuEntryButton<String>(
 | |
|       childBuilder: (TextStyle? style) => Text(
 | |
|         translate('Insert Lock'),
 | |
|         style: style,
 | |
|       ),
 | |
|       proc: () {
 | |
|         bind.sessionLockScreen(sessionId: sessionId);
 | |
|         if (dismissFunc != null) {
 | |
|           dismissFunc();
 | |
|         }
 | |
|       },
 | |
|       padding: padding,
 | |
|       dismissOnClicked: true,
 | |
|       dismissCallback: dismissCallback,
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   static insertCtrlAltDel(
 | |
|     SessionID sessionId,
 | |
|     EdgeInsets? padding, {
 | |
|     DismissFunc? dismissFunc,
 | |
|     DismissCallback? dismissCallback,
 | |
|   }) {
 | |
|     return MenuEntryButton<String>(
 | |
|       childBuilder: (TextStyle? style) => Text(
 | |
|         '${translate("Insert")} Ctrl + Alt + Del',
 | |
|         style: style,
 | |
|       ),
 | |
|       proc: () {
 | |
|         bind.sessionCtrlAltDel(sessionId: sessionId);
 | |
|         if (dismissFunc != null) {
 | |
|           dismissFunc();
 | |
|         }
 | |
|       },
 | |
|       padding: padding,
 | |
|       dismissOnClicked: true,
 | |
|       dismissCallback: dismissCallback,
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class RemoteToolbar extends StatefulWidget {
 | |
|   final String id;
 | |
|   final FFI ffi;
 | |
|   final ToolbarState state;
 | |
|   final Function(int, Function(bool)) onEnterOrLeaveImageSetter;
 | |
|   final Function(int) onEnterOrLeaveImageCleaner;
 | |
|   final Function(VoidCallback) setRemoteState;
 | |
| 
 | |
|   RemoteToolbar({
 | |
|     Key? key,
 | |
|     required this.id,
 | |
|     required this.ffi,
 | |
|     required this.state,
 | |
|     required this.onEnterOrLeaveImageSetter,
 | |
|     required this.onEnterOrLeaveImageCleaner,
 | |
|     required this.setRemoteState,
 | |
|   }) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   State<RemoteToolbar> createState() => _RemoteToolbarState();
 | |
| }
 | |
| 
 | |
| class _RemoteToolbarState extends State<RemoteToolbar> {
 | |
|   late Debouncer<int> _debouncerHide;
 | |
|   bool _isCursorOverImage = false;
 | |
|   final _fractionX = 0.5.obs;
 | |
|   final _dragging = false.obs;
 | |
| 
 | |
|   int get windowId => stateGlobal.windowId;
 | |
| 
 | |
|   void _setFullscreen(bool v) {
 | |
|     stateGlobal.setFullscreen(v);
 | |
|     // stateGlobal.fullscreen is RxBool now, no need to call setState.
 | |
|     // setState(() {});
 | |
|   }
 | |
| 
 | |
|   RxBool get show => widget.state.show;
 | |
|   bool get pin => widget.state.pin;
 | |
| 
 | |
|   PeerInfo get pi => widget.ffi.ffiModel.pi;
 | |
|   FfiModel get ffiModel => widget.ffi.ffiModel;
 | |
| 
 | |
|   triggerAutoHide() => _debouncerHide.value = _debouncerHide.value + 1;
 | |
| 
 | |
|   void _minimize() async =>
 | |
|       await WindowController.fromWindowId(windowId).minimize();
 | |
| 
 | |
|   @override
 | |
|   initState() {
 | |
|     super.initState();
 | |
| 
 | |
|     WidgetsBinding.instance.addPostFrameCallback((_) async {
 | |
|       _fractionX.value = double.tryParse(await bind.sessionGetOption(
 | |
|                   sessionId: widget.ffi.sessionId,
 | |
|                   arg: 'remote-menubar-drag-x') ??
 | |
|               '0.5') ??
 | |
|           0.5;
 | |
|     });
 | |
| 
 | |
|     _debouncerHide = Debouncer<int>(
 | |
|       Duration(milliseconds: 5000),
 | |
|       onChanged: _debouncerHideProc,
 | |
|       initialValue: 0,
 | |
|     );
 | |
| 
 | |
|     widget.onEnterOrLeaveImageSetter(identityHashCode(this), (enter) {
 | |
|       if (enter) {
 | |
|         triggerAutoHide();
 | |
|         _isCursorOverImage = true;
 | |
|       } else {
 | |
|         _isCursorOverImage = false;
 | |
|       }
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   _debouncerHideProc(int v) {
 | |
|     if (!pin && show.isTrue && _isCursorOverImage && _dragging.isFalse) {
 | |
|       show.value = false;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   dispose() {
 | |
|     super.dispose();
 | |
| 
 | |
|     widget.onEnterOrLeaveImageCleaner(identityHashCode(this));
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return Align(
 | |
|       alignment: Alignment.topCenter,
 | |
|       child: Obx(() => show.value
 | |
|           ? _buildToolbar(context)
 | |
|           : _buildDraggableShowHide(context)),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   Widget _buildDraggableShowHide(BuildContext context) {
 | |
|     return Obx(() {
 | |
|       if (show.isTrue && _dragging.isFalse) {
 | |
|         triggerAutoHide();
 | |
|       }
 | |
|       final borderRadius = BorderRadius.vertical(
 | |
|         bottom: Radius.circular(5),
 | |
|       );
 | |
|       return Align(
 | |
|         alignment: FractionalOffset(_fractionX.value, 0),
 | |
|         child: Offstage(
 | |
|           offstage: _dragging.isTrue,
 | |
|           child: Material(
 | |
|             elevation: _ToolbarTheme.elevation,
 | |
|             shadowColor: MyTheme.color(context).shadow,
 | |
|             borderRadius: borderRadius,
 | |
|             child: _DraggableShowHide(
 | |
|               id: widget.id,
 | |
|               sessionId: widget.ffi.sessionId,
 | |
|               dragging: _dragging,
 | |
|               fractionX: _fractionX,
 | |
|               toolbarState: widget.state,
 | |
|               setFullscreen: _setFullscreen,
 | |
|               setMinimize: _minimize,
 | |
|               borderRadius: borderRadius,
 | |
|             ),
 | |
|           ),
 | |
|         ),
 | |
|       );
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   Widget _buildToolbar(BuildContext context) {
 | |
|     final List<Widget> toolbarItems = [];
 | |
|     toolbarItems.add(_PinMenu(state: widget.state));
 | |
|     if (!isWebDesktop) {
 | |
|       toolbarItems.add(_MobileActionMenu(ffi: widget.ffi));
 | |
|     }
 | |
| 
 | |
|     toolbarItems.add(Obx(() {
 | |
|       if (PrivacyModeState.find(widget.id).isEmpty &&
 | |
|           pi.displaysCount.value > 1) {
 | |
|         return _MonitorMenu(
 | |
|             id: widget.id,
 | |
|             ffi: widget.ffi,
 | |
|             setRemoteState: widget.setRemoteState);
 | |
|       } else {
 | |
|         return Offstage();
 | |
|       }
 | |
|     }));
 | |
| 
 | |
|     toolbarItems
 | |
|         .add(_ControlMenu(id: widget.id, ffi: widget.ffi, state: widget.state));
 | |
|     toolbarItems.add(_DisplayMenu(
 | |
|       id: widget.id,
 | |
|       ffi: widget.ffi,
 | |
|       state: widget.state,
 | |
|       setFullscreen: _setFullscreen,
 | |
|     ));
 | |
|     toolbarItems.add(_KeyboardMenu(id: widget.id, ffi: widget.ffi));
 | |
|     toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi));
 | |
|     if (!isWeb) {
 | |
|       toolbarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi));
 | |
|     }
 | |
|     if (!isWeb) toolbarItems.add(_RecordMenu());
 | |
|     toolbarItems.add(_CloseMenu(id: widget.id, ffi: widget.ffi));
 | |
|     final toolbarBorderRadius = BorderRadius.all(Radius.circular(4.0));
 | |
|     return Column(
 | |
|       mainAxisSize: MainAxisSize.min,
 | |
|       children: [
 | |
|         Material(
 | |
|           elevation: _ToolbarTheme.elevation,
 | |
|           shadowColor: MyTheme.color(context).shadow,
 | |
|           borderRadius: toolbarBorderRadius,
 | |
|           color: Theme.of(context)
 | |
|               .menuBarTheme
 | |
|               .style
 | |
|               ?.backgroundColor
 | |
|               ?.resolve(MaterialState.values.toSet()),
 | |
|           child: SingleChildScrollView(
 | |
|             scrollDirection: Axis.horizontal,
 | |
|             child: Theme(
 | |
|               data: themeData(),
 | |
|               child: _ToolbarTheme.borderWrapper(
 | |
|                   context,
 | |
|                   Row(
 | |
|                     children: [
 | |
|                       SizedBox(width: _ToolbarTheme.buttonHMargin * 2),
 | |
|                       ...toolbarItems,
 | |
|                       SizedBox(width: _ToolbarTheme.buttonHMargin * 2)
 | |
|                     ],
 | |
|                   ),
 | |
|                   toolbarBorderRadius),
 | |
|             ),
 | |
|           ),
 | |
|         ),
 | |
|         _buildDraggableShowHide(context),
 | |
|       ],
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   ThemeData themeData() {
 | |
|     return Theme.of(context).copyWith(
 | |
|       menuButtonTheme: MenuButtonThemeData(
 | |
|         style: ButtonStyle(
 | |
|           minimumSize: MaterialStatePropertyAll(Size(64, 32)),
 | |
|           textStyle: MaterialStatePropertyAll(
 | |
|             TextStyle(fontWeight: FontWeight.normal),
 | |
|           ),
 | |
|           shape: MaterialStatePropertyAll(RoundedRectangleBorder(
 | |
|               borderRadius:
 | |
|                   BorderRadius.circular(_ToolbarTheme.menuButtonBorderRadius))),
 | |
|         ),
 | |
|       ),
 | |
|       dividerTheme: DividerThemeData(
 | |
|         space: _ToolbarTheme.dividerSpaceToAction,
 | |
|         color: _ToolbarTheme.dividerColor(context),
 | |
|       ),
 | |
|       menuBarTheme: MenuBarThemeData(
 | |
|           style: MenuStyle(
 | |
|         padding: MaterialStatePropertyAll(EdgeInsets.zero),
 | |
|         elevation: MaterialStatePropertyAll(0),
 | |
|         shape: MaterialStatePropertyAll(BeveledRectangleBorder()),
 | |
|       ).copyWith(
 | |
|               backgroundColor:
 | |
|                   Theme.of(context).menuBarTheme.style?.backgroundColor)),
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _PinMenu extends StatelessWidget {
 | |
|   final ToolbarState state;
 | |
|   const _PinMenu({Key? key, required this.state}) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return Obx(
 | |
|       () => _IconMenuButton(
 | |
|         assetName: state.pin ? "assets/pinned.svg" : "assets/unpinned.svg",
 | |
|         tooltip: state.pin ? 'Unpin Toolbar' : 'Pin Toolbar',
 | |
|         onPressed: state.switchPin,
 | |
|         color:
 | |
|             state.pin ? _ToolbarTheme.blueColor : _ToolbarTheme.inactiveColor,
 | |
|         hoverColor: state.pin
 | |
|             ? _ToolbarTheme.hoverBlueColor
 | |
|             : _ToolbarTheme.hoverInactiveColor,
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _MobileActionMenu extends StatelessWidget {
 | |
|   final FFI ffi;
 | |
|   const _MobileActionMenu({Key? key, required this.ffi}) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     if (!ffi.ffiModel.isPeerAndroid) return Offstage();
 | |
|     return Obx(() => _IconMenuButton(
 | |
|           assetName: 'assets/actions_mobile.svg',
 | |
|           tooltip: 'Mobile Actions',
 | |
|           onPressed: () => ffi.dialogManager.setMobileActionsOverlayVisible(
 | |
|               !ffi.dialogManager.mobileActionsOverlayVisible.value),
 | |
|           color: ffi.dialogManager.mobileActionsOverlayVisible.isTrue
 | |
|               ? _ToolbarTheme.blueColor
 | |
|               : _ToolbarTheme.inactiveColor,
 | |
|           hoverColor: ffi.dialogManager.mobileActionsOverlayVisible.isTrue
 | |
|               ? _ToolbarTheme.hoverBlueColor
 | |
|               : _ToolbarTheme.hoverInactiveColor,
 | |
|         ));
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _MonitorMenu extends StatelessWidget {
 | |
|   final String id;
 | |
|   final FFI ffi;
 | |
|   final Function(VoidCallback) setRemoteState;
 | |
|   const _MonitorMenu({
 | |
|     Key? key,
 | |
|     required this.id,
 | |
|     required this.ffi,
 | |
|     required this.setRemoteState,
 | |
|   }) : super(key: key);
 | |
| 
 | |
|   bool get showMonitorsToolbar =>
 | |
|       bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y';
 | |
| 
 | |
|   bool get supportIndividualWindows =>
 | |
|       !isWeb && ffi.ffiModel.pi.isSupportMultiDisplay;
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) => showMonitorsToolbar
 | |
|       ? buildMultiMonitorMenu(context)
 | |
|       : Obx(() => buildMonitorMenu(context));
 | |
| 
 | |
|   Widget buildMonitorMenu(BuildContext context) {
 | |
|     final width = SimpleWrapper<double>(0);
 | |
|     final monitorsIcon =
 | |
|         globalMonitorsWidget(width, Colors.white, Colors.black38);
 | |
|     return _IconSubmenuButton(
 | |
|         tooltip: 'Select Monitor',
 | |
|         icon: monitorsIcon,
 | |
|         ffi: ffi,
 | |
|         width: width.value,
 | |
|         color: _ToolbarTheme.blueColor,
 | |
|         hoverColor: _ToolbarTheme.hoverBlueColor,
 | |
|         menuStyle: MenuStyle(
 | |
|             padding:
 | |
|                 MaterialStatePropertyAll(EdgeInsets.symmetric(horizontal: 6))),
 | |
|         menuChildrenGetter: () => [buildMonitorSubmenuWidget(context)]);
 | |
|   }
 | |
| 
 | |
|   Widget buildMultiMonitorMenu(BuildContext context) {
 | |
|     return Row(children: buildMonitorList(context, true));
 | |
|   }
 | |
| 
 | |
|   Widget buildMonitorSubmenuWidget(BuildContext context) {
 | |
|     return Column(
 | |
|       mainAxisSize: MainAxisSize.min,
 | |
|       children: [
 | |
|         Row(children: buildMonitorList(context, false)),
 | |
|         supportIndividualWindows ? Divider() : Offstage(),
 | |
|         supportIndividualWindows ? chooseDisplayBehavior() : Offstage(),
 | |
|       ],
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   Widget chooseDisplayBehavior() {
 | |
|     final value =
 | |
|         bind.sessionGetDisplaysAsIndividualWindows(sessionId: ffi.sessionId) ==
 | |
|             'Y';
 | |
|     return CkbMenuButton(
 | |
|         value: value,
 | |
|         onChanged: (value) async {
 | |
|           if (value == null) return;
 | |
|           await bind.sessionSetDisplaysAsIndividualWindows(
 | |
|               sessionId: ffi.sessionId, value: value ? 'Y' : 'N');
 | |
|         },
 | |
|         ffi: ffi,
 | |
|         child: Text(translate('Show displays as individual windows')));
 | |
|   }
 | |
| 
 | |
|   buildOneMonitorButton(i, curDisplay) => Text(
 | |
|         '${i + 1}',
 | |
|         style: TextStyle(
 | |
|           color: i == curDisplay
 | |
|               ? _ToolbarTheme.blueColor
 | |
|               : _ToolbarTheme.inactiveColor,
 | |
|           fontSize: 12,
 | |
|           fontWeight: FontWeight.bold,
 | |
|         ),
 | |
|       );
 | |
| 
 | |
|   List<Widget> buildMonitorList(BuildContext context, bool isMulti) {
 | |
|     final List<Widget> monitorList = [];
 | |
|     final pi = ffi.ffiModel.pi;
 | |
| 
 | |
|     buildMonitorButton(int i) => Obx(() {
 | |
|           RxInt display = CurrentDisplayState.find(id);
 | |
| 
 | |
|           final isAllMonitors = i == kAllDisplayValue;
 | |
|           final width = SimpleWrapper<double>(0);
 | |
|           Widget? monitorsIcon;
 | |
|           if (isAllMonitors) {
 | |
|             monitorsIcon = globalMonitorsWidget(
 | |
|                 width, Colors.white, _ToolbarTheme.blueColor);
 | |
|           }
 | |
|           return _IconMenuButton(
 | |
|             tooltip: isMulti
 | |
|                 ? ''
 | |
|                 : isAllMonitors
 | |
|                     ? 'all monitors'
 | |
|                     : '#${i + 1} monitor',
 | |
|             hMargin: isMulti ? null : 6,
 | |
|             vMargin: isMulti ? null : 12,
 | |
|             topLevel: false,
 | |
|             color: i == display.value
 | |
|                 ? _ToolbarTheme.blueColor
 | |
|                 : _ToolbarTheme.inactiveColor,
 | |
|             hoverColor: i == display.value
 | |
|                 ? _ToolbarTheme.hoverBlueColor
 | |
|                 : _ToolbarTheme.hoverInactiveColor,
 | |
|             width: isAllMonitors ? width.value : null,
 | |
|             icon: isAllMonitors
 | |
|                 ? monitorsIcon
 | |
|                 : Container(
 | |
|                     alignment: AlignmentDirectional.center,
 | |
|                     constraints:
 | |
|                         const BoxConstraints(minHeight: _ToolbarTheme.height),
 | |
|                     child: Stack(
 | |
|                       alignment: Alignment.center,
 | |
|                       children: [
 | |
|                         SvgPicture.asset(
 | |
|                           "assets/screen.svg",
 | |
|                           colorFilter:
 | |
|                               ColorFilter.mode(Colors.white, BlendMode.srcIn),
 | |
|                         ),
 | |
|                         Obx(() => buildOneMonitorButton(i, display.value)),
 | |
|                       ],
 | |
|                     ),
 | |
|                   ),
 | |
|             onPressed: () => onPressed(i, pi, isMulti),
 | |
|           );
 | |
|         });
 | |
| 
 | |
|     for (int i = 0; i < pi.displays.length; i++) {
 | |
|       monitorList.add(buildMonitorButton(i));
 | |
|     }
 | |
|     if (supportIndividualWindows && pi.displays.length > 1) {
 | |
|       monitorList.add(buildMonitorButton(kAllDisplayValue));
 | |
|     }
 | |
|     return monitorList;
 | |
|   }
 | |
| 
 | |
|   globalMonitorsWidget(
 | |
|       SimpleWrapper<double> width, Color activeTextColor, Color activeBgColor) {
 | |
|     getMonitors() {
 | |
|       final pi = ffi.ffiModel.pi;
 | |
|       RxInt display = CurrentDisplayState.find(id);
 | |
|       final rect = ffi.ffiModel.globalDisplaysRect();
 | |
|       if (rect == null) {
 | |
|         return Offstage();
 | |
|       }
 | |
| 
 | |
|       final scale = _ToolbarTheme.buttonSize / rect.height * 0.75;
 | |
|       final startY = (_ToolbarTheme.buttonSize - rect.height * scale) * 0.5;
 | |
|       final startX = startY;
 | |
| 
 | |
|       final children = <Widget>[];
 | |
|       for (var i = 0; i < pi.displays.length; i++) {
 | |
|         final d = pi.displays[i];
 | |
|         double s = d.scale;
 | |
|         int dWidth = d.width.toDouble() ~/ s;
 | |
|         int dHeight = d.height.toDouble() ~/ s;
 | |
|         final fontSize = (dWidth * scale < dHeight * scale
 | |
|                 ? dWidth * scale
 | |
|                 : dHeight * scale) *
 | |
|             0.65;
 | |
|         children.add(Positioned(
 | |
|           left: (d.x - rect.left) * scale + startX,
 | |
|           top: (d.y - rect.top) * scale + startY,
 | |
|           width: dWidth * scale,
 | |
|           height: dHeight * scale,
 | |
|           child: Container(
 | |
|             decoration: BoxDecoration(
 | |
|               border: Border.all(
 | |
|                 color: Colors.grey,
 | |
|                 width: 1.0,
 | |
|               ),
 | |
|               color: display.value == i ? activeBgColor : Colors.white,
 | |
|             ),
 | |
|             child: Center(
 | |
|                 child: Text(
 | |
|               '${i + 1}',
 | |
|               style: TextStyle(
 | |
|                 color: display.value == i
 | |
|                     ? activeTextColor
 | |
|                     : _ToolbarTheme.inactiveColor,
 | |
|                 fontSize: fontSize,
 | |
|                 fontWeight: FontWeight.bold,
 | |
|               ),
 | |
|             )),
 | |
|           ),
 | |
|         ));
 | |
|       }
 | |
|       width.value = rect.width * scale + startX * 2;
 | |
|       return SizedBox(
 | |
|         width: width.value,
 | |
|         height: rect.height * scale + startY * 2,
 | |
|         child: Stack(
 | |
|           children: children,
 | |
|         ),
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     return Stack(
 | |
|       alignment: Alignment.center,
 | |
|       children: [
 | |
|         SizedBox(height: _ToolbarTheme.buttonSize),
 | |
|         getMonitors(),
 | |
|       ],
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   onPressed(int i, PeerInfo pi, bool isMulti) {
 | |
|     if (!isMulti) {
 | |
|       // If show monitors in toolbar(`buildMultiMonitorMenu()`), then the menu will dismiss automatically.
 | |
|       _menuDismissCallback(ffi);
 | |
|     }
 | |
|     RxInt display = CurrentDisplayState.find(id);
 | |
|     if (display.value != i) {
 | |
|       final isChooseDisplayToOpenInNewWindow = pi.isSupportMultiDisplay &&
 | |
|           bind.sessionGetDisplaysAsIndividualWindows(
 | |
|                   sessionId: ffi.sessionId) ==
 | |
|               'Y';
 | |
|       if (isChooseDisplayToOpenInNewWindow) {
 | |
|         openMonitorInNewTabOrWindow(i, ffi.id, pi);
 | |
|       } else {
 | |
|         openMonitorInTheSameTab(i, ffi, pi, updateCursorPos: !isMulti);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _ControlMenu extends StatelessWidget {
 | |
|   final String id;
 | |
|   final FFI ffi;
 | |
|   final ToolbarState state;
 | |
|   _ControlMenu(
 | |
|       {Key? key, required this.id, required this.ffi, required this.state})
 | |
|       : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return _IconSubmenuButton(
 | |
|         tooltip: 'Control Actions',
 | |
|         svg: "assets/actions.svg",
 | |
|         color: _ToolbarTheme.blueColor,
 | |
|         hoverColor: _ToolbarTheme.hoverBlueColor,
 | |
|         ffi: ffi,
 | |
|         menuChildrenGetter: () => toolbarControls(context, id, ffi).map((e) {
 | |
|               if (e.divider) {
 | |
|                 return Divider();
 | |
|               } else {
 | |
|                 return MenuButton(
 | |
|                     child: e.child,
 | |
|                     onPressed: e.onPressed,
 | |
|                     ffi: ffi,
 | |
|                     trailingIcon: e.trailingIcon);
 | |
|               }
 | |
|             }).toList());
 | |
|   }
 | |
| }
 | |
| 
 | |
| class ScreenAdjustor {
 | |
|   final String id;
 | |
|   final FFI ffi;
 | |
|   final VoidCallback cbExitFullscreen;
 | |
|   window_size.Screen? _screen;
 | |
| 
 | |
|   ScreenAdjustor({
 | |
|     required this.id,
 | |
|     required this.ffi,
 | |
|     required this.cbExitFullscreen,
 | |
|   });
 | |
| 
 | |
|   bool get isFullscreen => stateGlobal.fullscreen.isTrue;
 | |
|   int get windowId => stateGlobal.windowId;
 | |
| 
 | |
|   adjustWindow(BuildContext context) {
 | |
|     return futureBuilder(
 | |
|         future: isWindowCanBeAdjusted(),
 | |
|         hasData: (data) {
 | |
|           final visible = data as bool;
 | |
|           if (!visible) return Offstage();
 | |
|           return Column(
 | |
|             children: [
 | |
|               MenuButton(
 | |
|                   child: Text(translate('Adjust Window')),
 | |
|                   onPressed: () => doAdjustWindow(context),
 | |
|                   ffi: ffi),
 | |
|               Divider(),
 | |
|             ],
 | |
|           );
 | |
|         });
 | |
|   }
 | |
| 
 | |
|   doAdjustWindow(BuildContext context) async {
 | |
|     await updateScreen();
 | |
|     if (_screen != null) {
 | |
|       cbExitFullscreen();
 | |
|       double scale = _screen!.scaleFactor;
 | |
|       final wndRect = await WindowController.fromWindowId(windowId).getFrame();
 | |
|       final mediaSize = MediaQueryData.fromView(View.of(context)).size;
 | |
|       // On windows, wndRect is equal to GetWindowRect and mediaSize is equal to GetClientRect.
 | |
|       // https://stackoverflow.com/a/7561083
 | |
|       double magicWidth =
 | |
|           wndRect.right - wndRect.left - mediaSize.width * scale;
 | |
|       double magicHeight =
 | |
|           wndRect.bottom - wndRect.top - mediaSize.height * scale;
 | |
|       final canvasModel = ffi.canvasModel;
 | |
|       final width = (canvasModel.getDisplayWidth() * canvasModel.scale +
 | |
|                   CanvasModel.leftToEdge +
 | |
|                   CanvasModel.rightToEdge) *
 | |
|               scale +
 | |
|           magicWidth;
 | |
|       final height = (canvasModel.getDisplayHeight() * canvasModel.scale +
 | |
|                   CanvasModel.topToEdge +
 | |
|                   CanvasModel.bottomToEdge) *
 | |
|               scale +
 | |
|           magicHeight;
 | |
|       double left = wndRect.left + (wndRect.width - width) / 2;
 | |
|       double top = wndRect.top + (wndRect.height - height) / 2;
 | |
| 
 | |
|       Rect frameRect = _screen!.frame;
 | |
|       if (!isFullscreen) {
 | |
|         frameRect = _screen!.visibleFrame;
 | |
|       }
 | |
|       if (left < frameRect.left) {
 | |
|         left = frameRect.left;
 | |
|       }
 | |
|       if (top < frameRect.top) {
 | |
|         top = frameRect.top;
 | |
|       }
 | |
|       if ((left + width) > frameRect.right) {
 | |
|         left = frameRect.right - width;
 | |
|       }
 | |
|       if ((top + height) > frameRect.bottom) {
 | |
|         top = frameRect.bottom - height;
 | |
|       }
 | |
|       await WindowController.fromWindowId(windowId)
 | |
|           .setFrame(Rect.fromLTWH(left, top, width, height));
 | |
|       stateGlobal.setMaximized(false);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   updateScreen() async {
 | |
|     final String info =
 | |
|         isWeb ? screenInfo : await _getScreenInfoDesktop() ?? '';
 | |
|     if (info.isEmpty) {
 | |
|       _screen = null;
 | |
|     } else {
 | |
|       final screenMap = jsonDecode(info);
 | |
|       _screen = window_size.Screen(
 | |
|           Rect.fromLTRB(screenMap['frame']['l'], screenMap['frame']['t'],
 | |
|               screenMap['frame']['r'], screenMap['frame']['b']),
 | |
|           Rect.fromLTRB(
 | |
|               screenMap['visibleFrame']['l'],
 | |
|               screenMap['visibleFrame']['t'],
 | |
|               screenMap['visibleFrame']['r'],
 | |
|               screenMap['visibleFrame']['b']),
 | |
|           screenMap['scaleFactor']);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _getScreenInfoDesktop() async {
 | |
|     final v = await rustDeskWinManager.call(
 | |
|         WindowType.Main, kWindowGetWindowInfo, '');
 | |
|     return v.result;
 | |
|   }
 | |
| 
 | |
|   Future<bool> isWindowCanBeAdjusted() async {
 | |
|     final viewStyle =
 | |
|         await bind.sessionGetViewStyle(sessionId: ffi.sessionId) ?? '';
 | |
|     if (viewStyle != kRemoteViewStyleOriginal) {
 | |
|       return false;
 | |
|     }
 | |
|     if (!isWeb) {
 | |
|       final remoteCount = RemoteCountState.find().value;
 | |
|       if (remoteCount != 1) {
 | |
|         return false;
 | |
|       }
 | |
|     }
 | |
|     if (_screen == null) {
 | |
|       return false;
 | |
|     }
 | |
|     final scale = kIgnoreDpi ? 1.0 : _screen!.scaleFactor;
 | |
|     double selfWidth = _screen!.visibleFrame.width;
 | |
|     double selfHeight = _screen!.visibleFrame.height;
 | |
|     if (isFullscreen) {
 | |
|       selfWidth = _screen!.frame.width;
 | |
|       selfHeight = _screen!.frame.height;
 | |
|     }
 | |
| 
 | |
|     final canvasModel = ffi.canvasModel;
 | |
|     final displayWidth = canvasModel.getDisplayWidth();
 | |
|     final displayHeight = canvasModel.getDisplayHeight();
 | |
|     final requiredWidth =
 | |
|         CanvasModel.leftToEdge + displayWidth + CanvasModel.rightToEdge;
 | |
|     final requiredHeight =
 | |
|         CanvasModel.topToEdge + displayHeight + CanvasModel.bottomToEdge;
 | |
|     return selfWidth > (requiredWidth * scale) &&
 | |
|         selfHeight > (requiredHeight * scale);
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _DisplayMenu extends StatefulWidget {
 | |
|   final String id;
 | |
|   final FFI ffi;
 | |
|   final ToolbarState state;
 | |
|   final Function(bool) setFullscreen;
 | |
|   final Widget pluginItem;
 | |
|   _DisplayMenu(
 | |
|       {Key? key,
 | |
|       required this.id,
 | |
|       required this.ffi,
 | |
|       required this.state,
 | |
|       required this.setFullscreen})
 | |
|       : pluginItem = LocationItem.createLocationItem(
 | |
|           id,
 | |
|           ffi,
 | |
|           kLocationClientRemoteToolbarDisplay,
 | |
|           true,
 | |
|         ),
 | |
|         super(key: key);
 | |
| 
 | |
|   @override
 | |
|   State<_DisplayMenu> createState() => _DisplayMenuState();
 | |
| }
 | |
| 
 | |
| class _DisplayMenuState extends State<_DisplayMenu> {
 | |
|   late final ScreenAdjustor _screenAdjustor = ScreenAdjustor(
 | |
|     id: widget.id,
 | |
|     ffi: widget.ffi,
 | |
|     cbExitFullscreen: () => widget.setFullscreen(false),
 | |
|   );
 | |
| 
 | |
|   int get windowId => stateGlobal.windowId;
 | |
|   Map<String, bool> get perms => widget.ffi.ffiModel.permissions;
 | |
|   PeerInfo get pi => widget.ffi.ffiModel.pi;
 | |
|   FfiModel get ffiModel => widget.ffi.ffiModel;
 | |
|   FFI get ffi => widget.ffi;
 | |
|   String get id => widget.id;
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     _screenAdjustor.updateScreen();
 | |
|     menuChildrenGetter() {
 | |
|       final menuChildren = <Widget>[
 | |
|         _screenAdjustor.adjustWindow(context),
 | |
|         viewStyle(),
 | |
|         scrollStyle(),
 | |
|         imageQuality(),
 | |
|         codec(),
 | |
|         _ResolutionsMenu(
 | |
|           id: widget.id,
 | |
|           ffi: widget.ffi,
 | |
|           screenAdjustor: _screenAdjustor,
 | |
|         ),
 | |
|         if (showVirtualDisplayMenu(ffi))
 | |
|           _SubmenuButton(
 | |
|             ffi: widget.ffi,
 | |
|             menuChildren: getVirtualDisplayMenuChildren(ffi, id, null),
 | |
|             child: Text(translate("Virtual display")),
 | |
|           ),
 | |
|         cursorToggles(),
 | |
|         Divider(),
 | |
|         toggles(),
 | |
|       ];
 | |
|       // privacy mode
 | |
|       if (ffiModel.keyboard && pi.features.privacyMode) {
 | |
|         final privacyModeState = PrivacyModeState.find(id);
 | |
|         final privacyModeList =
 | |
|             toolbarPrivacyMode(privacyModeState, context, id, ffi);
 | |
|         if (privacyModeList.length == 1) {
 | |
|           menuChildren.add(CkbMenuButton(
 | |
|               value: privacyModeList[0].value,
 | |
|               onChanged: privacyModeList[0].onChanged,
 | |
|               child: privacyModeList[0].child,
 | |
|               ffi: ffi));
 | |
|         } else if (privacyModeList.length > 1) {
 | |
|           menuChildren.addAll([
 | |
|             Divider(),
 | |
|             _SubmenuButton(
 | |
|                 ffi: widget.ffi,
 | |
|                 child: Text(translate('Privacy mode')),
 | |
|                 menuChildren: privacyModeList
 | |
|                     .map((e) => CkbMenuButton(
 | |
|                         value: e.value,
 | |
|                         onChanged: e.onChanged,
 | |
|                         child: e.child,
 | |
|                         ffi: ffi))
 | |
|                     .toList()),
 | |
|           ]);
 | |
|         }
 | |
|       }
 | |
|       menuChildren.add(widget.pluginItem);
 | |
|       return menuChildren;
 | |
|     }
 | |
| 
 | |
|     return _IconSubmenuButton(
 | |
|       tooltip: 'Display Settings',
 | |
|       svg: "assets/display.svg",
 | |
|       ffi: widget.ffi,
 | |
|       color: _ToolbarTheme.blueColor,
 | |
|       hoverColor: _ToolbarTheme.hoverBlueColor,
 | |
|       menuChildrenGetter: menuChildrenGetter,
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   viewStyle() {
 | |
|     return futureBuilder(
 | |
|         future: toolbarViewStyle(context, widget.id, widget.ffi),
 | |
|         hasData: (data) {
 | |
|           final v = data as List<TRadioMenu<String>>;
 | |
|           return Column(children: [
 | |
|             ...v
 | |
|                 .map((e) => RdoMenuButton<String>(
 | |
|                     value: e.value,
 | |
|                     groupValue: e.groupValue,
 | |
|                     onChanged: e.onChanged,
 | |
|                     child: e.child,
 | |
|                     ffi: ffi))
 | |
|                 .toList(),
 | |
|             Divider(),
 | |
|           ]);
 | |
|         });
 | |
|   }
 | |
| 
 | |
|   scrollStyle() {
 | |
|     return futureBuilder(future: () async {
 | |
|       final viewStyle =
 | |
|           await bind.sessionGetViewStyle(sessionId: ffi.sessionId) ?? '';
 | |
|       final visible = viewStyle == kRemoteViewStyleOriginal;
 | |
|       final scrollStyle =
 | |
|           await bind.sessionGetScrollStyle(sessionId: ffi.sessionId) ?? '';
 | |
|       return {'visible': visible, 'scrollStyle': scrollStyle};
 | |
|     }(), hasData: (data) {
 | |
|       final visible = data['visible'] as bool;
 | |
|       if (!visible) return Offstage();
 | |
|       final groupValue = data['scrollStyle'] as String;
 | |
|       onChange(String? value) async {
 | |
|         if (value == null) return;
 | |
|         await bind.sessionSetScrollStyle(
 | |
|             sessionId: ffi.sessionId, value: value);
 | |
|         widget.ffi.canvasModel.updateScrollStyle();
 | |
|       }
 | |
| 
 | |
|       final enabled = widget.ffi.canvasModel.imageOverflow.value;
 | |
|       return Column(children: [
 | |
|         RdoMenuButton<String>(
 | |
|           child: Text(translate('ScrollAuto')),
 | |
|           value: kRemoteScrollStyleAuto,
 | |
|           groupValue: groupValue,
 | |
|           onChanged: enabled ? (value) => onChange(value) : null,
 | |
|           ffi: widget.ffi,
 | |
|         ),
 | |
|         RdoMenuButton<String>(
 | |
|           child: Text(translate('Scrollbar')),
 | |
|           value: kRemoteScrollStyleBar,
 | |
|           groupValue: groupValue,
 | |
|           onChanged: enabled ? (value) => onChange(value) : null,
 | |
|           ffi: widget.ffi,
 | |
|         ),
 | |
|         Divider(),
 | |
|       ]);
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   imageQuality() {
 | |
|     return futureBuilder(
 | |
|         future: toolbarImageQuality(context, widget.id, widget.ffi),
 | |
|         hasData: (data) {
 | |
|           final v = data as List<TRadioMenu<String>>;
 | |
|           return _SubmenuButton(
 | |
|             ffi: widget.ffi,
 | |
|             child: Text(translate('Image Quality')),
 | |
|             menuChildren: v
 | |
|                 .map((e) => RdoMenuButton<String>(
 | |
|                     value: e.value,
 | |
|                     groupValue: e.groupValue,
 | |
|                     onChanged: e.onChanged,
 | |
|                     child: e.child,
 | |
|                     ffi: ffi))
 | |
|                 .toList(),
 | |
|           );
 | |
|         });
 | |
|   }
 | |
| 
 | |
|   codec() {
 | |
|     return futureBuilder(
 | |
|         future: toolbarCodec(context, id, ffi),
 | |
|         hasData: (data) {
 | |
|           final v = data as List<TRadioMenu<String>>;
 | |
|           if (v.isEmpty) return Offstage();
 | |
| 
 | |
|           return _SubmenuButton(
 | |
|               ffi: widget.ffi,
 | |
|               child: Text(translate('Codec')),
 | |
|               menuChildren: v
 | |
|                   .map((e) => RdoMenuButton(
 | |
|                       value: e.value,
 | |
|                       groupValue: e.groupValue,
 | |
|                       onChanged: e.onChanged,
 | |
|                       child: e.child,
 | |
|                       ffi: ffi))
 | |
|                   .toList());
 | |
|         });
 | |
|   }
 | |
| 
 | |
|   cursorToggles() {
 | |
|     return futureBuilder(
 | |
|         future: toolbarCursor(context, id, ffi),
 | |
|         hasData: (data) {
 | |
|           final v = data as List<TToggleMenu>;
 | |
|           if (v.isEmpty) return Offstage();
 | |
|           return Column(children: [
 | |
|             Divider(),
 | |
|             ...v
 | |
|                 .map((e) => CkbMenuButton(
 | |
|                     value: e.value,
 | |
|                     onChanged: e.onChanged,
 | |
|                     child: e.child,
 | |
|                     ffi: ffi))
 | |
|                 .toList(),
 | |
|           ]);
 | |
|         });
 | |
|   }
 | |
| 
 | |
|   toggles() {
 | |
|     return futureBuilder(
 | |
|         future: toolbarDisplayToggle(context, id, ffi),
 | |
|         hasData: (data) {
 | |
|           final v = data as List<TToggleMenu>;
 | |
|           if (v.isEmpty) return Offstage();
 | |
|           return Column(
 | |
|               children: v
 | |
|                   .map((e) => CkbMenuButton(
 | |
|                       value: e.value,
 | |
|                       onChanged: e.onChanged,
 | |
|                       child: e.child,
 | |
|                       ffi: ffi))
 | |
|                   .toList());
 | |
|         });
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _ResolutionsMenu extends StatefulWidget {
 | |
|   final String id;
 | |
|   final FFI ffi;
 | |
|   final ScreenAdjustor screenAdjustor;
 | |
| 
 | |
|   _ResolutionsMenu({
 | |
|     Key? key,
 | |
|     required this.id,
 | |
|     required this.ffi,
 | |
|     required this.screenAdjustor,
 | |
|   }) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   State<_ResolutionsMenu> createState() => _ResolutionsMenuState();
 | |
| }
 | |
| 
 | |
| const double _kCustomResolutionEditingWidth = 42;
 | |
| const _kCustomResolutionValue = 'custom';
 | |
| 
 | |
| class _ResolutionsMenuState extends State<_ResolutionsMenu> {
 | |
|   String _groupValue = '';
 | |
|   Resolution? _localResolution;
 | |
| 
 | |
|   late final TextEditingController _customWidth =
 | |
|       TextEditingController(text: rect?.width.toInt().toString() ?? '');
 | |
|   late final TextEditingController _customHeight =
 | |
|       TextEditingController(text: rect?.height.toInt().toString() ?? '');
 | |
| 
 | |
|   FFI get ffi => widget.ffi;
 | |
|   PeerInfo get pi => widget.ffi.ffiModel.pi;
 | |
|   FfiModel get ffiModel => widget.ffi.ffiModel;
 | |
|   Rect? get rect => scaledRect();
 | |
|   List<Resolution> get resolutions => pi.resolutions;
 | |
|   bool get isWayland => bind.mainCurrentIsWayland();
 | |
| 
 | |
|   @override
 | |
|   void initState() {
 | |
|     super.initState();
 | |
|     WidgetsBinding.instance.addPostFrameCallback((_) {
 | |
|       _getLocalResolutionWayland();
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   Rect? scaledRect() {
 | |
|     final scale = pi.scaleOfDisplay(pi.currentDisplay);
 | |
|     final rect = ffiModel.rect;
 | |
|     if (rect == null) {
 | |
|       return null;
 | |
|     }
 | |
|     return Rect.fromLTWH(
 | |
|       rect.left,
 | |
|       rect.top,
 | |
|       rect.width / scale,
 | |
|       rect.height / scale,
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     final isVirtualDisplay = ffiModel.isVirtualDisplayResolution;
 | |
|     final visible = ffiModel.keyboard &&
 | |
|         (isVirtualDisplay || resolutions.length > 1) &&
 | |
|         pi.currentDisplay != kAllDisplayValue;
 | |
|     if (!visible) return Offstage();
 | |
|     final showOriginalBtn =
 | |
|         ffiModel.isOriginalResolutionSet && !ffiModel.isOriginalResolution;
 | |
|     final showFitLocalBtn = !_isRemoteResolutionFitLocal();
 | |
|     _setGroupValue();
 | |
|     return _SubmenuButton(
 | |
|       ffi: widget.ffi,
 | |
|       menuChildren: <Widget>[
 | |
|             _OriginalResolutionMenuButton(context, showOriginalBtn),
 | |
|             _FitLocalResolutionMenuButton(context, showFitLocalBtn),
 | |
|             _customResolutionMenuButton(context, isVirtualDisplay),
 | |
|             _menuDivider(showOriginalBtn, showFitLocalBtn, isVirtualDisplay),
 | |
|           ] +
 | |
|           _supportedResolutionMenuButtons(),
 | |
|       child: Text(translate("Resolution")),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   _setGroupValue() {
 | |
|     if (pi.currentDisplay == kAllDisplayValue) {
 | |
|       return;
 | |
|     }
 | |
|     final lastGroupValue =
 | |
|         stateGlobal.getLastResolutionGroupValue(widget.id, pi.currentDisplay);
 | |
|     if (lastGroupValue == _kCustomResolutionValue) {
 | |
|       _groupValue = _kCustomResolutionValue;
 | |
|     } else {
 | |
|       _groupValue =
 | |
|           '${(rect?.width ?? 0).toInt()}x${(rect?.height ?? 0).toInt()}';
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _menuDivider(
 | |
|       bool showOriginalBtn, bool showFitLocalBtn, bool isVirtualDisplay) {
 | |
|     return Offstage(
 | |
|       offstage: !(showOriginalBtn || showFitLocalBtn || isVirtualDisplay),
 | |
|       child: Divider(),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   Future<void> _getLocalResolutionWayland() async {
 | |
|     if (!isWayland) return _getLocalResolution();
 | |
|     final window = await window_size.getWindowInfo();
 | |
|     final screen = window.screen;
 | |
|     if (screen != null) {
 | |
|       setState(() {
 | |
|         _localResolution = Resolution(
 | |
|           screen.frame.width.toInt(),
 | |
|           screen.frame.height.toInt(),
 | |
|         );
 | |
|       });
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _getLocalResolution() {
 | |
|     _localResolution = null;
 | |
|     final String mainDisplay = bind.mainGetMainDisplay();
 | |
|     if (mainDisplay.isNotEmpty) {
 | |
|       try {
 | |
|         final display = json.decode(mainDisplay);
 | |
|         if (display['w'] != null && display['h'] != null) {
 | |
|           _localResolution = Resolution(display['w'], display['h']);
 | |
|           if (isWeb) {
 | |
|             if (display['scaleFactor'] != null) {
 | |
|               _localResolution = Resolution(
 | |
|                 (display['w'] / display['scaleFactor']).toInt(),
 | |
|                 (display['h'] / display['scaleFactor']).toInt(),
 | |
|               );
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|       } catch (e) {
 | |
|         debugPrint('Failed to decode $mainDisplay, $e');
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // This widget has been unmounted, so the State no longer has a context
 | |
|   _onChanged(String? value) async {
 | |
|     if (pi.currentDisplay == kAllDisplayValue) {
 | |
|       return;
 | |
|     }
 | |
|     stateGlobal.setLastResolutionGroupValue(
 | |
|         widget.id, pi.currentDisplay, value);
 | |
|     if (value == null) return;
 | |
| 
 | |
|     int? w;
 | |
|     int? h;
 | |
|     if (value == _kCustomResolutionValue) {
 | |
|       w = int.tryParse(_customWidth.text);
 | |
|       h = int.tryParse(_customHeight.text);
 | |
|     } else {
 | |
|       final list = value.split('x');
 | |
|       if (list.length == 2) {
 | |
|         w = int.tryParse(list[0]);
 | |
|         h = int.tryParse(list[1]);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (w != null && h != null) {
 | |
|       if (w != rect?.width.toInt() || h != rect?.height.toInt()) {
 | |
|         await _changeResolution(w, h);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _changeResolution(int w, int h) async {
 | |
|     if (pi.currentDisplay == kAllDisplayValue) {
 | |
|       return;
 | |
|     }
 | |
|     await bind.sessionChangeResolution(
 | |
|       sessionId: ffi.sessionId,
 | |
|       display: pi.currentDisplay,
 | |
|       width: w,
 | |
|       height: h,
 | |
|     );
 | |
|     Future.delayed(Duration(seconds: 3), () async {
 | |
|       final rect = ffiModel.rect;
 | |
|       if (rect == null) {
 | |
|         return;
 | |
|       }
 | |
|       if (w == rect.width.toInt() && h == rect.height.toInt()) {
 | |
|         if (await widget.screenAdjustor.isWindowCanBeAdjusted()) {
 | |
|           widget.screenAdjustor.doAdjustWindow(context);
 | |
|         }
 | |
|       }
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   Widget _OriginalResolutionMenuButton(
 | |
|       BuildContext context, bool showOriginalBtn) {
 | |
|     final display = pi.tryGetDisplayIfNotAllDisplay();
 | |
|     if (display == null) {
 | |
|       return Offstage();
 | |
|     }
 | |
|     if (!resolutions.any((e) =>
 | |
|         e.width == display.originalWidth &&
 | |
|         e.height == display.originalHeight)) {
 | |
|       return Offstage();
 | |
|     }
 | |
|     return Offstage(
 | |
|       offstage: !showOriginalBtn,
 | |
|       child: MenuButton(
 | |
|         onPressed: () =>
 | |
|             _changeResolution(display.originalWidth, display.originalHeight),
 | |
|         ffi: widget.ffi,
 | |
|         child: Text(
 | |
|             '${translate('resolution_original_tip')} ${display.originalWidth}x${display.originalHeight}'),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   Widget _FitLocalResolutionMenuButton(
 | |
|       BuildContext context, bool showFitLocalBtn) {
 | |
|     return Offstage(
 | |
|       offstage: !showFitLocalBtn,
 | |
|       child: MenuButton(
 | |
|         onPressed: () {
 | |
|           final resolution = _getBestFitResolution();
 | |
|           if (resolution != null) {
 | |
|             _changeResolution(resolution.width, resolution.height);
 | |
|           }
 | |
|         },
 | |
|         ffi: widget.ffi,
 | |
|         child: Text(
 | |
|             '${translate('resolution_fit_local_tip')} ${_localResolution?.width ?? 0}x${_localResolution?.height ?? 0}'),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   Widget _customResolutionMenuButton(BuildContext context, isVirtualDisplay) {
 | |
|     return Offstage(
 | |
|       offstage: !isVirtualDisplay,
 | |
|       child: RdoMenuButton(
 | |
|         value: _kCustomResolutionValue,
 | |
|         groupValue: _groupValue,
 | |
|         onChanged: (String? value) => _onChanged(value),
 | |
|         ffi: widget.ffi,
 | |
|         child: Row(
 | |
|           children: [
 | |
|             Text('${translate('resolution_custom_tip')} '),
 | |
|             SizedBox(
 | |
|               width: _kCustomResolutionEditingWidth,
 | |
|               child: _resolutionInput(_customWidth),
 | |
|             ),
 | |
|             Text(' x '),
 | |
|             SizedBox(
 | |
|               width: _kCustomResolutionEditingWidth,
 | |
|               child: _resolutionInput(_customHeight),
 | |
|             ),
 | |
|           ],
 | |
|         ),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   TextField _resolutionInput(TextEditingController controller) {
 | |
|     return TextField(
 | |
|       decoration: InputDecoration(
 | |
|         border: InputBorder.none,
 | |
|         isDense: true,
 | |
|         contentPadding: EdgeInsets.fromLTRB(3, 3, 3, 3),
 | |
|       ),
 | |
|       keyboardType: TextInputType.number,
 | |
|       inputFormatters: <TextInputFormatter>[
 | |
|         FilteringTextInputFormatter.digitsOnly,
 | |
|         LengthLimitingTextInputFormatter(4),
 | |
|         FilteringTextInputFormatter.allow(RegExp(r'[0-9]')),
 | |
|       ],
 | |
|       controller: controller,
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   List<Widget> _supportedResolutionMenuButtons() => resolutions
 | |
|       .map((e) => RdoMenuButton(
 | |
|           value: '${e.width}x${e.height}',
 | |
|           groupValue: _groupValue,
 | |
|           onChanged: (String? value) => _onChanged(value),
 | |
|           ffi: widget.ffi,
 | |
|           child: Text('${e.width}x${e.height}')))
 | |
|       .toList();
 | |
| 
 | |
|   Resolution? _getBestFitResolution() {
 | |
|     if (_localResolution == null) {
 | |
|       return null;
 | |
|     }
 | |
| 
 | |
|     if (ffiModel.isVirtualDisplayResolution) {
 | |
|       return _localResolution!;
 | |
|     }
 | |
| 
 | |
|     for (final r in resolutions) {
 | |
|       if (r.width == _localResolution!.width &&
 | |
|           r.height == _localResolution!.height) {
 | |
|         return r;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   bool _isRemoteResolutionFitLocal() {
 | |
|     if (_localResolution == null) {
 | |
|       return true;
 | |
|     }
 | |
|     final bestFitResolution = _getBestFitResolution();
 | |
|     if (bestFitResolution == null) {
 | |
|       return true;
 | |
|     }
 | |
|     return bestFitResolution.width == rect?.width.toInt() &&
 | |
|         bestFitResolution.height == rect?.height.toInt();
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _KeyboardMenu extends StatelessWidget {
 | |
|   final String id;
 | |
|   final FFI ffi;
 | |
|   _KeyboardMenu({
 | |
|     Key? key,
 | |
|     required this.id,
 | |
|     required this.ffi,
 | |
|   }) : super(key: key);
 | |
| 
 | |
|   PeerInfo get pi => ffi.ffiModel.pi;
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     var ffiModel = Provider.of<FfiModel>(context);
 | |
|     if (!ffiModel.keyboard) return Offstage();
 | |
|     toolbarToggles() => toolbarKeyboardToggles(ffi)
 | |
|         .map((e) => CkbMenuButton(
 | |
|             value: e.value, onChanged: e.onChanged, child: e.child, ffi: ffi))
 | |
|         .toList();
 | |
|     return _IconSubmenuButton(
 | |
|         tooltip: 'Keyboard Settings',
 | |
|         svg: "assets/keyboard.svg",
 | |
|         ffi: ffi,
 | |
|         color: _ToolbarTheme.blueColor,
 | |
|         hoverColor: _ToolbarTheme.hoverBlueColor,
 | |
|         menuChildrenGetter: () => [
 | |
|               keyboardMode(),
 | |
|               localKeyboardType(),
 | |
|               inputSource(),
 | |
|               Divider(),
 | |
|               viewMode(),
 | |
|               Divider(),
 | |
|               ...toolbarToggles(),
 | |
|               ...mobileActions(),
 | |
|             ]);
 | |
|   }
 | |
| 
 | |
|   keyboardMode() {
 | |
|     return futureBuilder(future: () async {
 | |
|       return await bind.sessionGetKeyboardMode(sessionId: ffi.sessionId) ??
 | |
|           kKeyLegacyMode;
 | |
|     }(), hasData: (data) {
 | |
|       final groupValue = data as String;
 | |
|       List<InputModeMenu> modes = [
 | |
|         InputModeMenu(key: kKeyLegacyMode, menu: 'Legacy mode'),
 | |
|         InputModeMenu(key: kKeyMapMode, menu: 'Map mode'),
 | |
|         InputModeMenu(key: kKeyTranslateMode, menu: 'Translate mode'),
 | |
|       ];
 | |
|       List<RdoMenuButton> list = [];
 | |
|       final enabled = !ffi.ffiModel.viewOnly;
 | |
|       onChanged(String? value) async {
 | |
|         if (value == null) return;
 | |
|         await bind.sessionSetKeyboardMode(
 | |
|             sessionId: ffi.sessionId, value: value);
 | |
|         await ffi.inputModel.updateKeyboardMode();
 | |
|       }
 | |
| 
 | |
|       // If use flutter to grab keys, we can only use one mode.
 | |
|       // Map mode and Legacy mode, at least one of them is supported.
 | |
|       String? modeOnly;
 | |
|       // Keep both map and legacy mode on web at the moment.
 | |
|       // TODO: Remove legacy mode after web supports translate mode on web.
 | |
|       if (isInputSourceFlutter && isDesktop) {
 | |
|         if (bind.sessionIsKeyboardModeSupported(
 | |
|             sessionId: ffi.sessionId, mode: kKeyMapMode)) {
 | |
|           modeOnly = kKeyMapMode;
 | |
|         } else if (bind.sessionIsKeyboardModeSupported(
 | |
|             sessionId: ffi.sessionId, mode: kKeyLegacyMode)) {
 | |
|           modeOnly = kKeyLegacyMode;
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       for (InputModeMenu mode in modes) {
 | |
|         if (modeOnly != null && mode.key != modeOnly) {
 | |
|           continue;
 | |
|         } else if (!bind.sessionIsKeyboardModeSupported(
 | |
|             sessionId: ffi.sessionId, mode: mode.key)) {
 | |
|           continue;
 | |
|         }
 | |
| 
 | |
|         if (pi.isWayland && mode.key != kKeyMapMode) {
 | |
|           continue;
 | |
|         }
 | |
| 
 | |
|         var text = translate(mode.menu);
 | |
|         if (mode.key == kKeyTranslateMode) {
 | |
|           text = '$text beta';
 | |
|         }
 | |
|         list.add(RdoMenuButton<String>(
 | |
|           child: Text(text),
 | |
|           value: mode.key,
 | |
|           groupValue: groupValue,
 | |
|           onChanged: enabled ? onChanged : null,
 | |
|           ffi: ffi,
 | |
|         ));
 | |
|       }
 | |
|       return Column(children: list);
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   localKeyboardType() {
 | |
|     final localPlatform = getLocalPlatformForKBLayoutType(pi.platform);
 | |
|     final visible = localPlatform != '';
 | |
|     if (!visible) return Offstage();
 | |
|     final enabled = !ffi.ffiModel.viewOnly;
 | |
|     return Column(
 | |
|       children: [
 | |
|         Divider(),
 | |
|         MenuButton(
 | |
|           child: Text(
 | |
|               '${translate('Local keyboard type')}: ${KBLayoutType.value}'),
 | |
|           trailingIcon: const Icon(Icons.settings),
 | |
|           ffi: ffi,
 | |
|           onPressed: enabled
 | |
|               ? () => showKBLayoutTypeChooser(localPlatform, ffi.dialogManager)
 | |
|               : null,
 | |
|         )
 | |
|       ],
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   inputSource() {
 | |
|     final supportedInputSource = bind.mainSupportedInputSource();
 | |
|     if (supportedInputSource.isEmpty) return Offstage();
 | |
|     late final List<dynamic> supportedInputSourceList;
 | |
|     try {
 | |
|       supportedInputSourceList = jsonDecode(supportedInputSource);
 | |
|     } catch (e) {
 | |
|       debugPrint('Failed to decode $supportedInputSource, $e');
 | |
|       return;
 | |
|     }
 | |
|     if (supportedInputSourceList.length < 2) return Offstage();
 | |
|     final inputSource = stateGlobal.getInputSource();
 | |
|     final enabled = !ffi.ffiModel.viewOnly;
 | |
|     final children = <Widget>[Divider()];
 | |
|     children.addAll(supportedInputSourceList.map((e) {
 | |
|       final d = e as List<dynamic>;
 | |
|       return RdoMenuButton<String>(
 | |
|         child: Text(translate(d[1] as String)),
 | |
|         value: d[0] as String,
 | |
|         groupValue: inputSource,
 | |
|         onChanged: enabled
 | |
|             ? (v) async {
 | |
|                 if (v != null) {
 | |
|                   await stateGlobal.setInputSource(ffi.sessionId, v);
 | |
|                   await ffi.ffiModel.checkDesktopKeyboardMode();
 | |
|                   await ffi.inputModel.updateKeyboardMode();
 | |
|                 }
 | |
|               }
 | |
|             : null,
 | |
|         ffi: ffi,
 | |
|       );
 | |
|     }));
 | |
|     return Column(children: children);
 | |
|   }
 | |
| 
 | |
|   viewMode() {
 | |
|     final ffiModel = ffi.ffiModel;
 | |
|     final enabled = versionCmp(pi.version, '1.2.0') >= 0 && ffiModel.keyboard;
 | |
|     return CkbMenuButton(
 | |
|         value: ffiModel.viewOnly,
 | |
|         onChanged: enabled
 | |
|             ? (value) async {
 | |
|                 if (value == null) return;
 | |
|                 await bind.sessionToggleOption(
 | |
|                     sessionId: ffi.sessionId, value: kOptionToggleViewOnly);
 | |
|                 final viewOnly = await bind.sessionGetToggleOption(
 | |
|                     sessionId: ffi.sessionId, arg: kOptionToggleViewOnly);
 | |
|                 ffiModel.setViewOnly(id, viewOnly ?? value);
 | |
|               }
 | |
|             : null,
 | |
|         ffi: ffi,
 | |
|         child: Text(translate('View Mode')));
 | |
|   }
 | |
| 
 | |
|   mobileActions() {
 | |
|     if (pi.platform != kPeerPlatformAndroid) return [];
 | |
|     final enabled = versionCmp(pi.version, '1.2.7') >= 0;
 | |
|     if (!enabled) return [];
 | |
|     return [
 | |
|       Divider(),
 | |
|       MenuButton(
 | |
|           child: Text(translate('Back')),
 | |
|           onPressed: () => ffi.inputModel.onMobileBack(),
 | |
|           ffi: ffi),
 | |
|       MenuButton(
 | |
|           child: Text(translate('Home')),
 | |
|           onPressed: () => ffi.inputModel.onMobileHome(),
 | |
|           ffi: ffi),
 | |
|       MenuButton(
 | |
|           child: Text(translate('Apps')),
 | |
|           onPressed: () => ffi.inputModel.onMobileApps(),
 | |
|           ffi: ffi),
 | |
|       MenuButton(
 | |
|           child: Text(translate('Volume up')),
 | |
|           onPressed: () => ffi.inputModel.onMobileVolumeUp(),
 | |
|           ffi: ffi),
 | |
|       MenuButton(
 | |
|           child: Text(translate('Volume down')),
 | |
|           onPressed: () => ffi.inputModel.onMobileVolumeDown(),
 | |
|           ffi: ffi),
 | |
|       MenuButton(
 | |
|           child: Text(translate('Power')),
 | |
|           onPressed: () => ffi.inputModel.onMobilePower(),
 | |
|           ffi: ffi),
 | |
|     ];
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _ChatMenu extends StatefulWidget {
 | |
|   final String id;
 | |
|   final FFI ffi;
 | |
|   _ChatMenu({
 | |
|     Key? key,
 | |
|     required this.id,
 | |
|     required this.ffi,
 | |
|   }) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   State<_ChatMenu> createState() => _ChatMenuState();
 | |
| }
 | |
| 
 | |
| class _ChatMenuState extends State<_ChatMenu> {
 | |
|   // Using in StatelessWidget got `Looking up a deactivated widget's ancestor is unsafe`.
 | |
|   final chatButtonKey = GlobalKey();
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     if (isWeb) {
 | |
|       return buildTextChatButton();
 | |
|     } else {
 | |
|       return _IconSubmenuButton(
 | |
|           tooltip: 'Chat',
 | |
|           key: chatButtonKey,
 | |
|           svg: 'assets/chat.svg',
 | |
|           ffi: widget.ffi,
 | |
|           color: _ToolbarTheme.blueColor,
 | |
|           hoverColor: _ToolbarTheme.hoverBlueColor,
 | |
|           menuChildrenGetter: () => [textChat(), voiceCall()]);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   buildTextChatButton() {
 | |
|     return _IconMenuButton(
 | |
|       assetName: 'assets/message_24dp_5F6368.svg',
 | |
|       tooltip: 'Text chat',
 | |
|       key: chatButtonKey,
 | |
|       onPressed: _textChatOnPressed,
 | |
|       color: _ToolbarTheme.blueColor,
 | |
|       hoverColor: _ToolbarTheme.hoverBlueColor,
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   textChat() {
 | |
|     return MenuButton(
 | |
|         child: Text(translate('Text chat')),
 | |
|         ffi: widget.ffi,
 | |
|         onPressed: _textChatOnPressed);
 | |
|   }
 | |
| 
 | |
|   _textChatOnPressed() {
 | |
|     RenderBox? renderBox =
 | |
|         chatButtonKey.currentContext?.findRenderObject() as RenderBox?;
 | |
|     Offset? initPos;
 | |
|     if (renderBox != null) {
 | |
|       final pos = renderBox.localToGlobal(Offset.zero);
 | |
|       initPos = Offset(pos.dx, pos.dy + _ToolbarTheme.dividerHeight);
 | |
|     }
 | |
|     widget.ffi.chatModel
 | |
|         .changeCurrentKey(MessageKey(widget.ffi.id, ChatModel.clientModeID));
 | |
|     widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos);
 | |
|   }
 | |
| 
 | |
|   voiceCall() {
 | |
|     return MenuButton(
 | |
|       child: Text(translate('Voice call')),
 | |
|       ffi: widget.ffi,
 | |
|       onPressed: () =>
 | |
|           bind.sessionRequestVoiceCall(sessionId: widget.ffi.sessionId),
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _VoiceCallMenu extends StatelessWidget {
 | |
|   final String id;
 | |
|   final FFI ffi;
 | |
|   _VoiceCallMenu({
 | |
|     Key? key,
 | |
|     required this.id,
 | |
|     required this.ffi,
 | |
|   }) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     menuChildrenGetter() {
 | |
|       final audioInput = AudioInput(
 | |
|         builder: (devices, currentDevice, setDevice) {
 | |
|           return Column(
 | |
|             children: devices
 | |
|                 .map((d) => RdoMenuButton<String>(
 | |
|                       child: Container(
 | |
|                         child: Text(
 | |
|                           d,
 | |
|                           overflow: TextOverflow.ellipsis,
 | |
|                         ),
 | |
|                         constraints: BoxConstraints(maxWidth: 250),
 | |
|                       ),
 | |
|                       value: d,
 | |
|                       groupValue: currentDevice,
 | |
|                       onChanged: (v) {
 | |
|                         if (v != null) setDevice(v);
 | |
|                       },
 | |
|                       ffi: ffi,
 | |
|                     ))
 | |
|                 .toList(),
 | |
|           );
 | |
|         },
 | |
|         isCm: false,
 | |
|         isVoiceCall: true,
 | |
|       );
 | |
|       return [
 | |
|         audioInput,
 | |
|         Divider(),
 | |
|         MenuButton(
 | |
|           child: Text(translate('End call')),
 | |
|           onPressed: () => bind.sessionCloseVoiceCall(sessionId: ffi.sessionId),
 | |
|           ffi: ffi,
 | |
|         ),
 | |
|       ];
 | |
|     }
 | |
| 
 | |
|     return Obx(
 | |
|       () {
 | |
|         switch (ffi.chatModel.voiceCallStatus.value) {
 | |
|           case VoiceCallStatus.waitingForResponse:
 | |
|             return buildCallWaiting(context);
 | |
|           case VoiceCallStatus.connected:
 | |
|             return _IconSubmenuButton(
 | |
|               tooltip: 'Voice call',
 | |
|               svg: 'assets/voice_call.svg',
 | |
|               color: _ToolbarTheme.blueColor,
 | |
|               hoverColor: _ToolbarTheme.hoverBlueColor,
 | |
|               menuChildrenGetter: menuChildrenGetter,
 | |
|               ffi: ffi,
 | |
|             );
 | |
|           default:
 | |
|             return Offstage();
 | |
|         }
 | |
|       },
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   Widget buildCallWaiting(BuildContext context) {
 | |
|     return _IconMenuButton(
 | |
|       assetName: "assets/call_wait.svg",
 | |
|       tooltip: "Waiting",
 | |
|       onPressed: () => bind.sessionCloseVoiceCall(sessionId: ffi.sessionId),
 | |
|       color: _ToolbarTheme.redColor,
 | |
|       hoverColor: _ToolbarTheme.hoverRedColor,
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _RecordMenu extends StatelessWidget {
 | |
|   const _RecordMenu({Key? key}) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     var ffi = Provider.of<FfiModel>(context);
 | |
|     var recordingModel = Provider.of<RecordingModel>(context);
 | |
|     final visible =
 | |
|         (recordingModel.start || ffi.permissions['recording'] != false);
 | |
|     if (!visible) return Offstage();
 | |
|     return _IconMenuButton(
 | |
|       assetName: 'assets/rec.svg',
 | |
|       tooltip: recordingModel.start
 | |
|           ? 'Stop session recording'
 | |
|           : 'Start session recording',
 | |
|       onPressed: () => recordingModel.toggle(),
 | |
|       color: recordingModel.start
 | |
|           ? _ToolbarTheme.redColor
 | |
|           : _ToolbarTheme.blueColor,
 | |
|       hoverColor: recordingModel.start
 | |
|           ? _ToolbarTheme.hoverRedColor
 | |
|           : _ToolbarTheme.hoverBlueColor,
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _CloseMenu extends StatelessWidget {
 | |
|   final String id;
 | |
|   final FFI ffi;
 | |
|   const _CloseMenu({Key? key, required this.id, required this.ffi})
 | |
|       : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return _IconMenuButton(
 | |
|       assetName: 'assets/close.svg',
 | |
|       tooltip: 'Close',
 | |
|       onPressed: () => closeConnection(id: id),
 | |
|       color: _ToolbarTheme.redColor,
 | |
|       hoverColor: _ToolbarTheme.hoverRedColor,
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _IconMenuButton extends StatefulWidget {
 | |
|   final String? assetName;
 | |
|   final Widget? icon;
 | |
|   final String tooltip;
 | |
|   final Color color;
 | |
|   final Color hoverColor;
 | |
|   final VoidCallback? onPressed;
 | |
|   final double? hMargin;
 | |
|   final double? vMargin;
 | |
|   final bool topLevel;
 | |
|   final double? width;
 | |
|   const _IconMenuButton({
 | |
|     Key? key,
 | |
|     this.assetName,
 | |
|     this.icon,
 | |
|     required this.tooltip,
 | |
|     required this.color,
 | |
|     required this.hoverColor,
 | |
|     required this.onPressed,
 | |
|     this.hMargin,
 | |
|     this.vMargin,
 | |
|     this.topLevel = true,
 | |
|     this.width,
 | |
|   }) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   State<_IconMenuButton> createState() => _IconMenuButtonState();
 | |
| }
 | |
| 
 | |
| class _IconMenuButtonState extends State<_IconMenuButton> {
 | |
|   bool hover = false;
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     assert(widget.assetName != null || widget.icon != null);
 | |
|     final icon = widget.icon ??
 | |
|         SvgPicture.asset(
 | |
|           widget.assetName!,
 | |
|           colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn),
 | |
|           width: _ToolbarTheme.buttonSize,
 | |
|           height: _ToolbarTheme.buttonSize,
 | |
|         );
 | |
|     var button = SizedBox(
 | |
|       width: widget.width ?? _ToolbarTheme.buttonSize,
 | |
|       height: _ToolbarTheme.buttonSize,
 | |
|       child: MenuItemButton(
 | |
|           style: ButtonStyle(
 | |
|               backgroundColor: MaterialStatePropertyAll(Colors.transparent),
 | |
|               padding: MaterialStatePropertyAll(EdgeInsets.zero),
 | |
|               overlayColor: MaterialStatePropertyAll(Colors.transparent)),
 | |
|           onHover: (value) => setState(() {
 | |
|                 hover = value;
 | |
|               }),
 | |
|           onPressed: widget.onPressed,
 | |
|           child: Tooltip(
 | |
|             message: translate(widget.tooltip),
 | |
|             child: Material(
 | |
|                 type: MaterialType.transparency,
 | |
|                 child: Ink(
 | |
|                     decoration: BoxDecoration(
 | |
|                       borderRadius:
 | |
|                           BorderRadius.circular(_ToolbarTheme.iconRadius),
 | |
|                       color: hover ? widget.hoverColor : widget.color,
 | |
|                     ),
 | |
|                     child: icon)),
 | |
|           )),
 | |
|     ).marginSymmetric(
 | |
|         horizontal: widget.hMargin ?? _ToolbarTheme.buttonHMargin,
 | |
|         vertical: widget.vMargin ?? _ToolbarTheme.buttonVMargin);
 | |
|     button = Tooltip(
 | |
|       message: widget.tooltip,
 | |
|       child: button,
 | |
|     );
 | |
|     if (widget.topLevel) {
 | |
|       return MenuBar(children: [button]);
 | |
|     } else {
 | |
|       return button;
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _IconSubmenuButton extends StatefulWidget {
 | |
|   final String tooltip;
 | |
|   final String? svg;
 | |
|   final Widget? icon;
 | |
|   final Color color;
 | |
|   final Color hoverColor;
 | |
|   final List<Widget> Function() menuChildrenGetter;
 | |
|   final MenuStyle? menuStyle;
 | |
|   final FFI? ffi;
 | |
|   final double? width;
 | |
| 
 | |
|   _IconSubmenuButton({
 | |
|     Key? key,
 | |
|     this.svg,
 | |
|     this.icon,
 | |
|     required this.tooltip,
 | |
|     required this.color,
 | |
|     required this.hoverColor,
 | |
|     required this.menuChildrenGetter,
 | |
|     this.ffi,
 | |
|     this.menuStyle,
 | |
|     this.width,
 | |
|   }) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   State<_IconSubmenuButton> createState() => _IconSubmenuButtonState();
 | |
| }
 | |
| 
 | |
| class _IconSubmenuButtonState extends State<_IconSubmenuButton> {
 | |
|   bool hover = false;
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     assert(widget.svg != null || widget.icon != null);
 | |
|     final icon = widget.icon ??
 | |
|         SvgPicture.asset(
 | |
|           widget.svg!,
 | |
|           colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn),
 | |
|           width: _ToolbarTheme.buttonSize,
 | |
|           height: _ToolbarTheme.buttonSize,
 | |
|         );
 | |
|     final button = SizedBox(
 | |
|         width: widget.width ?? _ToolbarTheme.buttonSize,
 | |
|         height: _ToolbarTheme.buttonSize,
 | |
|         child: SubmenuButton(
 | |
|             menuStyle:
 | |
|                 widget.menuStyle ?? _ToolbarTheme.defaultMenuStyle(context),
 | |
|             style: _ToolbarTheme.defaultMenuButtonStyle,
 | |
|             onHover: (value) => setState(() {
 | |
|                   hover = value;
 | |
|                 }),
 | |
|             child: Tooltip(
 | |
|                 message: translate(widget.tooltip),
 | |
|                 child: Material(
 | |
|                     type: MaterialType.transparency,
 | |
|                     child: Ink(
 | |
|                         decoration: BoxDecoration(
 | |
|                           borderRadius:
 | |
|                               BorderRadius.circular(_ToolbarTheme.iconRadius),
 | |
|                           color: hover ? widget.hoverColor : widget.color,
 | |
|                         ),
 | |
|                         child: icon))),
 | |
|             menuChildren: widget
 | |
|                 .menuChildrenGetter()
 | |
|                 .map((e) => _buildPointerTrackWidget(e, widget.ffi))
 | |
|                 .toList()));
 | |
|     return MenuBar(children: [
 | |
|       button.marginSymmetric(
 | |
|           horizontal: _ToolbarTheme.buttonHMargin,
 | |
|           vertical: _ToolbarTheme.buttonVMargin)
 | |
|     ]);
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _SubmenuButton extends StatelessWidget {
 | |
|   final List<Widget> menuChildren;
 | |
|   final Widget? child;
 | |
|   final FFI ffi;
 | |
|   const _SubmenuButton({
 | |
|     Key? key,
 | |
|     required this.menuChildren,
 | |
|     required this.child,
 | |
|     required this.ffi,
 | |
|   }) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return SubmenuButton(
 | |
|       key: key,
 | |
|       child: child,
 | |
|       menuChildren:
 | |
|           menuChildren.map((e) => _buildPointerTrackWidget(e, ffi)).toList(),
 | |
|       menuStyle: _ToolbarTheme.defaultMenuStyle(context),
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class MenuButton extends StatelessWidget {
 | |
|   final VoidCallback? onPressed;
 | |
|   final Widget? trailingIcon;
 | |
|   final Widget? child;
 | |
|   final FFI? ffi;
 | |
|   MenuButton(
 | |
|       {Key? key,
 | |
|       this.onPressed,
 | |
|       this.trailingIcon,
 | |
|       required this.child,
 | |
|       this.ffi})
 | |
|       : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return MenuItemButton(
 | |
|         key: key,
 | |
|         onPressed: onPressed != null
 | |
|             ? () {
 | |
|                 if (ffi != null) {
 | |
|                   _menuDismissCallback(ffi!);
 | |
|                 }
 | |
|                 onPressed?.call();
 | |
|               }
 | |
|             : null,
 | |
|         trailingIcon: trailingIcon,
 | |
|         child: child);
 | |
|   }
 | |
| }
 | |
| 
 | |
| class CkbMenuButton extends StatelessWidget {
 | |
|   final bool? value;
 | |
|   final ValueChanged<bool?>? onChanged;
 | |
|   final Widget? child;
 | |
|   final FFI? ffi;
 | |
|   const CkbMenuButton(
 | |
|       {Key? key,
 | |
|       required this.value,
 | |
|       required this.onChanged,
 | |
|       required this.child,
 | |
|       this.ffi})
 | |
|       : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return CheckboxMenuButton(
 | |
|       key: key,
 | |
|       value: value,
 | |
|       child: child,
 | |
|       onChanged: onChanged != null
 | |
|           ? (bool? value) {
 | |
|               if (ffi != null) {
 | |
|                 _menuDismissCallback(ffi!);
 | |
|               }
 | |
|               onChanged?.call(value);
 | |
|             }
 | |
|           : null,
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class RdoMenuButton<T> extends StatelessWidget {
 | |
|   final T value;
 | |
|   final T? groupValue;
 | |
|   final ValueChanged<T?>? onChanged;
 | |
|   final Widget? child;
 | |
|   final FFI? ffi;
 | |
|   const RdoMenuButton({
 | |
|     Key? key,
 | |
|     required this.value,
 | |
|     required this.groupValue,
 | |
|     required this.child,
 | |
|     this.ffi,
 | |
|     this.onChanged,
 | |
|   }) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return RadioMenuButton(
 | |
|       value: value,
 | |
|       groupValue: groupValue,
 | |
|       child: child,
 | |
|       onChanged: onChanged != null
 | |
|           ? (T? value) {
 | |
|               if (ffi != null) {
 | |
|                 _menuDismissCallback(ffi!);
 | |
|               }
 | |
|               onChanged?.call(value);
 | |
|             }
 | |
|           : null,
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _DraggableShowHide extends StatefulWidget {
 | |
|   final String id;
 | |
|   final SessionID sessionId;
 | |
|   final RxDouble fractionX;
 | |
|   final RxBool dragging;
 | |
|   final ToolbarState toolbarState;
 | |
|   final BorderRadius borderRadius;
 | |
| 
 | |
|   final Function(bool) setFullscreen;
 | |
|   final Function() setMinimize;
 | |
| 
 | |
|   const _DraggableShowHide({
 | |
|     Key? key,
 | |
|     required this.id,
 | |
|     required this.sessionId,
 | |
|     required this.fractionX,
 | |
|     required this.dragging,
 | |
|     required this.toolbarState,
 | |
|     required this.setFullscreen,
 | |
|     required this.setMinimize,
 | |
|     required this.borderRadius,
 | |
|   }) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   State<_DraggableShowHide> createState() => _DraggableShowHideState();
 | |
| }
 | |
| 
 | |
| class _DraggableShowHideState extends State<_DraggableShowHide> {
 | |
|   Offset position = Offset.zero;
 | |
|   Size size = Size.zero;
 | |
|   double left = 0.0;
 | |
|   double right = 1.0;
 | |
| 
 | |
|   RxBool get show => widget.toolbarState.show;
 | |
| 
 | |
|   @override
 | |
|   initState() {
 | |
|     super.initState();
 | |
| 
 | |
|     final confLeft = double.tryParse(
 | |
|         bind.mainGetLocalOption(key: kOptionRemoteMenubarDragLeft));
 | |
|     if (confLeft == null) {
 | |
|       bind.mainSetLocalOption(
 | |
|           key: kOptionRemoteMenubarDragLeft, value: left.toString());
 | |
|     } else {
 | |
|       left = confLeft;
 | |
|     }
 | |
|     final confRight = double.tryParse(
 | |
|         bind.mainGetLocalOption(key: kOptionRemoteMenubarDragRight));
 | |
|     if (confRight == null) {
 | |
|       bind.mainSetLocalOption(
 | |
|           key: kOptionRemoteMenubarDragRight, value: right.toString());
 | |
|     } else {
 | |
|       right = confRight;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   Widget _buildDraggable(BuildContext context) {
 | |
|     return Draggable(
 | |
|       axis: Axis.horizontal,
 | |
|       child: Icon(
 | |
|         Icons.drag_indicator,
 | |
|         size: 20,
 | |
|         color: MyTheme.color(context).drag_indicator,
 | |
|       ),
 | |
|       feedback: widget,
 | |
|       onDragStarted: (() {
 | |
|         final RenderObject? renderObj = context.findRenderObject();
 | |
|         if (renderObj != null) {
 | |
|           final RenderBox renderBox = renderObj as RenderBox;
 | |
|           size = renderBox.size;
 | |
|           position = renderBox.localToGlobal(Offset.zero);
 | |
|         }
 | |
|         widget.dragging.value = true;
 | |
|       }),
 | |
|       onDragEnd: (details) {
 | |
|         final mediaSize = MediaQueryData.fromView(View.of(context)).size;
 | |
|         widget.fractionX.value +=
 | |
|             (details.offset.dx - position.dx) / (mediaSize.width - size.width);
 | |
|         if (widget.fractionX.value < left) {
 | |
|           widget.fractionX.value = left;
 | |
|         }
 | |
|         if (widget.fractionX.value > right) {
 | |
|           widget.fractionX.value = right;
 | |
|         }
 | |
|         bind.sessionPeerOption(
 | |
|           sessionId: widget.sessionId,
 | |
|           name: 'remote-menubar-drag-x',
 | |
|           value: widget.fractionX.value.toString(),
 | |
|         );
 | |
|         widget.dragging.value = false;
 | |
|       },
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     final ButtonStyle buttonStyle = ButtonStyle(
 | |
|       minimumSize: MaterialStateProperty.all(const Size(0, 0)),
 | |
|       padding: MaterialStateProperty.all(EdgeInsets.zero),
 | |
|     );
 | |
|     final isFullscreen = stateGlobal.fullscreen;
 | |
|     const double iconSize = 20;
 | |
| 
 | |
|     buttonWrapper(VoidCallback? onPressed, Widget child,
 | |
|         {Color hoverColor = _ToolbarTheme.blueColor}) {
 | |
|       final bgColor = buttonStyle.backgroundColor?.resolve({});
 | |
|       return TextButton(
 | |
|         onPressed: onPressed,
 | |
|         child: child,
 | |
|         style: buttonStyle.copyWith(
 | |
|           backgroundColor: MaterialStateProperty.resolveWith((states) {
 | |
|             if (states.contains(MaterialState.hovered)) {
 | |
|               return (bgColor ?? hoverColor).withOpacity(0.15);
 | |
|             }
 | |
|             return bgColor;
 | |
|           }),
 | |
|         ),
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     final child = Row(
 | |
|       mainAxisSize: MainAxisSize.min,
 | |
|       children: [
 | |
|         _buildDraggable(context),
 | |
|         Obx(() => buttonWrapper(
 | |
|               () {
 | |
|                 widget.setFullscreen(!isFullscreen.value);
 | |
|               },
 | |
|               Tooltip(
 | |
|                 message: translate(
 | |
|                     isFullscreen.isTrue ? 'Exit Fullscreen' : 'Fullscreen'),
 | |
|                 child: Icon(
 | |
|                   isFullscreen.isTrue
 | |
|                       ? Icons.fullscreen_exit
 | |
|                       : Icons.fullscreen,
 | |
|                   size: iconSize,
 | |
|                 ),
 | |
|               ),
 | |
|             )),
 | |
|         if (!isMacOS && !isWebDesktop)
 | |
|           Obx(() => Offstage(
 | |
|                 offstage: isFullscreen.isFalse,
 | |
|                 child: buttonWrapper(
 | |
|                   widget.setMinimize,
 | |
|                   Tooltip(
 | |
|                     message: translate('Minimize'),
 | |
|                     child: Icon(
 | |
|                       Icons.remove,
 | |
|                       size: iconSize,
 | |
|                     ),
 | |
|                   ),
 | |
|                 ),
 | |
|               )),
 | |
|         buttonWrapper(
 | |
|           () => setState(() {
 | |
|             widget.toolbarState.switchShow(widget.sessionId);
 | |
|           }),
 | |
|           Obx((() => Tooltip(
 | |
|                 message:
 | |
|                     translate(show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
 | |
|                 child: Icon(
 | |
|                   show.isTrue ? Icons.expand_less : Icons.expand_more,
 | |
|                   size: iconSize,
 | |
|                 ),
 | |
|               ))),
 | |
|         ),
 | |
|         if (isWebDesktop)
 | |
|           Obx(() {
 | |
|             if (show.isTrue) {
 | |
|               return Offstage();
 | |
|             } else {
 | |
|               return buttonWrapper(
 | |
|                 () => closeConnection(id: widget.id),
 | |
|                 Tooltip(
 | |
|                   message: translate('Close'),
 | |
|                   child: Icon(
 | |
|                     Icons.close,
 | |
|                     size: iconSize,
 | |
|                     color: _ToolbarTheme.redColor,
 | |
|                   ),
 | |
|                 ),
 | |
|                 hoverColor: _ToolbarTheme.redColor,
 | |
|               ).paddingOnly(left: iconSize / 2);
 | |
|             }
 | |
|           })
 | |
|       ],
 | |
|     );
 | |
|     return TextButtonTheme(
 | |
|       data: TextButtonThemeData(style: buttonStyle),
 | |
|       child: Container(
 | |
|         decoration: BoxDecoration(
 | |
|           color: Theme.of(context)
 | |
|               .menuBarTheme
 | |
|               .style
 | |
|               ?.backgroundColor
 | |
|               ?.resolve(MaterialState.values.toSet()),
 | |
|           border: Border.all(
 | |
|             color: _ToolbarTheme.borderColor(context),
 | |
|             width: 1,
 | |
|           ),
 | |
|           borderRadius: widget.borderRadius,
 | |
|         ),
 | |
|         child: SizedBox(
 | |
|           height: 20,
 | |
|           child: child,
 | |
|         ),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class InputModeMenu {
 | |
|   final String key;
 | |
|   final String menu;
 | |
| 
 | |
|   InputModeMenu({required this.key, required this.menu});
 | |
| }
 | |
| 
 | |
| _menuDismissCallback(FFI ffi) => ffi.inputModel.refreshMousePos();
 | |
| 
 | |
| Widget _buildPointerTrackWidget(Widget child, FFI? ffi) {
 | |
|   return Listener(
 | |
|     onPointerHover: (PointerHoverEvent e) => {
 | |
|       if (ffi != null) {ffi.inputModel.lastMousePos = e.position}
 | |
|     },
 | |
|     child: MouseRegion(
 | |
|       child: child,
 | |
|     ),
 | |
|   );
 | |
| }
 |