123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466 |
- 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),
- ),
- ],
- ),
- ),
- );
- }
- Widget _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,
- ).workaroundFreezeLinuxMint();
- }
- 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,
- ),
- );
- }
|