2022-09-16 14:43:28 +03:00
import ' dart:convert ' ;
2022-09-08 18:25:19 +03:00
import ' dart:io ' ;
2022-10-09 14:27:30 +03:00
import ' dart:ui ' as ui ;
2022-09-08 18:25:19 +03:00
2022-08-26 18:28:08 +03:00
import ' package:flutter/material.dart ' ;
import ' package:flutter/services.dart ' ;
import ' package:flutter_hbb/models/chat_model.dart ' ;
2022-11-01 12:01:43 +03:00
import ' package:flutter_hbb/models/state_model.dart ' ;
2022-11-24 06:19:16 +03:00
import ' package:flutter_hbb/consts.dart ' ;
2022-12-01 08:52:12 +03:00
import ' package:flutter_hbb/utils/multi_window_manager.dart ' ;
2023-01-31 17:49:17 +03:00
import ' package:flutter_svg/flutter_svg.dart ' ;
2022-08-26 18:28:08 +03:00
import ' package:get/get.dart ' ;
2022-09-15 12:31:28 +03:00
import ' package:provider/provider.dart ' ;
2022-12-07 10:13:24 +03:00
import ' package:debounce_throttle/debounce_throttle.dart ' ;
2022-10-08 12:27:30 +03:00
import ' package:desktop_multi_window/desktop_multi_window.dart ' ;
2022-10-09 14:27:30 +03:00
import ' package:window_size/window_size.dart ' as window_size ;
2022-08-26 18:28:08 +03:00
import ' ../../common.dart ' ;
2023-03-24 10:21:14 +03:00
import ' ../../common/widgets/dialog.dart ' ;
2022-08-26 18:28:08 +03:00
import ' ../../models/model.dart ' ;
import ' ../../models/platform_model.dart ' ;
2022-08-29 13:48:12 +03:00
import ' ../../common/shared_state.dart ' ;
2022-08-26 18:28:08 +03:00
import ' ./popup_menu.dart ' ;
2022-12-27 11:45:13 +03:00
import ' ./kb_layout_type_chooser.dart ' ;
2022-08-26 18:28:08 +03:00
2023-02-25 17:59:59 +03:00
const _kKeyLegacyMode = ' legacy ' ;
const _kKeyMapMode = ' map ' ;
const _kKeyTranslateMode = ' translate ' ;
2022-11-10 09:32:22 +03:00
class MenubarState {
2022-11-17 13:52:27 +03:00
final kStoreKey = ' remoteMenubarState ' ;
2022-11-10 09:32:22 +03:00
late RxBool show ;
late RxBool _pin ;
2022-11-24 06:19:16 +03:00
RxString viewStyle = RxString ( kRemoteViewStyleOriginal ) ;
2022-11-10 09:32:22 +03:00
MenubarState ( ) {
2022-11-10 16:25:12 +03:00
final s = bind . getLocalFlutterConfig ( k: kStoreKey ) ;
if ( s . isEmpty ) {
2022-11-10 09:32:22 +03:00
_initSet ( false , false ) ;
2022-11-10 16:25:12 +03:00
return ;
2022-11-10 09:32:22 +03:00
}
2022-11-10 16:25:12 +03:00
2022-11-10 09:32:22 +03:00
try {
2022-11-10 16:25:12 +03:00
final m = jsonDecode ( s ) ;
2022-11-10 09:32:22 +03:00
if ( m = = null ) {
_initSet ( false , false ) ;
} else {
_initSet ( m [ ' pin ' ] ? ? false , m [ ' pin ' ] ? ? false ) ;
}
} catch ( e ) {
debugPrint ( ' Failed to decode menubar state ${ e . toString ( ) } ' ) ;
_initSet ( false , false ) ;
}
}
_initSet ( bool s , bool p ) {
2022-11-16 13:07:58 +03:00
// Show remubar when connection is established.
show = RxBool ( true ) ;
2022-11-10 09:32:22 +03:00
_pin = RxBool ( p ) ;
}
bool get pin = > _pin . value ;
switchShow ( ) async {
show . value = ! show . value ;
}
setShow ( bool v ) async {
if ( show . value ! = v ) {
show . value = v ;
}
}
switchPin ( ) async {
_pin . value = ! _pin . value ;
// Save everytime changed, as this func will not be called frequently
2022-11-24 06:19:16 +03:00
await _savePin ( ) ;
2022-11-10 09:32:22 +03:00
}
setPin ( bool v ) async {
if ( _pin . value ! = v ) {
_pin . value = v ;
// Save everytime changed, as this func will not be called frequently
2022-11-24 06:19:16 +03:00
await _savePin ( ) ;
2022-11-10 09:32:22 +03:00
}
}
2022-11-24 06:19:16 +03:00
_savePin ( ) async {
2022-11-10 16:25:12 +03:00
bind . setLocalFlutterConfig (
k: kStoreKey , v: jsonEncode ( { ' pin ' : _pin . value } ) ) ;
2022-11-10 09:32:22 +03:00
}
2022-11-24 06:19:16 +03:00
save ( ) async {
await _savePin ( ) ;
}
2022-11-10 09:32:22 +03:00
}
2022-08-26 18:28:08 +03:00
class _MenubarTheme {
2023-02-15 13:40:17 +03:00
static const Color blueColor = MyTheme . button ;
static const Color hoverBlueColor = MyTheme . accent ;
static const Color redColor = Colors . redAccent ;
static const Color hoverRedColor = Colors . red ;
2022-08-29 13:48:12 +03:00
// kMinInteractiveDimension
2022-09-23 07:20:40 +03:00
static const double height = 20.0 ;
2022-08-29 13:48:12 +03:00
static const double dividerHeight = 12.0 ;
2023-02-23 09:30:29 +03:00
static const double buttonSize = 32 ;
static const double buttonHMargin = 3 ;
static const double buttonVMargin = 6 ;
static const double iconRadius = 8 ;
2023-03-12 05:48:54 +03:00
static const double elevation = 3 ;
2022-08-26 18:28:08 +03:00
}
2023-02-06 06:27:20 +03:00
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 ( id: remoteId ) ? ? ' ' ;
if ( rxViewStyle ! = null ) {
rxViewStyle . value = viewStyle ;
}
return viewStyle ;
} ,
optionSetter: ( String oldValue , String newValue ) async {
await bind . sessionSetViewStyle ( id: remoteId , 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 ,
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 ( id: remoteId , value: optKey ) ;
state . value =
bind . sessionGetToggleOptionSync ( id: remoteId , arg: optKey ) ;
if ( dismissFunc ! = null ) {
dismissFunc ( ) ;
}
} ,
padding: padding ,
dismissOnClicked: true ,
dismissCallback: dismissCallback ,
) ;
}
static MenuEntrySwitch < String > disableClipboard (
String remoteId ,
EdgeInsets ? padding , {
DismissFunc ? dismissFunc ,
DismissCallback ? dismissCallback ,
} ) {
return createSwitchMenuEntry (
remoteId ,
' Disable clipboard ' ,
' disable-clipboard ' ,
padding ,
true ,
dismissCallback: dismissCallback ,
) ;
}
static MenuEntrySwitch < String > createSwitchMenuEntry (
String remoteId ,
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 ( id: remoteId , arg: option ) ;
} ,
setter: ( bool v ) async {
await bind . sessionToggleOption ( id: remoteId , value: option ) ;
if ( dismissFunc ! = null ) {
dismissFunc ( ) ;
}
} ,
padding: padding ,
dismissOnClicked: dismissOnClicked ,
dismissCallback: dismissCallback ,
) ;
}
static MenuEntryButton < String > insertLock (
String remoteId ,
EdgeInsets ? padding , {
DismissFunc ? dismissFunc ,
DismissCallback ? dismissCallback ,
} ) {
return MenuEntryButton < String > (
childBuilder: ( TextStyle ? style ) = > Text (
translate ( ' Insert Lock ' ) ,
style: style ,
) ,
proc: ( ) {
bind . sessionLockScreen ( id: remoteId ) ;
if ( dismissFunc ! = null ) {
dismissFunc ( ) ;
}
} ,
padding: padding ,
dismissOnClicked: true ,
dismissCallback: dismissCallback ,
) ;
}
static insertCtrlAltDel (
String remoteId ,
EdgeInsets ? padding , {
DismissFunc ? dismissFunc ,
DismissCallback ? dismissCallback ,
} ) {
return MenuEntryButton < String > (
childBuilder: ( TextStyle ? style ) = > Text (
' ${ translate ( " Insert " ) } Ctrl + Alt + Del ' ,
style: style ,
) ,
proc: ( ) {
bind . sessionCtrlAltDel ( id: remoteId ) ;
if ( dismissFunc ! = null ) {
dismissFunc ( ) ;
}
} ,
padding: padding ,
dismissOnClicked: true ,
dismissCallback: dismissCallback ,
) ;
}
}
2022-08-26 18:28:08 +03:00
class RemoteMenubar extends StatefulWidget {
final String id ;
final FFI ffi ;
2022-11-10 09:32:22 +03:00
final MenubarState state ;
2022-09-14 05:10:55 +03:00
final Function ( Function ( bool ) ) onEnterOrLeaveImageSetter ;
final Function ( ) onEnterOrLeaveImageCleaner ;
2022-08-26 18:28:08 +03:00
2023-02-23 09:30:29 +03:00
RemoteMenubar ( {
2022-08-26 18:28:08 +03:00
Key ? key ,
required this . id ,
required this . ffi ,
2022-11-10 09:32:22 +03:00
required this . state ,
2022-09-14 05:10:55 +03:00
required this . onEnterOrLeaveImageSetter ,
required this . onEnterOrLeaveImageCleaner ,
2022-08-26 18:28:08 +03:00
} ) : super ( key: key ) ;
@ override
State < RemoteMenubar > createState ( ) = > _RemoteMenubarState ( ) ;
}
class _RemoteMenubarState extends State < RemoteMenubar > {
2022-12-07 10:13:24 +03:00
late Debouncer < int > _debouncerHide ;
2022-09-13 16:59:06 +03:00
bool _isCursorOverImage = false ;
2022-12-07 10:13:24 +03:00
final _fractionX = 0.5 . obs ;
final _dragging = false . obs ;
2022-08-26 18:28:08 +03:00
2022-11-01 12:01:43 +03:00
int get windowId = > stateGlobal . windowId ;
bool get isFullscreen = > stateGlobal . fullscreen ;
2022-09-13 16:59:06 +03:00
void _setFullscreen ( bool v ) {
2022-11-01 12:01:43 +03:00
stateGlobal . setFullscreen ( v ) ;
setState ( ( ) { } ) ;
2022-08-26 18:28:08 +03:00
}
2022-11-10 09:32:22 +03:00
RxBool get show = > widget . state . show ;
bool get pin = > widget . state . pin ;
2022-11-03 16:58:25 +03:00
2023-03-28 05:36:59 +03:00
PeerInfo get pi = > widget . ffi . ffiModel . pi ;
FfiModel get ffiModel = > widget . ffi . ffiModel ;
2023-04-01 10:51:42 +03:00
triggerAutoHide ( ) = > _debouncerHide . value = _debouncerHide . value + 1 ;
2022-09-13 16:59:06 +03:00
@ override
2022-09-14 05:10:55 +03:00
initState ( ) {
2022-09-13 16:59:06 +03:00
super . initState ( ) ;
2022-12-07 10:13:24 +03:00
_debouncerHide = Debouncer < int > (
Duration ( milliseconds: 5000 ) ,
onChanged: _debouncerHideProc ,
initialValue: 0 ,
) ;
2022-09-14 05:10:55 +03:00
widget . onEnterOrLeaveImageSetter ( ( enter ) {
2022-09-13 16:59:06 +03:00
if ( enter ) {
2023-04-01 10:51:42 +03:00
triggerAutoHide ( ) ;
2022-09-13 16:59:06 +03:00
_isCursorOverImage = true ;
} else {
_isCursorOverImage = false ;
}
} ) ;
2022-12-07 10:13:24 +03:00
}
2022-09-13 16:59:06 +03:00
2022-12-07 10:13:24 +03:00
_debouncerHideProc ( int v ) {
if ( ! pin & & show . isTrue & & _isCursorOverImage & & _dragging . isFalse ) {
show . value = false ;
}
2022-09-13 16:59:06 +03:00
}
2022-09-14 05:10:55 +03:00
@ override
dispose ( ) {
super . dispose ( ) ;
widget . onEnterOrLeaveImageCleaner ( ) ;
}
2022-08-26 18:28:08 +03:00
@ override
Widget build ( BuildContext context ) {
2022-12-23 18:10:34 +03:00
// No need to use future builder here.
2022-08-26 18:28:08 +03:00
return Align (
alignment: Alignment . topCenter ,
2022-12-07 10:13:24 +03:00
child: Obx ( ( ) = > show . value
2023-03-15 20:31:53 +03:00
? _buildToolbar ( context )
2022-12-07 10:13:24 +03:00
: _buildDraggableShowHide ( context ) ) ,
2022-08-26 18:28:08 +03:00
) ;
}
2022-12-07 10:13:24 +03:00
Widget _buildDraggableShowHide ( BuildContext context ) {
return Obx ( ( ) {
if ( show . isTrue & & _dragging . isFalse ) {
2023-04-01 10:51:42 +03:00
triggerAutoHide ( ) ;
2022-12-07 10:13:24 +03:00
}
return Align (
alignment: FractionalOffset ( _fractionX . value , 0 ) ,
child: Offstage (
offstage: _dragging . isTrue ,
2023-03-12 05:48:54 +03:00
child: Material (
elevation: _MenubarTheme . elevation ,
2023-03-15 11:57:24 +03:00
shadowColor: MyTheme . color ( context ) . shadow ,
2023-03-12 05:48:54 +03:00
child: _DraggableShowHide (
dragging: _dragging ,
fractionX: _fractionX ,
show: show ,
) ,
2022-12-07 10:13:24 +03:00
) ,
) ,
) ;
} ) ;
2022-08-26 18:28:08 +03:00
}
2023-03-15 20:31:53 +03:00
Widget _buildToolbar ( BuildContext context ) {
final List < Widget > toolbarItems = [ ] ;
2022-08-26 18:28:08 +03:00
if ( ! isWebDesktop ) {
2023-03-15 20:31:53 +03:00
toolbarItems . add ( _PinMenu ( state: widget . state ) ) ;
toolbarItems . add (
2023-02-23 09:30:29 +03:00
_FullscreenMenu ( state: widget . state , setFullscreen: _setFullscreen ) ) ;
2023-03-15 20:31:53 +03:00
toolbarItems . add ( _MobileActionMenu ( ffi: widget . ffi ) ) ;
2022-08-26 18:28:08 +03:00
}
2023-03-15 19:57:52 +03:00
2023-03-28 05:36:59 +03:00
if ( PrivacyModeState . find ( widget . id ) . isFalse & & pi . displays . length > 1 ) {
2023-03-15 20:31:53 +03:00
toolbarItems . add (
2023-03-17 06:27:22 +03:00
bind . mainGetUserDefaultOption ( key: ' show_monitors_toolbar ' ) = = ' Y '
2023-03-15 19:57:52 +03:00
? _MultiMonitorMenu ( id: widget . id , ffi: widget . ffi )
: _MonitorMenu ( id: widget . id , ffi: widget . ffi ) ,
) ;
}
2023-03-15 20:31:53 +03:00
toolbarItems
2023-02-23 09:30:29 +03:00
. add ( _ControlMenu ( id: widget . id , ffi: widget . ffi , state: widget . state ) ) ;
2023-03-15 20:31:53 +03:00
toolbarItems . add ( _DisplayMenu (
2023-02-23 09:30:29 +03:00
id: widget . id ,
ffi: widget . ffi ,
state: widget . state ,
setFullscreen: _setFullscreen ,
) ) ;
2023-03-17 06:27:22 +03:00
toolbarItems . add ( _KeyboardMenu ( id: widget . id , ffi: widget . ffi ) ) ;
2022-08-26 18:28:08 +03:00
if ( ! isWeb ) {
2023-03-15 20:31:53 +03:00
toolbarItems . add ( _ChatMenu ( id: widget . id , ffi: widget . ffi ) ) ;
toolbarItems . add ( _VoiceCallMenu ( id: widget . id , ffi: widget . ffi ) ) ;
2022-08-26 18:28:08 +03:00
}
2023-03-15 20:31:53 +03:00
toolbarItems . add ( _RecordMenu ( ) ) ;
toolbarItems . add ( _CloseMenu ( id: widget . id , ffi: widget . ffi ) ) ;
2023-02-23 09:30:29 +03:00
return Column (
mainAxisSize: MainAxisSize . min ,
children: [
2023-03-12 05:48:54 +03:00
Material (
elevation: _MenubarTheme . elevation ,
2023-03-15 11:57:24 +03:00
shadowColor: MyTheme . color ( context ) . shadow ,
2023-03-12 05:48:54 +03:00
borderRadius: BorderRadius . all ( Radius . circular ( 4.0 ) ) ,
color: Theme . of ( context )
. menuBarTheme
. style
? . backgroundColor
? . resolve ( MaterialState . values . toSet ( ) ) ,
2023-02-23 09:30:29 +03:00
child: SingleChildScrollView (
2023-03-01 18:35:51 +03:00
scrollDirection: Axis . horizontal ,
child: Theme (
data: themeData ( ) ,
2023-03-10 08:54:23 +03:00
child: Row (
2023-03-01 18:35:51 +03:00
children: [
2023-03-11 14:29:26 +03:00
SizedBox ( width: _MenubarTheme . buttonHMargin * 2 ) ,
2023-03-15 20:31:53 +03:00
. . . toolbarItems ,
2023-03-11 14:29:26 +03:00
SizedBox ( width: _MenubarTheme . buttonHMargin * 2 )
2023-03-01 18:35:51 +03:00
] ,
) ,
) ,
) ,
2023-02-23 09:30:29 +03:00
) ,
_buildDraggableShowHide ( context ) ,
] ,
2023-02-14 15:57:33 +03:00
) ;
2022-08-26 18:28:08 +03:00
}
2023-02-23 16:31:00 +03:00
ThemeData themeData ( ) {
return Theme . of ( context ) . copyWith (
menuButtonTheme: MenuButtonThemeData (
2023-03-01 18:35:51 +03:00
style: ButtonStyle (
minimumSize: MaterialStatePropertyAll ( Size ( 64 , 36 ) ) ,
textStyle: MaterialStatePropertyAll (
TextStyle ( fontWeight: FontWeight . normal ) ,
) ,
) ,
) ,
2023-02-23 16:31:00 +03:00
dividerTheme: DividerThemeData ( space: 4 ) ,
2023-03-10 08:54:23 +03:00
menuBarTheme: MenuBarThemeData (
2023-03-11 14:29:26 +03:00
style: MenuStyle (
padding: MaterialStatePropertyAll ( EdgeInsets . zero ) ,
elevation: MaterialStatePropertyAll ( 0 ) ,
shape: MaterialStatePropertyAll ( BeveledRectangleBorder ( ) ) ,
) . copyWith (
backgroundColor:
Theme . of ( context ) . menuBarTheme . style ? . backgroundColor ) ) ,
2023-02-23 16:31:00 +03:00
) ;
}
2023-02-23 09:30:29 +03:00
}
class _PinMenu extends StatelessWidget {
final MenubarState state ;
const _PinMenu ( { Key ? key , required this . state } ) : super ( key: key ) ;
2022-08-26 18:28:08 +03:00
2023-02-23 09:30:29 +03:00
@ override
Widget build ( BuildContext context ) {
2023-02-14 15:57:33 +03:00
return Obx (
2023-02-23 09:30:29 +03:00
( ) = > _IconMenuButton (
assetName: state . pin ? " assets/pinned.svg " : " assets/unpinned.svg " ,
tooltip: state . pin ? ' Unpin menubar ' : ' Pin menubar ' ,
onPressed: state . switchPin ,
color: state . pin ? _MenubarTheme . blueColor : Colors . grey [ 800 ] ! ,
hoverColor:
state . pin ? _MenubarTheme . hoverBlueColor : Colors . grey [ 850 ] ! ,
2023-02-14 15:57:33 +03:00
) ,
) ;
2022-09-13 16:59:06 +03:00
}
2023-02-23 09:30:29 +03:00
}
2022-09-13 16:59:06 +03:00
2023-02-23 09:30:29 +03:00
class _FullscreenMenu extends StatelessWidget {
final MenubarState state ;
final Function ( bool ) setFullscreen ;
bool get isFullscreen = > stateGlobal . fullscreen ;
const _FullscreenMenu (
{ Key ? key , required this . state , required this . setFullscreen } )
: super ( key: key ) ;
@ override
Widget build ( BuildContext context ) {
return _IconMenuButton (
assetName:
isFullscreen ? " assets/fullscreen_exit.svg " : " assets/fullscreen.svg " ,
tooltip: isFullscreen ? ' Exit Fullscreen ' : ' Fullscreen ' ,
onPressed: ( ) = > setFullscreen ( ! isFullscreen ) ,
2023-02-15 13:40:17 +03:00
color: _MenubarTheme . blueColor ,
hoverColor: _MenubarTheme . hoverBlueColor ,
2022-08-26 18:28:08 +03:00
) ;
}
2023-02-23 09:30:29 +03:00
}
2022-08-26 18:28:08 +03:00
2023-02-23 09:30:29 +03:00
class _MobileActionMenu extends StatelessWidget {
final FFI ffi ;
const _MobileActionMenu ( { Key ? key , required this . ffi } ) : super ( key: key ) ;
2022-08-26 18:28:08 +03:00
2023-02-23 09:30:29 +03:00
@ override
Widget build ( BuildContext context ) {
if ( ! ffi . ffiModel . isPeerAndroid ) return Offstage ( ) ;
return _IconMenuButton (
assetName: ' assets/actions_mobile.svg ' ,
tooltip: ' Mobile Actions ' ,
onPressed: ( ) = > ffi . dialogManager . toggleMobileActionsOverlay ( ffi: ffi ) ,
color: _MenubarTheme . blueColor ,
hoverColor: _MenubarTheme . hoverBlueColor ,
2022-08-26 18:28:08 +03:00
) ;
}
2023-02-23 09:30:29 +03:00
}
2022-08-26 18:28:08 +03:00
2023-02-23 09:30:29 +03:00
class _MonitorMenu extends StatelessWidget {
final String id ;
final FFI ffi ;
const _MonitorMenu ( { Key ? key , required this . id , required this . ffi } )
: super ( key: key ) ;
2022-08-26 18:28:08 +03:00
2023-02-23 09:30:29 +03:00
@ override
Widget build ( BuildContext context ) {
return _IconSubmenuButton (
2023-03-10 09:16:18 +03:00
tooltip: ' Select Monitor ' ,
2023-02-23 09:30:29 +03:00
icon: icon ( ) ,
ffi: ffi ,
color: _MenubarTheme . blueColor ,
hoverColor: _MenubarTheme . hoverBlueColor ,
menuStyle: MenuStyle (
padding:
MaterialStatePropertyAll ( EdgeInsets . symmetric ( horizontal: 6 ) ) ) ,
menuChildren: [ Row ( children: displays ( context ) ) ] ) ;
}
2023-02-21 13:43:43 +03:00
2023-02-23 09:30:29 +03:00
icon ( ) {
final pi = ffi . ffiModel . pi ;
return Stack (
alignment: Alignment . center ,
children: [
SvgPicture . asset (
2023-03-15 19:57:52 +03:00
" assets/screen.svg " ,
2023-02-23 09:30:29 +03:00
color: Colors . white ,
) ,
2023-03-15 19:57:52 +03:00
Obx ( ( ) {
RxInt display = CurrentDisplayState . find ( id ) ;
return Text (
' ${ display . value + 1 } / ${ pi . displays . length } ' ,
style: const TextStyle (
color: _MenubarTheme . blueColor ,
fontSize: 8 ,
fontWeight: FontWeight . bold ,
) ,
) ;
} ) ,
2023-02-23 09:30:29 +03:00
] ,
2022-09-05 17:18:29 +03:00
) ;
}
2023-02-23 09:30:29 +03:00
List < Widget > displays ( BuildContext context ) {
final List < Widget > rowChildren = [ ] ;
final pi = ffi . ffiModel . pi ;
for ( int i = 0 ; i < pi . displays . length ; i + + ) {
rowChildren . add ( _IconMenuButton (
2023-03-10 08:54:23 +03:00
topLevel: false ,
2023-02-23 09:30:29 +03:00
color: _MenubarTheme . blueColor ,
hoverColor: _MenubarTheme . hoverBlueColor ,
tooltip: " " ,
hMargin: 6 ,
vMargin: 12 ,
icon: Container (
alignment: AlignmentDirectional . center ,
constraints: const BoxConstraints ( minHeight: _MenubarTheme . height ) ,
child: Stack (
alignment: Alignment . center ,
children: [
SvgPicture . asset (
2023-03-15 19:57:52 +03:00
" assets/screen.svg " ,
2023-02-23 09:30:29 +03:00
color: Colors . white ,
) ,
2023-03-15 19:57:52 +03:00
Text (
( i + 1 ) . toString ( ) ,
style: TextStyle (
color: _MenubarTheme . blueColor ,
fontSize: 12 ,
fontWeight: FontWeight . bold ,
2023-02-23 09:30:29 +03:00
) ,
2023-03-15 19:57:52 +03:00
) ,
2023-02-23 09:30:29 +03:00
] ,
2023-02-14 15:57:33 +03:00
) ,
2023-02-23 09:30:29 +03:00
) ,
onPressed: ( ) {
_menuDismissCallback ( ffi ) ;
RxInt display = CurrentDisplayState . find ( id ) ;
if ( display . value ! = i ) {
bind . sessionSwitchDisplay ( id: id , value: i ) ;
}
} ,
) ) ;
}
return rowChildren ;
2022-09-15 12:31:28 +03:00
}
2023-02-23 09:30:29 +03:00
}
2022-09-15 12:31:28 +03:00
2023-02-23 09:30:29 +03:00
class _ControlMenu extends StatelessWidget {
final String id ;
final FFI ffi ;
final MenubarState state ;
_ControlMenu (
{ Key ? key , required this . id , required this . ffi , required this . state } )
: super ( key: key ) ;
@ override
Widget build ( BuildContext context ) {
return _IconSubmenuButton (
2023-03-10 09:16:18 +03:00
tooltip: ' Control Actions ' ,
2023-02-23 09:30:29 +03:00
svg: " assets/actions.svg " ,
color: _MenubarTheme . blueColor ,
hoverColor: _MenubarTheme . hoverBlueColor ,
ffi: ffi ,
menuChildren: [
2023-02-24 10:51:13 +03:00
requestElevation ( ) ,
2023-03-30 05:48:18 +03:00
ffi . ffiModel . pi . is_headless ? osAccount ( ) : osPassword ( ) ,
2023-02-23 09:30:29 +03:00
transferFile ( context ) ,
tcpTunneling ( context ) ,
note ( ) ,
Divider ( ) ,
ctrlAltDel ( ) ,
restart ( ) ,
2023-02-24 14:06:37 +03:00
insertLock ( ) ,
2023-02-23 09:30:29 +03:00
blockUserInput ( ) ,
switchSides ( ) ,
refresh ( ) ,
] ) ;
2022-08-26 18:28:08 +03:00
}
2023-02-24 10:51:13 +03:00
requestElevation ( ) {
final visible = ffi . elevationModel . showRequestMenu ;
if ( ! visible ) return Offstage ( ) ;
return _MenuItemButton (
child: Text ( translate ( ' Request Elevation ' ) ) ,
ffi: ffi ,
onPressed: ( ) = > showRequestElevationDialog ( id , ffi . dialogManager ) ) ;
}
2023-03-30 05:48:18 +03:00
osAccount ( ) {
return _MenuItemButton (
child: Text ( translate ( ' OS Account ' ) ) ,
trailingIcon: Transform . scale ( scale: 0.8 , child: Icon ( Icons . edit ) ) ,
ffi: ffi ,
2023-03-30 06:47:32 +03:00
onPressed: ( ) = > showSetOSAccount ( id , ffi . dialogManager ) ) ;
2023-03-30 05:48:18 +03:00
}
2023-02-23 09:30:29 +03:00
osPassword ( ) {
return _MenuItemButton (
child: Text ( translate ( ' OS Password ' ) ) ,
trailingIcon: Transform . scale ( scale: 0.8 , child: Icon ( Icons . edit ) ) ,
ffi: ffi ,
2023-03-30 06:47:32 +03:00
onPressed: ( ) = > showSetOSPassword ( id , false , ffi . dialogManager ) ) ;
2023-02-07 15:38:27 +03:00
}
2023-02-23 09:30:29 +03:00
transferFile ( BuildContext context ) {
return _MenuItemButton (
child: Text ( translate ( ' Transfer File ' ) ) ,
ffi: ffi ,
onPressed: ( ) = > connect ( context , id , isFileTransfer: true ) ) ;
2023-02-07 15:38:27 +03:00
}
2023-02-23 09:30:29 +03:00
tcpTunneling ( BuildContext context ) {
return _MenuItemButton (
child: Text ( translate ( ' TCP Tunneling ' ) ) ,
ffi: ffi ,
onPressed: ( ) = > connect ( context , id , isTcpTunneling: true ) ) ;
}
note ( ) {
final auditServer = bind . sessionGetAuditServerSync ( id: id , typ: " conn " ) ;
final visible = auditServer . isNotEmpty ;
if ( ! visible ) return Offstage ( ) ;
return _MenuItemButton (
child: Text ( translate ( ' Note ' ) ) ,
ffi: ffi ,
onPressed: ( ) = > _showAuditDialog ( id , ffi . dialogManager ) ,
2023-02-06 15:10:39 +03:00
) ;
}
2023-02-23 09:30:29 +03:00
_showAuditDialog ( String id , dialogManager ) async {
final controller = TextEditingController ( ) ;
dialogManager . show ( ( setState , close ) {
submit ( ) {
var text = controller . text . trim ( ) ;
if ( text ! = ' ' ) {
bind . sessionSendNote ( id: id , note: text ) ;
}
close ( ) ;
}
2023-02-08 16:29:51 +03:00
2023-02-23 09:30:29 +03:00
late final focusNode = FocusNode (
onKey: ( FocusNode node , RawKeyEvent evt ) {
if ( evt . logicalKey . keyLabel = = ' Enter ' ) {
if ( evt is RawKeyDownEvent ) {
int pos = controller . selection . base . offset ;
controller . text =
' ${ controller . text . substring ( 0 , pos ) } \n ${ controller . text . substring ( pos ) } ' ;
controller . selection =
TextSelection . fromPosition ( TextPosition ( offset: pos + 1 ) ) ;
}
return KeyEventResult . handled ;
}
if ( evt . logicalKey . keyLabel = = ' Esc ' ) {
if ( evt is RawKeyDownEvent ) {
close ( ) ;
}
return KeyEventResult . handled ;
} else {
return KeyEventResult . ignored ;
2023-02-08 16:29:51 +03:00
}
2023-02-06 15:10:39 +03:00
} ,
2023-02-23 09:30:29 +03:00
) ;
return CustomAlertDialog (
title: Text ( translate ( ' Note ' ) ) ,
content: SizedBox (
width: 250 ,
height: 120 ,
child: TextField (
autofocus: true ,
keyboardType: TextInputType . multiline ,
textInputAction: TextInputAction . newline ,
decoration: const InputDecoration . collapsed (
hintText: ' input note here ' ,
) ,
maxLines: null ,
maxLength: 256 ,
controller: controller ,
focusNode: focusNode ,
2022-09-23 07:20:40 +03:00
) ) ,
2023-02-23 09:30:29 +03:00
actions: [
dialogButton ( ' Cancel ' , onPressed: close , isOutline: true ) ,
dialogButton ( ' OK ' , onPressed: submit )
] ,
onSubmit: submit ,
onCancel: close ,
2022-09-08 18:25:19 +03:00
) ;
2023-02-23 09:30:29 +03:00
} ) ;
}
ctrlAltDel ( ) {
2023-03-16 04:37:35 +03:00
final viewOnly = ffi . ffiModel . viewOnly ;
2023-02-23 09:30:29 +03:00
final pi = ffi . ffiModel . pi ;
2023-03-16 04:37:35 +03:00
final visible = ! viewOnly & &
2023-03-28 05:36:59 +03:00
ffi . ffiModel . keyboard & &
2023-02-23 09:30:29 +03:00
( pi . platform = = kPeerPlatformLinux | | pi . sasEnabled ) ;
if ( ! visible ) return Offstage ( ) ;
return _MenuItemButton (
child: Text ( ' ${ translate ( " Insert " ) } Ctrl + Alt + Del ' ) ,
ffi: ffi ,
onPressed: ( ) = > bind . sessionCtrlAltDel ( id: id ) ) ;
}
restart ( ) {
final perms = ffi . ffiModel . permissions ;
final pi = ffi . ffiModel . pi ;
final visible = perms [ ' restart ' ] ! = false & &
2023-01-10 12:13:40 +03:00
( pi . platform = = kPeerPlatformLinux | |
pi . platform = = kPeerPlatformWindows | |
2023-02-23 09:30:29 +03:00
pi . platform = = kPeerPlatformMacOS ) ;
if ( ! visible ) return Offstage ( ) ;
return _MenuItemButton (
child: Text ( translate ( ' Restart Remote Device ' ) ) ,
ffi: ffi ,
onPressed: ( ) = > showRestartRemoteDevice ( pi , id , ffi . dialogManager ) ) ;
}
2022-08-26 18:28:08 +03:00
2023-02-24 14:06:37 +03:00
insertLock ( ) {
2023-03-16 04:37:35 +03:00
final viewOnly = ffi . ffiModel . viewOnly ;
2023-03-28 05:36:59 +03:00
final visible = ! viewOnly & & ffi . ffiModel . keyboard ;
2023-02-24 14:06:37 +03:00
if ( ! visible ) return Offstage ( ) ;
return _MenuItemButton (
child: Text ( translate ( ' Insert Lock ' ) ) ,
ffi: ffi ,
onPressed: ( ) = > bind . sessionLockScreen ( id: id ) ) ;
}
2023-02-23 09:30:29 +03:00
blockUserInput ( ) {
final pi = ffi . ffiModel . pi ;
final visible =
2023-03-28 05:36:59 +03:00
ffi . ffiModel . keyboard & & pi . platform = = kPeerPlatformWindows ;
2023-02-23 09:30:29 +03:00
if ( ! visible ) return Offstage ( ) ;
return _MenuItemButton (
child: Obx ( ( ) = > Text ( translate (
' ${ BlockInputState . find ( id ) . value ? ' Unb ' : ' B ' } lock user input ' ) ) ) ,
ffi: ffi ,
onPressed: ( ) {
RxBool blockInput = BlockInputState . find ( id ) ;
bind . sessionToggleOption (
id: id , value: ' ${ blockInput . value ? ' un ' : ' ' } block-input ' ) ;
blockInput . value = ! blockInput . value ;
} ) ;
}
2022-08-26 18:28:08 +03:00
2023-02-23 09:30:29 +03:00
switchSides ( ) {
final pi = ffi . ffiModel . pi ;
2023-03-28 05:36:59 +03:00
final visible = ffi . ffiModel . keyboard & &
2023-02-23 09:30:29 +03:00
pi . platform ! = kPeerPlatformAndroid & &
pi . platform ! = kPeerPlatformMacOS & &
version_cmp ( pi . version , ' 1.2.0 ' ) > = 0 ;
if ( ! visible ) return Offstage ( ) ;
return _MenuItemButton (
child: Text ( translate ( ' Switch Sides ' ) ) ,
ffi: ffi ,
onPressed: ( ) = > _showConfirmSwitchSidesDialog ( id , ffi . dialogManager ) ) ;
}
2022-08-26 18:28:08 +03:00
2023-02-23 09:30:29 +03:00
void _showConfirmSwitchSidesDialog (
String id , OverlayDialogManager dialogManager ) async {
dialogManager . show ( ( setState , close ) {
submit ( ) async {
await bind . sessionSwitchSides ( id: id ) ;
closeConnection ( id: id ) ;
}
2022-09-08 10:35:19 +03:00
2023-02-23 09:30:29 +03:00
return CustomAlertDialog (
content: msgboxContent ( ' info ' , ' Switch Sides ' ,
' Please confirm if you want to share your desktop? ' ) ,
actions: [
dialogButton ( ' Cancel ' , onPressed: close , isOutline: true ) ,
dialogButton ( ' OK ' , onPressed: submit ) ,
] ,
onSubmit: submit ,
onCancel: close ,
) ;
} ) ;
2022-08-26 18:28:08 +03:00
}
2023-02-23 09:30:29 +03:00
refresh ( ) {
final pi = ffi . ffiModel . pi ;
final visible = pi . version . isNotEmpty ;
if ( ! visible ) return Offstage ( ) ;
return _MenuItemButton (
child: Text ( translate ( ' Refresh ' ) ) ,
ffi: ffi ,
onPressed: ( ) = > bind . sessionRefresh ( id: id ) ) ;
}
}
2022-10-09 14:27:30 +03:00
2023-02-23 09:30:29 +03:00
class _DisplayMenu extends StatefulWidget {
final String id ;
final FFI ffi ;
final MenubarState state ;
final Function ( bool ) setFullscreen ;
_DisplayMenu (
{ Key ? key ,
required this . id ,
required this . ffi ,
required this . state ,
required this . setFullscreen } )
: super ( key: key ) ;
@ override
State < _DisplayMenu > createState ( ) = > _DisplayMenuState ( ) ;
}
class _DisplayMenuState extends State < _DisplayMenu > {
window_size . Screen ? _screen ;
bool get isFullscreen = > stateGlobal . fullscreen ;
int get windowId = > stateGlobal . windowId ;
Map < String , bool > get perms = > widget . ffi . ffiModel . permissions ;
PeerInfo get pi = > widget . ffi . ffiModel . pi ;
2023-03-28 05:36:59 +03:00
FfiModel get ffiModel = > widget . ffi . ffiModel ;
2023-02-23 09:30:29 +03:00
@ override
Widget build ( BuildContext context ) {
_updateScreen ( ) ;
return _IconSubmenuButton (
2023-03-10 09:16:18 +03:00
tooltip: ' Display Settings ' ,
2023-02-23 09:30:29 +03:00
svg: " assets/display.svg " ,
ffi: widget . ffi ,
color: _MenubarTheme . blueColor ,
hoverColor: _MenubarTheme . hoverBlueColor ,
menuChildren: [
adjustWindow ( ) ,
viewStyle ( ) ,
scrollStyle ( ) ,
imageQuality ( ) ,
codec ( ) ,
resolutions ( ) ,
Divider ( ) ,
showRemoteCursor ( ) ,
zoomCursor ( ) ,
showQualityMonitor ( ) ,
mute ( ) ,
fileCopyAndPaste ( ) ,
disableClipboard ( ) ,
lockAfterSessionEnd ( ) ,
privacyMode ( ) ,
2023-03-01 20:00:56 +03:00
swapKey ( ) ,
2023-02-23 09:30:29 +03:00
] ) ;
}
adjustWindow ( ) {
final visible = _isWindowCanBeAdjusted ( ) ;
if ( ! visible ) return Offstage ( ) ;
return Column (
children: [
_MenuItemButton (
child: Text ( translate ( ' Adjust Window ' ) ) ,
onPressed: _doAdjustWindow ,
ffi: widget . ffi ) ,
Divider ( ) ,
] ,
) ;
}
_doAdjustWindow ( ) async {
await _updateScreen ( ) ;
if ( _screen ! = null ) {
widget . setFullscreen ( false ) ;
double scale = _screen ! . scaleFactor ;
final wndRect = await WindowController . fromWindowId ( windowId ) . getFrame ( ) ;
final mediaSize = MediaQueryData . fromWindow ( ui . window ) . 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 = widget . ffi . canvasModel ;
final width = ( canvasModel . getDisplayWidth ( ) * canvasModel . scale +
2023-03-01 20:00:56 +03:00
CanvasModel . leftToEdge +
CanvasModel . rightToEdge ) *
2023-02-23 09:30:29 +03:00
scale +
magicWidth ;
final height = ( canvasModel . getDisplayHeight ( ) * canvasModel . scale +
2023-03-01 20:00:56 +03:00
CanvasModel . topToEdge +
CanvasModel . bottomToEdge ) *
2023-02-23 09:30:29 +03:00
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 ) ) ;
}
}
_updateScreen ( ) async {
final v = await rustDeskWinManager . call (
WindowType . Main , kWindowGetWindowInfo , ' ' ) ;
final String valueStr = v ;
if ( valueStr . isEmpty ) {
_screen = null ;
} else {
final screenMap = jsonDecode ( valueStr ) ;
_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 ' ] ) ;
}
}
_isWindowCanBeAdjusted ( ) {
if ( widget . state . viewStyle . value ! = kRemoteViewStyleOriginal ) {
return false ;
}
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 = widget . ffi . canvasModel ;
2022-10-09 14:27:30 +03:00
final displayWidth = canvasModel . getDisplayWidth ( ) ;
final displayHeight = canvasModel . getDisplayHeight ( ) ;
2023-03-01 20:00:56 +03:00
final requiredWidth =
CanvasModel . leftToEdge + displayWidth + CanvasModel . rightToEdge ;
final requiredHeight =
CanvasModel . topToEdge + displayHeight + CanvasModel . bottomToEdge ;
2022-10-09 14:27:30 +03:00
return selfWidth > ( requiredWidth * scale ) & &
selfHeight > ( requiredHeight * scale ) ;
2022-10-08 12:27:30 +03:00
}
2023-02-23 09:30:29 +03:00
viewStyle ( ) {
return futureBuilder ( future: ( ) async {
final viewStyle = await bind . sessionGetViewStyle ( id: widget . id ) ? ? ' ' ;
widget . state . viewStyle . value = viewStyle ;
return viewStyle ;
} ( ) , hasData: ( data ) {
final groupValue = data as String ;
onChanged ( String ? value ) async {
if ( value = = null ) return ;
await bind . sessionSetViewStyle ( id: widget . id , value: value ) ;
widget . state . viewStyle . value = value ;
widget . ffi . canvasModel . updateViewStyle ( ) ;
}
return Column ( children: [
_RadioMenuButton < String > (
child: Text ( translate ( ' Scale original ' ) ) ,
value: kRemoteViewStyleOriginal ,
groupValue: groupValue ,
onChanged: onChanged ,
ffi: widget . ffi ,
) ,
_RadioMenuButton < String > (
child: Text ( translate ( ' Scale adaptive ' ) ) ,
value: kRemoteViewStyleAdaptive ,
groupValue: groupValue ,
onChanged: onChanged ,
ffi: widget . ffi ,
) ,
Divider ( ) ,
] ) ;
} ) ;
}
scrollStyle ( ) {
final visible = widget . state . viewStyle . value = = kRemoteViewStyleOriginal ;
if ( ! visible ) return Offstage ( ) ;
return futureBuilder ( future: ( ) async {
final scrollStyle = await bind . sessionGetScrollStyle ( id: widget . id ) ? ? ' ' ;
return scrollStyle ;
} ( ) , hasData: ( data ) {
final groupValue = data as String ;
onChange ( String ? value ) async {
if ( value = = null ) return ;
await bind . sessionSetScrollStyle ( id: widget . id , value: value ) ;
widget . ffi . canvasModel . updateScrollStyle ( ) ;
}
final enabled = widget . ffi . canvasModel . imageOverflow . value ;
return Column ( children: [
_RadioMenuButton < String > (
child: Text ( translate ( ' ScrollAuto ' ) ) ,
value: kRemoteScrollStyleAuto ,
groupValue: groupValue ,
onChanged: enabled ? ( value ) = > onChange ( value ) : null ,
ffi: widget . ffi ,
) ,
_RadioMenuButton < String > (
child: Text ( translate ( ' Scrollbar ' ) ) ,
value: kRemoteScrollStyleBar ,
groupValue: groupValue ,
onChanged: enabled ? ( value ) = > onChange ( value ) : null ,
ffi: widget . ffi ,
) ,
Divider ( ) ,
] ) ;
} ) ;
}
imageQuality ( ) {
return futureBuilder ( future: ( ) async {
final imageQuality =
await bind . sessionGetImageQuality ( id: widget . id ) ? ? ' ' ;
return imageQuality ;
} ( ) , hasData: ( data ) {
final groupValue = data as String ;
onChanged ( String ? value ) async {
if ( value = = null ) return ;
await bind . sessionSetImageQuality ( id: widget . id , value: value ) ;
}
2023-02-24 11:20:00 +03:00
return _SubmenuButton (
ffi: widget . ffi ,
2023-02-23 09:30:29 +03:00
child: Text ( translate ( ' Image Quality ' ) ) ,
menuChildren: [
_RadioMenuButton < String > (
child: Text ( translate ( ' Good image quality ' ) ) ,
2022-11-24 06:19:16 +03:00
value: kRemoteImageQualityBest ,
2023-02-23 09:30:29 +03:00
groupValue: groupValue ,
onChanged: onChanged ,
ffi: widget . ffi ,
2022-09-23 07:20:40 +03:00
) ,
2023-02-23 09:30:29 +03:00
_RadioMenuButton < String > (
child: Text ( translate ( ' Balanced ' ) ) ,
2022-11-24 06:19:16 +03:00
value: kRemoteImageQualityBalanced ,
2023-02-23 09:30:29 +03:00
groupValue: groupValue ,
onChanged: onChanged ,
ffi: widget . ffi ,
2022-09-23 07:20:40 +03:00
) ,
2023-02-23 09:30:29 +03:00
_RadioMenuButton < String > (
child: Text ( translate ( ' Optimize reaction time ' ) ) ,
2022-11-24 06:19:16 +03:00
value: kRemoteImageQualityLow ,
2023-02-23 09:30:29 +03:00
groupValue: groupValue ,
onChanged: onChanged ,
ffi: widget . ffi ,
2022-09-23 07:20:40 +03:00
) ,
2023-02-23 09:30:29 +03:00
_RadioMenuButton < String > (
child: Text ( translate ( ' Custom ' ) ) ,
2023-02-03 14:17:59 +03:00
value: kRemoteImageQualityCustom ,
2023-02-23 09:30:29 +03:00
groupValue: groupValue ,
onChanged: ( value ) {
onChanged ( value ) ;
_customImageQualityDialog ( ) ;
} ,
ffi: widget . ffi ,
2023-02-03 14:17:59 +03:00
) ,
2023-02-24 11:20:00 +03:00
] ,
2023-02-23 09:30:29 +03:00
) ;
} ) ;
}
2022-10-19 05:19:49 +03:00
2023-02-23 09:30:29 +03:00
_customImageQualityDialog ( ) async {
double qualityInitValue = 50 ;
double fpsInitValue = 30 ;
bool qualitySet = false ;
bool fpsSet = false ;
setCustomValues ( { double ? quality , double ? fps } ) async {
if ( quality ! = null ) {
qualitySet = true ;
await bind . sessionSetCustomImageQuality (
id: widget . id , value: quality . toInt ( ) ) ;
}
if ( fps ! = null ) {
fpsSet = true ;
await bind . sessionSetCustomFps ( id: widget . id , fps: fps . toInt ( ) ) ;
}
if ( ! qualitySet ) {
qualitySet = true ;
await bind . sessionSetCustomImageQuality (
id: widget . id , value: qualityInitValue . toInt ( ) ) ;
}
if ( ! fpsSet ) {
fpsSet = true ;
await bind . sessionSetCustomFps (
id: widget . id , fps: fpsInitValue . toInt ( ) ) ;
}
}
2022-10-19 05:19:49 +03:00
2023-02-23 09:30:29 +03:00
final btnClose = dialogButton ( ' Close ' , onPressed: ( ) async {
await setCustomValues ( ) ;
widget . ffi . dialogManager . dismissAll ( ) ;
} ) ;
2022-11-26 05:46:57 +03:00
2023-02-23 09:30:29 +03:00
// quality
final quality = await bind . sessionGetCustomImageQuality ( id: widget . id ) ;
qualityInitValue =
quality ! = null & & quality . isNotEmpty ? quality [ 0 ] . toDouble ( ) : 50.0 ;
const qualityMinValue = 10.0 ;
const qualityMaxValue = 100.0 ;
if ( qualityInitValue < qualityMinValue ) {
qualityInitValue = qualityMinValue ;
}
if ( qualityInitValue > qualityMaxValue ) {
qualityInitValue = qualityMaxValue ;
}
final RxDouble qualitySliderValue = RxDouble ( qualityInitValue ) ;
final debouncerQuality = Debouncer < double > (
Duration ( milliseconds: 1000 ) ,
onChanged: ( double v ) {
setCustomValues ( quality: v ) ;
} ,
initialValue: qualityInitValue ,
) ;
final qualitySlider = Obx ( ( ) = > Row (
children: [
Slider (
value: qualitySliderValue . value ,
min: qualityMinValue ,
max: qualityMaxValue ,
divisions: 18 ,
onChanged: ( double value ) {
qualitySliderValue . value = value ;
debouncerQuality . value = value ;
} ,
) ,
SizedBox (
width: 40 ,
2023-01-10 11:07:48 +03:00
child: Text (
2023-02-23 09:30:29 +03:00
' ${ qualitySliderValue . value . round ( ) } % ' ,
style: const TextStyle ( fontSize: 15 ) ,
) ) ,
SizedBox (
width: 50 ,
child: Text (
translate ( ' Bitrate ' ) ,
style: const TextStyle ( fontSize: 15 ) ,
) )
] ,
) ) ;
// fps
final fpsOption =
await bind . sessionGetOption ( id: widget . id , arg: ' custom-fps ' ) ;
fpsInitValue = fpsOption = = null ? 30 : double . tryParse ( fpsOption ) ? ? 30 ;
2023-04-06 16:36:37 +03:00
if ( fpsInitValue < 5 | | fpsInitValue > 120 ) {
2023-02-23 09:30:29 +03:00
fpsInitValue = 30 ;
2022-10-08 12:27:30 +03:00
}
2023-02-23 09:30:29 +03:00
final RxDouble fpsSliderValue = RxDouble ( fpsInitValue ) ;
final debouncerFps = Debouncer < double > (
Duration ( milliseconds: 1000 ) ,
onChanged: ( double v ) {
setCustomValues ( fps: v ) ;
} ,
initialValue: qualityInitValue ,
) ;
bool ? direct ;
try {
direct = ConnectionTypeState . find ( widget . id ) . direct . value = =
ConnectionType . strDirect ;
} catch ( _ ) { }
final fpsSlider = Offstage (
offstage: ( await bind . mainIsUsingPublicServer ( ) & & direct ! = true ) | |
version_cmp ( pi . version , ' 1.2.0 ' ) < 0 ,
child: Row (
children: [
Obx ( ( ( ) = > Slider (
value: fpsSliderValue . value ,
2023-04-06 16:36:37 +03:00
min: 5 ,
2023-02-23 09:30:29 +03:00
max: 120 ,
2023-04-06 16:36:37 +03:00
divisions: 23 ,
2023-02-23 09:30:29 +03:00
onChanged: ( double value ) {
fpsSliderValue . value = value ;
debouncerFps . value = value ;
} ,
) ) ) ,
SizedBox (
width: 40 ,
child: Obx ( ( ) = > Text (
' ${ fpsSliderValue . value . round ( ) } ' ,
style: const TextStyle ( fontSize: 15 ) ,
) ) ) ,
SizedBox (
width: 50 ,
child: Text (
translate ( ' FPS ' ) ,
style: const TextStyle ( fontSize: 15 ) ,
) )
] ,
) ,
) ;
final content = Column (
children: [ qualitySlider , fpsSlider ] ,
) ;
msgBoxCommon (
widget . ffi . dialogManager , ' Custom Image Quality ' , content , [ btnClose ] ) ;
}
2022-10-08 12:27:30 +03:00
2023-02-23 09:30:29 +03:00
codec ( ) {
return futureBuilder ( future: ( ) async {
2023-03-31 11:10:52 +03:00
final alternativeCodecs =
await bind . sessionAlternativeCodecs ( id: widget . id ) ;
2023-02-23 09:30:29 +03:00
final codecPreference =
await bind . sessionGetOption ( id: widget . id , arg: ' codec-preference ' ) ? ?
' ' ;
return {
2023-03-31 11:10:52 +03:00
' alternativeCodecs ' : alternativeCodecs ,
2023-02-23 09:30:29 +03:00
' codecPreference ' : codecPreference
} ;
} ( ) , hasData: ( data ) {
2022-09-16 14:43:28 +03:00
final List < bool > codecs = [ ] ;
try {
2023-03-31 11:10:52 +03:00
final Map codecsJson = jsonDecode ( data [ ' alternativeCodecs ' ] ) ;
final vp8 = codecsJson [ ' vp8 ' ] ? ? false ;
2022-09-16 14:43:28 +03:00
final h264 = codecsJson [ ' h264 ' ] ? ? false ;
final h265 = codecsJson [ ' h265 ' ] ? ? false ;
2023-03-31 11:10:52 +03:00
codecs . add ( vp8 ) ;
2022-09-16 14:43:28 +03:00
codecs . add ( h264 ) ;
codecs . add ( h265 ) ;
2022-12-20 17:55:54 +03:00
} catch ( e ) {
debugPrint ( " Show Codec Preference err= $ e " ) ;
}
2023-03-31 11:10:52 +03:00
final visible =
codecs . length = = 3 & & ( codecs [ 0 ] | | codecs [ 1 ] | | codecs [ 2 ] ) ;
2023-02-23 09:30:29 +03:00
if ( ! visible ) return Offstage ( ) ;
final groupValue = data [ ' codecPreference ' ] as String ;
onChanged ( String ? value ) async {
if ( value = = null ) return ;
await bind . sessionPeerOption (
id: widget . id , name: ' codec-preference ' , value: value ) ;
bind . sessionChangePreferCodec ( id: widget . id ) ;
2022-09-16 14:43:28 +03:00
}
2023-02-24 11:20:00 +03:00
return _SubmenuButton (
ffi: widget . ffi ,
2023-02-23 09:30:29 +03:00
child: Text ( translate ( ' Codec ' ) ) ,
menuChildren: [
_RadioMenuButton < String > (
child: Text ( translate ( ' Auto ' ) ) ,
value: ' auto ' ,
groupValue: groupValue ,
onChanged: onChanged ,
ffi: widget . ffi ,
) ,
2023-03-31 11:10:52 +03:00
_RadioMenuButton < String > (
child: Text ( translate ( ' VP8 ' ) ) ,
value: ' vp8 ' ,
groupValue: groupValue ,
onChanged: codecs [ 0 ] ? onChanged : null ,
ffi: widget . ffi ,
) ,
2023-02-23 09:30:29 +03:00
_RadioMenuButton < String > (
child: Text ( translate ( ' VP9 ' ) ) ,
value: ' vp9 ' ,
groupValue: groupValue ,
onChanged: onChanged ,
ffi: widget . ffi ,
) ,
_RadioMenuButton < String > (
child: Text ( translate ( ' H264 ' ) ) ,
value: ' h264 ' ,
groupValue: groupValue ,
2023-03-31 11:10:52 +03:00
onChanged: codecs [ 1 ] ? onChanged : null ,
2023-02-23 09:30:29 +03:00
ffi: widget . ffi ,
) ,
_RadioMenuButton < String > (
child: Text ( translate ( ' H265 ' ) ) ,
value: ' h265 ' ,
groupValue: groupValue ,
2023-03-31 11:10:52 +03:00
onChanged: codecs [ 2 ] ? onChanged : null ,
2023-02-23 09:30:29 +03:00
ffi: widget . ffi ,
) ,
2023-02-24 11:20:00 +03:00
] ) ;
2023-02-23 09:30:29 +03:00
} ) ;
}
resolutions ( ) {
2023-03-28 05:36:59 +03:00
final resolutions = pi . resolutions ;
final visible = ffiModel . keyboard & & resolutions . length > 1 ;
2023-02-23 09:30:29 +03:00
if ( ! visible ) return Offstage ( ) ;
2023-03-28 05:36:59 +03:00
final display = ffiModel . display ;
2023-02-23 09:30:29 +03:00
final groupValue = " ${ display . width } x ${ display . height } " ;
onChanged ( String ? value ) async {
if ( value = = null ) return ;
final list = value . split ( ' x ' ) ;
if ( list . length = = 2 ) {
final w = int . tryParse ( list [ 0 ] ) ;
final h = int . tryParse ( list [ 1 ] ) ;
if ( w ! = null & & h ! = null ) {
await bind . sessionChangeResolution (
id: widget . id , width: w , height: h ) ;
2023-02-23 16:57:45 +03:00
Future . delayed ( Duration ( seconds: 3 ) , ( ) async {
2023-03-28 05:36:59 +03:00
final display = ffiModel . display ;
2023-02-23 16:57:45 +03:00
if ( w = = display . width & & h = = display . height ) {
if ( _isWindowCanBeAdjusted ( ) ) {
_doAdjustWindow ( ) ;
}
}
} ) ;
2023-02-23 09:30:29 +03:00
}
}
2022-11-24 06:19:16 +03:00
}
2022-11-16 13:07:58 +03:00
2023-02-24 11:20:00 +03:00
return _SubmenuButton (
ffi: widget . ffi ,
2023-02-23 09:30:29 +03:00
menuChildren: resolutions
. map ( ( e ) = > _RadioMenuButton (
value: ' ${ e . width } x ${ e . height } ' ,
groupValue: groupValue ,
onChanged: onChanged ,
ffi: widget . ffi ,
child: Text ( ' ${ e . width } x ${ e . height } ' ) ) )
. toList ( ) ,
child: Text ( translate ( " Resolution " ) ) ) ;
}
2022-08-26 18:28:08 +03:00
2023-02-23 09:30:29 +03:00
showRemoteCursor ( ) {
2023-03-28 05:36:59 +03:00
if ( pi . platform = = kPeerPlatformAndroid ) {
2023-03-11 13:21:23 +03:00
return Offstage ( ) ;
}
2023-03-21 07:25:47 +03:00
final visible =
! widget . ffi . canvasModel . cursorEmbedded & & ! ffiModel . pi . is_wayland ;
2023-02-23 09:30:29 +03:00
if ( ! visible ) return Offstage ( ) ;
2023-03-17 06:27:22 +03:00
final enabled = ! ffiModel . viewOnly ;
2023-02-23 09:30:29 +03:00
final state = ShowRemoteCursorState . find ( widget . id ) ;
final option = ' show-remote-cursor ' ;
return _CheckboxMenuButton (
value: state . value ,
2023-03-17 06:27:22 +03:00
onChanged: enabled
? ( value ) async {
if ( value = = null ) return ;
await bind . sessionToggleOption ( id: widget . id , value: option ) ;
state . value =
bind . sessionGetToggleOptionSync ( id: widget . id , arg: option ) ;
}
: null ,
2023-02-23 09:30:29 +03:00
ffi: widget . ffi ,
child: Text ( translate ( ' Show remote cursor ' ) ) ) ;
}
2022-08-26 18:28:08 +03:00
2023-02-23 09:30:29 +03:00
zoomCursor ( ) {
2023-03-28 05:36:59 +03:00
if ( pi . platform = = kPeerPlatformAndroid ) {
2023-03-11 13:26:36 +03:00
return Offstage ( ) ;
}
2023-02-23 09:30:29 +03:00
final visible = widget . state . viewStyle . value ! = kRemoteViewStyleOriginal ;
if ( ! visible ) return Offstage ( ) ;
final option = ' zoom-cursor ' ;
final peerState = PeerBoolOption . find ( widget . id , option ) ;
return _CheckboxMenuButton (
value: peerState . value ,
onChanged: ( value ) async {
if ( value = = null ) return ;
await bind . sessionToggleOption ( id: widget . id , value: option ) ;
peerState . value =
bind . sessionGetToggleOptionSync ( id: widget . id , arg: option ) ;
} ,
ffi: widget . ffi ,
child: Text ( translate ( ' Zoom cursor ' ) ) ) ;
}
2022-09-08 18:25:19 +03:00
2023-02-23 09:30:29 +03:00
showQualityMonitor ( ) {
final option = ' show-quality-monitor ' ;
final value = bind . sessionGetToggleOptionSync ( id: widget . id , arg: option ) ;
return _CheckboxMenuButton (
value: value ,
onChanged: ( value ) async {
if ( value = = null ) return ;
await bind . sessionToggleOption ( id: widget . id , value: option ) ;
widget . ffi . qualityMonitorModel . checkShowQualityMonitor ( widget . id ) ;
} ,
ffi: widget . ffi ,
child: Text ( translate ( ' Show quality monitor ' ) ) ) ;
}
2022-09-08 18:25:19 +03:00
2023-02-23 09:30:29 +03:00
mute ( ) {
final visible = perms [ ' audio ' ] ! = false ;
if ( ! visible ) return Offstage ( ) ;
final option = ' disable-audio ' ;
final value = bind . sessionGetToggleOptionSync ( id: widget . id , arg: option ) ;
return _CheckboxMenuButton (
value: value ,
onChanged: ( value ) {
if ( value = = null ) return ;
bind . sessionToggleOption ( id: widget . id , value: option ) ;
2022-12-26 13:30:25 +03:00
} ,
2023-02-23 09:30:29 +03:00
ffi: widget . ffi ,
child: Text ( translate ( ' Mute ' ) ) ) ;
}
fileCopyAndPaste ( ) {
final visible = Platform . isWindows & &
pi . platform = = kPeerPlatformWindows & &
perms [ ' file ' ] ! = false ;
if ( ! visible ) return Offstage ( ) ;
final option = ' enable-file-transfer ' ;
final value = bind . sessionGetToggleOptionSync ( id: widget . id , arg: option ) ;
return _CheckboxMenuButton (
value: value ,
onChanged: ( value ) {
if ( value = = null ) return ;
bind . sessionToggleOption ( id: widget . id , value: option ) ;
2022-11-16 10:09:29 +03:00
} ,
2023-02-23 09:30:29 +03:00
ffi: widget . ffi ,
child: Text ( translate ( ' Allow file copy and paste ' ) ) ) ;
}
disableClipboard ( ) {
2023-03-28 05:36:59 +03:00
final visible = ffiModel . keyboard & & perms [ ' clipboard ' ] ! = false ;
2023-02-23 09:30:29 +03:00
if ( ! visible ) return Offstage ( ) ;
2023-03-17 06:27:22 +03:00
final enabled = ! ffiModel . viewOnly ;
2023-02-23 09:30:29 +03:00
final option = ' disable-clipboard ' ;
2023-03-17 06:27:22 +03:00
var value = bind . sessionGetToggleOptionSync ( id: widget . id , arg: option ) ;
if ( ffiModel . viewOnly ) value = true ;
2023-02-23 09:30:29 +03:00
return _CheckboxMenuButton (
value: value ,
2023-03-17 06:27:22 +03:00
onChanged: enabled
? ( value ) {
if ( value = = null ) return ;
bind . sessionToggleOption ( id: widget . id , value: option ) ;
}
: null ,
2023-02-23 09:30:29 +03:00
ffi: widget . ffi ,
child: Text ( translate ( ' Disable clipboard ' ) ) ) ;
2022-09-05 17:18:29 +03:00
}
2023-02-23 09:30:29 +03:00
lockAfterSessionEnd ( ) {
2023-03-28 05:36:59 +03:00
if ( ! ffiModel . keyboard ) return Offstage ( ) ;
2023-02-23 09:30:29 +03:00
final option = ' lock-after-session-end ' ;
final value = bind . sessionGetToggleOptionSync ( id: widget . id , arg: option ) ;
return _CheckboxMenuButton (
value: value ,
onChanged: ( value ) {
if ( value = = null ) return ;
bind . sessionToggleOption ( id: widget . id , value: option ) ;
} ,
ffi: widget . ffi ,
child: Text ( translate ( ' Lock after session end ' ) ) ) ;
}
privacyMode ( ) {
2023-03-28 05:36:59 +03:00
bool visible = ffiModel . keyboard & & pi . features . privacyMode ;
2023-02-23 09:30:29 +03:00
if ( ! visible ) return Offstage ( ) ;
final option = ' privacy-mode ' ;
final rxValue = PrivacyModeState . find ( widget . id ) ;
return _CheckboxMenuButton (
value: rxValue . value ,
onChanged: ( value ) {
if ( value = = null ) return ;
2023-03-28 05:36:59 +03:00
if ( ffiModel . pi . currentDisplay ! = 0 ) {
2023-03-06 11:41:26 +03:00
msgBox (
widget . id ,
' custom-nook-nocancel-hasclose ' ,
' info ' ,
' Please switch to Display 1 first ' ,
' ' ,
widget . ffi . dialogManager ) ;
return ;
}
2023-02-23 09:30:29 +03:00
bind . sessionToggleOption ( id: widget . id , value: option ) ;
} ,
ffi: widget . ffi ,
child: Text ( translate ( ' Privacy mode ' ) ) ) ;
2022-08-26 18:28:08 +03:00
}
2023-03-01 20:00:56 +03:00
swapKey ( ) {
2023-03-28 05:36:59 +03:00
final visible = ffiModel . keyboard & &
2023-03-01 20:00:56 +03:00
( ( Platform . isMacOS & & pi . platform ! = kPeerPlatformMacOS ) | |
( ! Platform . isMacOS & & pi . platform = = kPeerPlatformMacOS ) ) ;
if ( ! visible ) return Offstage ( ) ;
final option = ' allow_swap_key ' ;
final value = bind . sessionGetToggleOptionSync ( id: widget . id , arg: option ) ;
return _CheckboxMenuButton (
value: value ,
onChanged: ( value ) {
if ( value = = null ) return ;
bind . sessionToggleOption ( id: widget . id , value: option ) ;
} ,
ffi: widget . ffi ,
child: Text ( translate ( ' Swap control-command key ' ) ) ) ;
}
2022-08-26 18:28:08 +03:00
}
2023-02-23 09:30:29 +03:00
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 ) {
2023-03-01 20:00:56 +03:00
var ffiModel = Provider . of < FfiModel > ( context ) ;
2023-03-28 05:36:59 +03:00
if ( ! ffiModel . keyboard ) return Offstage ( ) ;
2023-03-28 07:10:58 +03:00
String ? modeOnly ;
2023-02-23 09:30:29 +03:00
if ( stateGlobal . grabKeyboard ) {
2023-02-25 17:59:59 +03:00
if ( bind . sessionIsKeyboardModeSupported ( id: id , mode: _kKeyMapMode ) ) {
bind . sessionSetKeyboardMode ( id: id , value: _kKeyMapMode ) ;
2023-03-28 07:10:58 +03:00
modeOnly = _kKeyMapMode ;
2023-02-25 18:11:28 +03:00
} else if ( bind . sessionIsKeyboardModeSupported (
id: id , mode: _kKeyLegacyMode ) ) {
bind . sessionSetKeyboardMode ( id: id , value: _kKeyLegacyMode ) ;
2023-03-28 07:10:58 +03:00
modeOnly = _kKeyLegacyMode ;
2023-02-25 17:59:59 +03:00
}
2022-09-03 13:19:50 +03:00
}
2023-02-23 09:30:29 +03:00
return _IconSubmenuButton (
2023-03-10 09:16:18 +03:00
tooltip: ' Keyboard Settings ' ,
2023-02-23 09:30:29 +03:00
svg: " assets/keyboard.svg " ,
ffi: ffi ,
color: _MenubarTheme . blueColor ,
hoverColor: _MenubarTheme . hoverBlueColor ,
2023-03-17 06:27:22 +03:00
menuChildren: [
2023-03-28 07:10:58 +03:00
mode ( modeOnly ) ,
2023-03-17 06:27:22 +03:00
localKeyboardType ( ) ,
Divider ( ) ,
view_mode ( ) ,
] ) ;
2023-02-23 09:30:29 +03:00
}
2022-09-03 13:19:50 +03:00
2023-03-28 07:10:58 +03:00
mode ( String ? modeOnly ) {
2023-02-23 09:30:29 +03:00
return futureBuilder ( future: ( ) async {
2023-02-25 17:59:59 +03:00
return await bind . sessionGetKeyboardMode ( id: id ) ? ? _kKeyLegacyMode ;
2023-02-23 09:30:29 +03:00
} ( ) , hasData: ( data ) {
final groupValue = data as String ;
List < KeyboardModeMenu > modes = [
2023-02-25 17:59:59 +03:00
KeyboardModeMenu ( key: _kKeyLegacyMode , menu: ' Legacy mode ' ) ,
KeyboardModeMenu ( key: _kKeyMapMode , menu: ' Map mode ' ) ,
KeyboardModeMenu ( key: _kKeyTranslateMode , menu: ' Translate mode ' ) ,
2023-02-23 09:30:29 +03:00
] ;
List < _RadioMenuButton > list = [ ] ;
2023-03-17 06:27:22 +03:00
final enabled = ! ffi . ffiModel . viewOnly ;
2023-02-23 09:30:29 +03:00
onChanged ( String ? value ) async {
if ( value = = null ) return ;
await bind . sessionSetKeyboardMode ( id: id , value: value ) ;
}
for ( KeyboardModeMenu mode in modes ) {
2023-03-28 07:10:58 +03:00
if ( modeOnly ! = null & & mode . key ! = modeOnly ) {
continue ;
} else if ( ! bind . sessionIsKeyboardModeSupported (
id: id , mode: mode . key ) ) {
continue ;
}
if ( pi . is_wayland & & mode . key ! = _kKeyMapMode ) {
continue ;
2023-02-23 09:30:29 +03:00
}
2023-03-28 07:10:58 +03:00
var text = translate ( mode . menu ) ;
if ( mode . key = = _kKeyTranslateMode ) {
text = ' $ text beta ' ;
}
list . add ( _RadioMenuButton < String > (
child: Text ( text ) ,
value: mode . key ,
groupValue: groupValue ,
onChanged: enabled ? onChanged : null ,
ffi: ffi ,
) ) ;
2023-02-23 09:30:29 +03:00
}
return Column ( children: list ) ;
} ) ;
}
localKeyboardType ( ) {
final localPlatform = getLocalPlatformForKBLayoutType ( pi . platform ) ;
final visible = localPlatform ! = ' ' ;
if ( ! visible ) return Offstage ( ) ;
2023-03-17 06:27:22 +03:00
final enabled = ! ffi . ffiModel . viewOnly ;
2023-02-23 09:30:29 +03:00
return Column (
children: [
Divider ( ) ,
_MenuItemButton (
child: Text (
' ${ translate ( ' Local keyboard type ' ) } : ${ KBLayoutType . value } ' ) ,
trailingIcon: const Icon ( Icons . settings ) ,
ffi: ffi ,
2023-03-17 06:27:22 +03:00
onPressed: enabled
? ( ) = > showKBLayoutTypeChooser ( localPlatform , ffi . dialogManager )
: null ,
2023-02-23 09:30:29 +03:00
)
2022-09-03 13:19:50 +03:00
] ,
) ;
2023-02-23 09:30:29 +03:00
}
2023-03-17 06:27:22 +03:00
view_mode ( ) {
final ffiModel = ffi . ffiModel ;
2023-03-28 05:36:59 +03:00
final enabled = version_cmp ( pi . version , ' 1.2.0 ' ) > = 0 & & ffiModel . keyboard ;
2023-03-17 06:27:22 +03:00
return _CheckboxMenuButton (
value: ffiModel . viewOnly ,
onChanged: enabled
? ( value ) async {
if ( value = = null ) return ;
await bind . sessionToggleOption ( id: id , value: ' view-only ' ) ;
ffiModel . setViewOnly ( id , value ) ;
}
: null ,
ffi: ffi ,
child: Text ( translate ( ' View Mode ' ) ) ) ;
}
2022-08-26 18:28:08 +03:00
}
2022-09-08 10:35:19 +03:00
2023-02-23 09:30:29 +03:00
class _ChatMenu extends StatefulWidget {
final String id ;
final FFI ffi ;
_ChatMenu ( {
Key ? key ,
required this . id ,
required this . ffi ,
} ) : super ( key: key ) ;
2022-09-08 10:35:19 +03:00
2023-02-23 09:30:29 +03:00
@ 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 ) {
return _IconSubmenuButton (
2023-03-10 09:16:18 +03:00
tooltip: ' Chat ' ,
2023-02-23 09:30:29 +03:00
key: chatButtonKey ,
svg: ' assets/chat.svg ' ,
ffi: widget . ffi ,
color: _MenubarTheme . blueColor ,
hoverColor: _MenubarTheme . hoverBlueColor ,
menuChildren: [ textChat ( ) , voiceCall ( ) ] ) ;
}
textChat ( ) {
return _MenuItemButton (
child: Text ( translate ( ' Text chat ' ) ) ,
ffi: widget . ffi ,
onPressed: ( ) {
RenderBox ? renderBox =
chatButtonKey . currentContext ? . findRenderObject ( ) as RenderBox ? ;
Offset ? initPos ;
if ( renderBox ! = null ) {
final pos = renderBox . localToGlobal ( Offset . zero ) ;
initPos = Offset ( pos . dx , pos . dy + _MenubarTheme . dividerHeight ) ;
2022-09-08 10:35:19 +03:00
}
2023-02-23 09:30:29 +03:00
widget . ffi . chatModel . changeCurrentID ( ChatModel . clientModeID ) ;
widget . ffi . chatModel . toggleChatOverlay ( chatInitPos: initPos ) ;
} ) ;
}
voiceCall ( ) {
return _MenuItemButton (
child: Text ( translate ( ' Voice call ' ) ) ,
ffi: widget . ffi ,
onPressed: ( ) = > bind . sessionRequestVoiceCall ( id: widget . id ) ,
) ;
}
}
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 ) {
return Obx (
( ) {
final String tooltip ;
final String icon ;
switch ( ffi . chatModel . voiceCallStatus . value ) {
case VoiceCallStatus . waitingForResponse:
tooltip = " Waiting " ;
icon = " assets/call_wait.svg " ;
break ;
case VoiceCallStatus . connected:
tooltip = " Disconnect " ;
icon = " assets/call_end.svg " ;
break ;
default :
return Offstage ( ) ;
2022-09-08 10:35:19 +03:00
}
2023-02-23 09:30:29 +03:00
return _IconMenuButton (
assetName: icon ,
tooltip: tooltip ,
onPressed: ( ) = > bind . sessionCloseVoiceCall ( id: id ) ,
color: _MenubarTheme . redColor ,
hoverColor: _MenubarTheme . hoverRedColor ) ;
2022-09-08 10:35:19 +03:00
} ,
) ;
2023-02-23 09:30:29 +03:00
}
}
2022-09-08 10:35:19 +03:00
2023-02-23 09:30:29 +03:00
class _RecordMenu extends StatelessWidget {
const _RecordMenu ( { Key ? key } ) : super ( key: key ) ;
@ override
Widget build ( BuildContext context ) {
var ffi = Provider . of < FfiModel > ( context ) ;
final visible = ffi . permissions [ ' recording ' ] ! = false ;
if ( ! visible ) return Offstage ( ) ;
return Consumer < RecordingModel > (
builder: ( context , value , child ) = > _IconMenuButton (
assetName: ' assets/rec.svg ' ,
tooltip:
value . start ? ' Stop session recording ' : ' Start session recording ' ,
onPressed: ( ) = > value . toggle ( ) ,
color: value . start ? _MenubarTheme . redColor : _MenubarTheme . blueColor ,
hoverColor: value . start
? _MenubarTheme . hoverRedColor
: _MenubarTheme . hoverBlueColor ,
) ,
2023-01-17 08:28:33 +03:00
) ;
2023-02-23 09:30:29 +03:00
}
2023-01-17 08:28:33 +03:00
}
2023-02-23 09:30:29 +03:00
class _CloseMenu extends StatelessWidget {
final String id ;
final FFI ffi ;
const _CloseMenu ( { Key ? key , required this . id , required this . ffi } )
: super ( key: key ) ;
2023-01-17 08:28:33 +03:00
2023-02-23 09:30:29 +03:00
@ override
Widget build ( BuildContext context ) {
return _IconMenuButton (
assetName: ' assets/close.svg ' ,
tooltip: ' Close ' ,
onPressed: ( ) = > clientClose ( id , ffi . dialogManager ) ,
color: _MenubarTheme . redColor ,
hoverColor: _MenubarTheme . hoverRedColor ,
) ;
}
}
class _IconMenuButton extends StatefulWidget {
final String ? assetName ;
final Widget ? icon ;
2023-03-10 09:16:18 +03:00
final String ? tooltip ;
2023-02-23 09:30:29 +03:00
final Color color ;
final Color hoverColor ;
final VoidCallback ? onPressed ;
final double ? hMargin ;
final double ? vMargin ;
2023-03-10 08:54:23 +03:00
final bool topLevel ;
2023-02-23 09:30:29 +03:00
const _IconMenuButton ( {
Key ? key ,
this . assetName ,
this . icon ,
2023-03-10 09:16:18 +03:00
this . tooltip ,
2023-02-23 09:30:29 +03:00
required this . color ,
required this . hoverColor ,
required this . onPressed ,
this . hMargin ,
this . vMargin ,
2023-03-10 08:54:23 +03:00
this . topLevel = true ,
2023-02-23 09:30:29 +03:00
} ) : 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 ! ,
color: Colors . white ,
width: _MenubarTheme . buttonSize ,
height: _MenubarTheme . buttonSize ,
) ;
2023-03-10 08:54:23 +03:00
final button = SizedBox (
2023-02-23 09:30:29 +03:00
width: _MenubarTheme . buttonSize ,
height: _MenubarTheme . buttonSize ,
child: MenuItemButton (
style: ButtonStyle (
2023-03-11 14:29:26 +03:00
backgroundColor: MaterialStatePropertyAll ( Colors . transparent ) ,
2023-02-23 09:30:29 +03:00
padding: MaterialStatePropertyAll ( EdgeInsets . zero ) ,
overlayColor: MaterialStatePropertyAll ( Colors . transparent ) ) ,
onHover: ( value ) = > setState ( ( ) {
hover = value ;
} ) ,
onPressed: widget . onPressed ,
2023-03-10 09:16:18 +03:00
child: Material (
type: MaterialType . transparency ,
child: Ink (
decoration: BoxDecoration (
borderRadius: BorderRadius . circular ( _MenubarTheme . iconRadius ) ,
color: hover ? widget . hoverColor : widget . color ,
) ,
child: icon ) ) ,
2023-02-23 09:30:29 +03:00
) ,
) . marginSymmetric (
horizontal: widget . hMargin ? ? _MenubarTheme . buttonHMargin ,
vertical: widget . vMargin ? ? _MenubarTheme . buttonVMargin ) ;
2023-03-10 08:54:23 +03:00
if ( widget . topLevel ) {
return MenuBar ( children: [ button ] ) ;
} else {
return button ;
}
2023-02-23 09:30:29 +03:00
}
}
class _IconSubmenuButton extends StatefulWidget {
2023-03-10 09:16:18 +03:00
final String tooltip ;
2023-02-23 09:30:29 +03:00
final String ? svg ;
final Widget ? icon ;
final Color color ;
final Color hoverColor ;
final List < Widget > menuChildren ;
final MenuStyle ? menuStyle ;
final FFI ffi ;
_IconSubmenuButton (
{ Key ? key ,
this . svg ,
this . icon ,
2023-03-10 09:16:18 +03:00
required this . tooltip ,
2023-02-23 09:30:29 +03:00
required this . color ,
required this . hoverColor ,
required this . menuChildren ,
required this . ffi ,
this . menuStyle } )
: 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 ! ,
color: Colors . white ,
width: _MenubarTheme . buttonSize ,
height: _MenubarTheme . buttonSize ,
) ;
2023-03-10 08:54:23 +03:00
final button = SizedBox (
2023-03-10 09:16:18 +03:00
width: _MenubarTheme . buttonSize ,
height: _MenubarTheme . buttonSize ,
child: SubmenuButton (
menuStyle: widget . menuStyle ,
style: ButtonStyle (
2023-03-11 14:29:26 +03:00
backgroundColor: MaterialStatePropertyAll ( Colors . transparent ) ,
2023-03-10 09:16:18 +03:00
padding: MaterialStatePropertyAll ( EdgeInsets . zero ) ,
overlayColor: MaterialStatePropertyAll ( Colors . transparent ) ) ,
onHover: ( value ) = > setState ( ( ) {
hover = value ;
} ) ,
child: Material (
type: MaterialType . transparency ,
child: Ink (
decoration: BoxDecoration (
borderRadius:
BorderRadius . circular ( _MenubarTheme . iconRadius ) ,
color: hover ? widget . hoverColor : widget . color ,
) ,
child: icon ) ) ,
menuChildren: widget . menuChildren
. map ( ( e ) = > _buildPointerTrackWidget ( e , widget . ffi ) )
. toList ( ) ) ) ;
return MenuBar ( children: [
button . marginSymmetric (
horizontal: _MenubarTheme . buttonHMargin ,
vertical: _MenubarTheme . buttonVMargin )
] ) ;
2023-02-23 09:30:29 +03:00
}
}
2023-02-24 11:20:00 +03:00
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 ( ) ,
) ;
}
}
2023-02-23 09:30:29 +03:00
class _MenuItemButton extends StatelessWidget {
final VoidCallback ? onPressed ;
final Widget ? trailingIcon ;
final Widget ? child ;
final FFI ffi ;
_MenuItemButton (
{ Key ? key ,
this . onPressed ,
this . trailingIcon ,
required this . child ,
required this . ffi } )
: super ( key: key ) ;
@ override
Widget build ( BuildContext context ) {
return MenuItemButton (
key: key ,
onPressed: onPressed ! = null
? ( ) {
_menuDismissCallback ( ffi ) ;
onPressed ? . call ( ) ;
}
: null ,
trailingIcon: trailingIcon ,
child: child ) ;
}
}
class _CheckboxMenuButton extends StatelessWidget {
final bool ? value ;
final ValueChanged < bool ? > ? onChanged ;
final Widget ? child ;
final FFI ffi ;
const _CheckboxMenuButton (
{ Key ? key ,
required this . value ,
required this . onChanged ,
required this . child ,
required this . ffi } )
: super ( key: key ) ;
@ override
Widget build ( BuildContext context ) {
return CheckboxMenuButton (
key: key ,
value: value ,
child: child ,
onChanged: onChanged ! = null
? ( bool ? value ) {
_menuDismissCallback ( ffi ) ;
onChanged ? . call ( value ) ;
}
: null ,
2022-09-08 10:35:19 +03:00
) ;
2023-02-23 09:30:29 +03:00
}
}
class _RadioMenuButton < T > extends StatelessWidget {
final T value ;
final T ? groupValue ;
final ValueChanged < T ? > ? onChanged ;
final Widget ? child ;
final FFI ffi ;
const _RadioMenuButton (
{ Key ? key ,
required this . value ,
required this . groupValue ,
required this . onChanged ,
required this . child ,
required this . ffi } )
: super ( key: key ) ;
@ override
Widget build ( BuildContext context ) {
return RadioMenuButton (
value: value ,
groupValue: groupValue ,
child: child ,
onChanged: onChanged ! = null
? ( T ? value ) {
_menuDismissCallback ( ffi ) ;
onChanged ? . call ( value ) ;
}
: null ,
) ;
}
2022-09-08 10:35:19 +03:00
}
2022-12-07 10:13:24 +03:00
class _DraggableShowHide extends StatefulWidget {
final RxDouble fractionX ;
final RxBool dragging ;
final RxBool show ;
const _DraggableShowHide ( {
Key ? key ,
required this . fractionX ,
required this . dragging ,
required this . show ,
} ) : super ( key: key ) ;
@ override
2023-01-04 11:41:05 +03:00
State < _DraggableShowHide > createState ( ) = > _DraggableShowHideState ( ) ;
2022-12-07 10:13:24 +03:00
}
2023-01-04 11:41:05 +03:00
class _DraggableShowHideState extends State < _DraggableShowHide > {
2022-12-07 10:13:24 +03:00
Offset position = Offset . zero ;
Size size = Size . zero ;
Widget _buildDraggable ( BuildContext context ) {
return Draggable (
axis: Axis . horizontal ,
child: Icon (
Icons . drag_indicator ,
2023-01-03 18:42:40 +03:00
size: 20 ,
2023-03-11 14:53:19 +03:00
color: MyTheme . color ( context ) . drag_indicator ,
2022-12-07 10:13:24 +03:00
) ,
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 . fromWindow ( ui . window ) . size ;
widget . fractionX . value + =
( details . offset . dx - position . dx ) / ( mediaSize . width - size . width ) ;
if ( widget . fractionX . value < 0.35 ) {
widget . fractionX . value = 0.35 ;
}
if ( widget . fractionX . value > 0.65 ) {
widget . fractionX . value = 0.65 ;
}
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 child = Row (
mainAxisSize: MainAxisSize . min ,
children: [
_buildDraggable ( context ) ,
TextButton (
onPressed: ( ) = > setState ( ( ) {
widget . show . value = ! widget . show . value ;
} ) ,
child: Obx ( ( ( ) = > Icon (
widget . show . isTrue ? Icons . expand_less : Icons . expand_more ,
2023-01-03 18:42:40 +03:00
size: 20 ,
2022-12-07 10:13:24 +03:00
) ) ) ,
) ,
] ,
) ;
return TextButtonTheme (
data: TextButtonThemeData ( style: buttonStyle ) ,
child: Container (
decoration: BoxDecoration (
2023-03-11 14:53:19 +03:00
color: Theme . of ( context )
. menuBarTheme
. style
? . backgroundColor
? . resolve ( MaterialState . values . toSet ( ) ) ,
2023-02-14 15:57:33 +03:00
borderRadius: BorderRadius . vertical (
bottom: Radius . circular ( 5 ) ,
) ,
2022-12-07 10:13:24 +03:00
) ,
child: SizedBox (
2023-01-03 18:42:40 +03:00
height: 20 ,
2022-12-07 10:13:24 +03:00
child: child ,
) ,
) ,
) ;
}
}
2023-02-03 05:41:47 +03:00
class KeyboardModeMenu {
final String key ;
final String menu ;
KeyboardModeMenu ( { required this . key , required this . menu } ) ;
}
2023-02-23 09:30:29 +03:00
_menuDismissCallback ( FFI ffi ) = > ffi . inputModel . refreshMousePos ( ) ;
Widget _buildPointerTrackWidget ( Widget child , FFI ffi ) {
return Listener (
onPointerHover: ( PointerHoverEvent e ) = >
ffi . inputModel . lastMousePos = e . position ,
child: MouseRegion (
child: child ,
) ,
) ;
}
2023-03-15 19:57:52 +03:00
class _MultiMonitorMenu extends StatelessWidget {
final String id ;
final FFI ffi ;
const _MultiMonitorMenu ( {
2023-03-23 20:54:49 +03:00
Key ? key ,
2023-03-15 19:57:52 +03:00
required this . id ,
required this . ffi ,
2023-03-23 20:54:49 +03:00
} ) : super ( key: key ) ;
2023-03-15 19:57:52 +03:00
@ override
Widget build ( BuildContext context ) {
final List < Widget > rowChildren = [ ] ;
final pi = ffi . ffiModel . pi ;
for ( int i = 0 ; i < pi . displays . length ; i + + ) {
rowChildren . add (
Obx ( ( ) {
RxInt display = CurrentDisplayState . find ( id ) ;
return _IconMenuButton (
topLevel: false ,
color: i = = display . value
? _MenubarTheme . blueColor
: Colors . grey [ 800 ] ! ,
hoverColor: i = = display . value
? _MenubarTheme . hoverBlueColor
: Colors . grey [ 850 ] ! ,
icon: Container (
alignment: AlignmentDirectional . center ,
constraints:
const BoxConstraints ( minHeight: _MenubarTheme . height ) ,
child: Stack (
alignment: Alignment . center ,
children: [
SvgPicture . asset (
" assets/screen.svg " ,
color: Colors . white ,
) ,
Obx (
( ) = > Text (
( i + 1 ) . toString ( ) ,
style: TextStyle (
color: i = = display . value
? _MenubarTheme . blueColor
: Colors . grey [ 800 ] ! ,
fontSize: 12 ,
fontWeight: FontWeight . bold ,
) ,
) ,
) ,
] ,
) ,
) ,
onPressed: ( ) {
if ( display . value ! = i ) {
bind . sessionSwitchDisplay ( id: id , value: i ) ;
}
} ,
) ;
} ) ,
) ;
}
return Row ( children: rowChildren ) ;
}
}