remote_toolbar.dart 74 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493
  1. import 'dart:convert';
  2. import 'dart:async';
  3. import 'package:flutter/material.dart';
  4. import 'package:flutter/services.dart';
  5. import 'package:flutter_hbb/common/widgets/audio_input.dart';
  6. import 'package:flutter_hbb/common/widgets/dialog.dart';
  7. import 'package:flutter_hbb/common/widgets/toolbar.dart';
  8. import 'package:flutter_hbb/models/chat_model.dart';
  9. import 'package:flutter_hbb/models/state_model.dart';
  10. import 'package:flutter_hbb/consts.dart';
  11. import 'package:flutter_hbb/utils/multi_window_manager.dart';
  12. import 'package:flutter_hbb/plugin/widgets/desc_ui.dart';
  13. import 'package:flutter_hbb/plugin/common.dart';
  14. import 'package:flutter_svg/flutter_svg.dart';
  15. import 'package:get/get.dart';
  16. import 'package:provider/provider.dart';
  17. import 'package:debounce_throttle/debounce_throttle.dart';
  18. import 'package:desktop_multi_window/desktop_multi_window.dart';
  19. import 'package:window_size/window_size.dart' as window_size;
  20. import '../../common.dart';
  21. import '../../models/model.dart';
  22. import '../../models/platform_model.dart';
  23. import '../../common/shared_state.dart';
  24. import './popup_menu.dart';
  25. import './kb_layout_type_chooser.dart';
  26. class ToolbarState {
  27. late RxBool _pin;
  28. bool isShowInited = false;
  29. RxBool show = false.obs;
  30. ToolbarState() {
  31. _pin = RxBool(false);
  32. final s = bind.getLocalFlutterOption(k: kOptionRemoteMenubarState);
  33. if (s.isEmpty) {
  34. return;
  35. }
  36. try {
  37. final m = jsonDecode(s);
  38. if (m != null) {
  39. _pin = RxBool(m['pin'] ?? false);
  40. }
  41. } catch (e) {
  42. debugPrint('Failed to decode toolbar state ${e.toString()}');
  43. }
  44. }
  45. bool get pin => _pin.value;
  46. switchShow(SessionID sessionId) async {
  47. bind.sessionToggleOption(
  48. sessionId: sessionId, value: kOptionCollapseToolbar);
  49. show.value = !show.value;
  50. }
  51. initShow(SessionID sessionId) async {
  52. if (!isShowInited) {
  53. show.value = !(await bind.sessionGetToggleOption(
  54. sessionId: sessionId, arg: kOptionCollapseToolbar) ??
  55. false);
  56. isShowInited = true;
  57. }
  58. }
  59. switchPin() async {
  60. _pin.value = !_pin.value;
  61. // Save everytime changed, as this func will not be called frequently
  62. await _savePin();
  63. }
  64. setPin(bool v) async {
  65. if (_pin.value != v) {
  66. _pin.value = v;
  67. // Save everytime changed, as this func will not be called frequently
  68. await _savePin();
  69. }
  70. }
  71. _savePin() async {
  72. bind.setLocalFlutterOption(
  73. k: kOptionRemoteMenubarState, v: jsonEncode({'pin': _pin.value}));
  74. }
  75. }
  76. class _ToolbarTheme {
  77. static const Color blueColor = MyTheme.button;
  78. static const Color hoverBlueColor = MyTheme.accent;
  79. static Color inactiveColor = Colors.grey[800]!;
  80. static Color hoverInactiveColor = Colors.grey[850]!;
  81. static const Color redColor = Colors.redAccent;
  82. static const Color hoverRedColor = Colors.red;
  83. // kMinInteractiveDimension
  84. static const double height = 20.0;
  85. static const double dividerHeight = 12.0;
  86. static const double buttonSize = 32;
  87. static const double buttonHMargin = 2;
  88. static const double buttonVMargin = 6;
  89. static const double iconRadius = 8;
  90. static const double elevation = 3;
  91. static double dividerSpaceToAction = isWindows ? 8 : 14;
  92. static double menuBorderRadius = isWindows ? 5.0 : 7.0;
  93. static EdgeInsets menuPadding = isWindows
  94. ? EdgeInsets.fromLTRB(4, 12, 4, 12)
  95. : EdgeInsets.fromLTRB(6, 14, 6, 14);
  96. static const double menuButtonBorderRadius = 3.0;
  97. static Color borderColor(BuildContext context) =>
  98. MyTheme.color(context).border3 ?? MyTheme.border;
  99. static Color? dividerColor(BuildContext context) =>
  100. MyTheme.color(context).divider;
  101. static MenuStyle defaultMenuStyle(BuildContext context) => MenuStyle(
  102. side: MaterialStateProperty.all(BorderSide(
  103. width: 1,
  104. color: borderColor(context),
  105. )),
  106. shape: MaterialStatePropertyAll(RoundedRectangleBorder(
  107. borderRadius:
  108. BorderRadius.circular(_ToolbarTheme.menuBorderRadius))),
  109. padding: MaterialStateProperty.all(_ToolbarTheme.menuPadding),
  110. );
  111. static final defaultMenuButtonStyle = ButtonStyle(
  112. backgroundColor: MaterialStatePropertyAll(Colors.transparent),
  113. padding: MaterialStatePropertyAll(EdgeInsets.zero),
  114. overlayColor: MaterialStatePropertyAll(Colors.transparent),
  115. );
  116. static Widget borderWrapper(
  117. BuildContext context, Widget child, BorderRadius borderRadius) {
  118. return Container(
  119. decoration: BoxDecoration(
  120. border: Border.all(
  121. color: borderColor(context),
  122. width: 1,
  123. ),
  124. borderRadius: borderRadius,
  125. ),
  126. child: child,
  127. );
  128. }
  129. }
  130. typedef DismissFunc = void Function();
  131. class RemoteMenuEntry {
  132. static MenuEntryRadios<String> viewStyle(
  133. String remoteId,
  134. FFI ffi,
  135. EdgeInsets padding, {
  136. DismissFunc? dismissFunc,
  137. DismissCallback? dismissCallback,
  138. RxString? rxViewStyle,
  139. }) {
  140. return MenuEntryRadios<String>(
  141. text: translate('Ratio'),
  142. optionsGetter: () => [
  143. MenuEntryRadioOption(
  144. text: translate('Scale original'),
  145. value: kRemoteViewStyleOriginal,
  146. dismissOnClicked: true,
  147. dismissCallback: dismissCallback,
  148. ),
  149. MenuEntryRadioOption(
  150. text: translate('Scale adaptive'),
  151. value: kRemoteViewStyleAdaptive,
  152. dismissOnClicked: true,
  153. dismissCallback: dismissCallback,
  154. ),
  155. ],
  156. curOptionGetter: () async {
  157. // null means peer id is not found, which there's no need to care about
  158. final viewStyle =
  159. await bind.sessionGetViewStyle(sessionId: ffi.sessionId) ?? '';
  160. if (rxViewStyle != null) {
  161. rxViewStyle.value = viewStyle;
  162. }
  163. return viewStyle;
  164. },
  165. optionSetter: (String oldValue, String newValue) async {
  166. await bind.sessionSetViewStyle(
  167. sessionId: ffi.sessionId, value: newValue);
  168. if (rxViewStyle != null) {
  169. rxViewStyle.value = newValue;
  170. }
  171. ffi.canvasModel.updateViewStyle();
  172. if (dismissFunc != null) {
  173. dismissFunc();
  174. }
  175. },
  176. padding: padding,
  177. dismissOnClicked: true,
  178. dismissCallback: dismissCallback,
  179. );
  180. }
  181. static MenuEntrySwitch2<String> showRemoteCursor(
  182. String remoteId,
  183. SessionID sessionId,
  184. EdgeInsets padding, {
  185. DismissFunc? dismissFunc,
  186. DismissCallback? dismissCallback,
  187. }) {
  188. final state = ShowRemoteCursorState.find(remoteId);
  189. final optKey = 'show-remote-cursor';
  190. return MenuEntrySwitch2<String>(
  191. switchType: SwitchType.scheckbox,
  192. text: translate('Show remote cursor'),
  193. getter: () {
  194. return state;
  195. },
  196. setter: (bool v) async {
  197. await bind.sessionToggleOption(sessionId: sessionId, value: optKey);
  198. state.value =
  199. bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: optKey);
  200. if (dismissFunc != null) {
  201. dismissFunc();
  202. }
  203. },
  204. padding: padding,
  205. dismissOnClicked: true,
  206. dismissCallback: dismissCallback,
  207. );
  208. }
  209. static MenuEntrySwitch<String> disableClipboard(
  210. SessionID sessionId,
  211. EdgeInsets? padding, {
  212. DismissFunc? dismissFunc,
  213. DismissCallback? dismissCallback,
  214. }) {
  215. return createSwitchMenuEntry(
  216. sessionId,
  217. 'Disable clipboard',
  218. 'disable-clipboard',
  219. padding,
  220. true,
  221. dismissCallback: dismissCallback,
  222. );
  223. }
  224. static MenuEntrySwitch<String> createSwitchMenuEntry(
  225. SessionID sessionId,
  226. String text,
  227. String option,
  228. EdgeInsets? padding,
  229. bool dismissOnClicked, {
  230. DismissFunc? dismissFunc,
  231. DismissCallback? dismissCallback,
  232. }) {
  233. return MenuEntrySwitch<String>(
  234. switchType: SwitchType.scheckbox,
  235. text: translate(text),
  236. getter: () async {
  237. return bind.sessionGetToggleOptionSync(
  238. sessionId: sessionId, arg: option);
  239. },
  240. setter: (bool v) async {
  241. await bind.sessionToggleOption(sessionId: sessionId, value: option);
  242. if (dismissFunc != null) {
  243. dismissFunc();
  244. }
  245. },
  246. padding: padding,
  247. dismissOnClicked: dismissOnClicked,
  248. dismissCallback: dismissCallback,
  249. );
  250. }
  251. static MenuEntryButton<String> insertLock(
  252. SessionID sessionId,
  253. EdgeInsets? padding, {
  254. DismissFunc? dismissFunc,
  255. DismissCallback? dismissCallback,
  256. }) {
  257. return MenuEntryButton<String>(
  258. childBuilder: (TextStyle? style) => Text(
  259. translate('Insert Lock'),
  260. style: style,
  261. ),
  262. proc: () {
  263. bind.sessionLockScreen(sessionId: sessionId);
  264. if (dismissFunc != null) {
  265. dismissFunc();
  266. }
  267. },
  268. padding: padding,
  269. dismissOnClicked: true,
  270. dismissCallback: dismissCallback,
  271. );
  272. }
  273. static insertCtrlAltDel(
  274. SessionID sessionId,
  275. EdgeInsets? padding, {
  276. DismissFunc? dismissFunc,
  277. DismissCallback? dismissCallback,
  278. }) {
  279. return MenuEntryButton<String>(
  280. childBuilder: (TextStyle? style) => Text(
  281. translate("Insert Ctrl + Alt + Del"),
  282. style: style,
  283. ),
  284. proc: () {
  285. bind.sessionCtrlAltDel(sessionId: sessionId);
  286. if (dismissFunc != null) {
  287. dismissFunc();
  288. }
  289. },
  290. padding: padding,
  291. dismissOnClicked: true,
  292. dismissCallback: dismissCallback,
  293. );
  294. }
  295. }
  296. class RemoteToolbar extends StatefulWidget {
  297. final String id;
  298. final FFI ffi;
  299. final ToolbarState state;
  300. final Function(int, Function(bool)) onEnterOrLeaveImageSetter;
  301. final Function(int) onEnterOrLeaveImageCleaner;
  302. final Function(VoidCallback) setRemoteState;
  303. RemoteToolbar({
  304. Key? key,
  305. required this.id,
  306. required this.ffi,
  307. required this.state,
  308. required this.onEnterOrLeaveImageSetter,
  309. required this.onEnterOrLeaveImageCleaner,
  310. required this.setRemoteState,
  311. }) : super(key: key);
  312. @override
  313. State<RemoteToolbar> createState() => _RemoteToolbarState();
  314. }
  315. class _RemoteToolbarState extends State<RemoteToolbar> {
  316. late Debouncer<int> _debouncerHide;
  317. bool _isCursorOverImage = false;
  318. final _fractionX = 0.5.obs;
  319. final _dragging = false.obs;
  320. int get windowId => stateGlobal.windowId;
  321. void _setFullscreen(bool v) {
  322. stateGlobal.setFullscreen(v);
  323. // stateGlobal.fullscreen is RxBool now, no need to call setState.
  324. // setState(() {});
  325. }
  326. RxBool get show => widget.state.show;
  327. bool get pin => widget.state.pin;
  328. PeerInfo get pi => widget.ffi.ffiModel.pi;
  329. FfiModel get ffiModel => widget.ffi.ffiModel;
  330. triggerAutoHide() => _debouncerHide.value = _debouncerHide.value + 1;
  331. void _minimize() async =>
  332. await WindowController.fromWindowId(windowId).minimize();
  333. @override
  334. initState() {
  335. super.initState();
  336. WidgetsBinding.instance.addPostFrameCallback((_) async {
  337. _fractionX.value = double.tryParse(await bind.sessionGetOption(
  338. sessionId: widget.ffi.sessionId,
  339. arg: 'remote-menubar-drag-x') ??
  340. '0.5') ??
  341. 0.5;
  342. });
  343. _debouncerHide = Debouncer<int>(
  344. Duration(milliseconds: 5000),
  345. onChanged: _debouncerHideProc,
  346. initialValue: 0,
  347. );
  348. widget.onEnterOrLeaveImageSetter(identityHashCode(this), (enter) {
  349. if (enter) {
  350. triggerAutoHide();
  351. _isCursorOverImage = true;
  352. } else {
  353. _isCursorOverImage = false;
  354. }
  355. });
  356. }
  357. _debouncerHideProc(int v) {
  358. if (!pin && show.isTrue && _isCursorOverImage && _dragging.isFalse) {
  359. show.value = false;
  360. }
  361. }
  362. @override
  363. dispose() {
  364. super.dispose();
  365. widget.onEnterOrLeaveImageCleaner(identityHashCode(this));
  366. }
  367. @override
  368. Widget build(BuildContext context) {
  369. return Align(
  370. alignment: Alignment.topCenter,
  371. child: Obx(() => show.value
  372. ? _buildToolbar(context)
  373. : _buildDraggableShowHide(context)),
  374. );
  375. }
  376. Widget _buildDraggableShowHide(BuildContext context) {
  377. return Obx(() {
  378. if (show.isTrue && _dragging.isFalse) {
  379. triggerAutoHide();
  380. }
  381. final borderRadius = BorderRadius.vertical(
  382. bottom: Radius.circular(5),
  383. );
  384. return Align(
  385. alignment: FractionalOffset(_fractionX.value, 0),
  386. child: Offstage(
  387. offstage: _dragging.isTrue,
  388. child: Material(
  389. elevation: _ToolbarTheme.elevation,
  390. shadowColor: MyTheme.color(context).shadow,
  391. borderRadius: borderRadius,
  392. child: _DraggableShowHide(
  393. id: widget.id,
  394. sessionId: widget.ffi.sessionId,
  395. dragging: _dragging,
  396. fractionX: _fractionX,
  397. toolbarState: widget.state,
  398. setFullscreen: _setFullscreen,
  399. setMinimize: _minimize,
  400. borderRadius: borderRadius,
  401. ),
  402. ),
  403. ),
  404. );
  405. });
  406. }
  407. Widget _buildToolbar(BuildContext context) {
  408. final List<Widget> toolbarItems = [];
  409. toolbarItems.add(_PinMenu(state: widget.state));
  410. if (!isWebDesktop) {
  411. toolbarItems.add(_MobileActionMenu(ffi: widget.ffi));
  412. }
  413. toolbarItems.add(Obx(() {
  414. if (PrivacyModeState.find(widget.id).isEmpty &&
  415. pi.displaysCount.value > 1) {
  416. return _MonitorMenu(
  417. id: widget.id,
  418. ffi: widget.ffi,
  419. setRemoteState: widget.setRemoteState);
  420. } else {
  421. return Offstage();
  422. }
  423. }));
  424. toolbarItems
  425. .add(_ControlMenu(id: widget.id, ffi: widget.ffi, state: widget.state));
  426. toolbarItems.add(_DisplayMenu(
  427. id: widget.id,
  428. ffi: widget.ffi,
  429. state: widget.state,
  430. setFullscreen: _setFullscreen,
  431. ));
  432. // Do not show keyboard for camera connection type.
  433. if (widget.ffi.connType == ConnType.defaultConn) {
  434. toolbarItems.add(_KeyboardMenu(id: widget.id, ffi: widget.ffi));
  435. }
  436. toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi));
  437. if (!isWeb) {
  438. toolbarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi));
  439. }
  440. if (!isWeb) toolbarItems.add(_RecordMenu());
  441. toolbarItems.add(_CloseMenu(id: widget.id, ffi: widget.ffi));
  442. final toolbarBorderRadius = BorderRadius.all(Radius.circular(4.0));
  443. return Column(
  444. mainAxisSize: MainAxisSize.min,
  445. children: [
  446. Material(
  447. elevation: _ToolbarTheme.elevation,
  448. shadowColor: MyTheme.color(context).shadow,
  449. borderRadius: toolbarBorderRadius,
  450. color: Theme.of(context)
  451. .menuBarTheme
  452. .style
  453. ?.backgroundColor
  454. ?.resolve(MaterialState.values.toSet()),
  455. child: SingleChildScrollView(
  456. scrollDirection: Axis.horizontal,
  457. child: Theme(
  458. data: themeData(),
  459. child: _ToolbarTheme.borderWrapper(
  460. context,
  461. Row(
  462. children: [
  463. SizedBox(width: _ToolbarTheme.buttonHMargin * 2),
  464. ...toolbarItems,
  465. SizedBox(width: _ToolbarTheme.buttonHMargin * 2)
  466. ],
  467. ),
  468. toolbarBorderRadius),
  469. ),
  470. ),
  471. ),
  472. _buildDraggableShowHide(context),
  473. ],
  474. );
  475. }
  476. ThemeData themeData() {
  477. return Theme.of(context).copyWith(
  478. menuButtonTheme: MenuButtonThemeData(
  479. style: ButtonStyle(
  480. minimumSize: MaterialStatePropertyAll(Size(64, 32)),
  481. textStyle: MaterialStatePropertyAll(
  482. TextStyle(fontWeight: FontWeight.normal),
  483. ),
  484. shape: MaterialStatePropertyAll(RoundedRectangleBorder(
  485. borderRadius:
  486. BorderRadius.circular(_ToolbarTheme.menuButtonBorderRadius))),
  487. ),
  488. ),
  489. dividerTheme: DividerThemeData(
  490. space: _ToolbarTheme.dividerSpaceToAction,
  491. color: _ToolbarTheme.dividerColor(context),
  492. ),
  493. menuBarTheme: MenuBarThemeData(
  494. style: MenuStyle(
  495. padding: MaterialStatePropertyAll(EdgeInsets.zero),
  496. elevation: MaterialStatePropertyAll(0),
  497. shape: MaterialStatePropertyAll(BeveledRectangleBorder()),
  498. ).copyWith(
  499. backgroundColor:
  500. Theme.of(context).menuBarTheme.style?.backgroundColor)),
  501. );
  502. }
  503. }
  504. class _PinMenu extends StatelessWidget {
  505. final ToolbarState state;
  506. const _PinMenu({Key? key, required this.state}) : super(key: key);
  507. @override
  508. Widget build(BuildContext context) {
  509. return Obx(
  510. () => _IconMenuButton(
  511. assetName: state.pin ? "assets/pinned.svg" : "assets/unpinned.svg",
  512. tooltip: state.pin ? 'Unpin Toolbar' : 'Pin Toolbar',
  513. onPressed: state.switchPin,
  514. color:
  515. state.pin ? _ToolbarTheme.blueColor : _ToolbarTheme.inactiveColor,
  516. hoverColor: state.pin
  517. ? _ToolbarTheme.hoverBlueColor
  518. : _ToolbarTheme.hoverInactiveColor,
  519. ),
  520. );
  521. }
  522. }
  523. class _MobileActionMenu extends StatelessWidget {
  524. final FFI ffi;
  525. const _MobileActionMenu({Key? key, required this.ffi}) : super(key: key);
  526. @override
  527. Widget build(BuildContext context) {
  528. if (!ffi.ffiModel.isPeerAndroid) return Offstage();
  529. return Obx(() => _IconMenuButton(
  530. assetName: 'assets/actions_mobile.svg',
  531. tooltip: 'Mobile Actions',
  532. onPressed: () => ffi.dialogManager.setMobileActionsOverlayVisible(
  533. !ffi.dialogManager.mobileActionsOverlayVisible.value),
  534. color: ffi.dialogManager.mobileActionsOverlayVisible.isTrue
  535. ? _ToolbarTheme.blueColor
  536. : _ToolbarTheme.inactiveColor,
  537. hoverColor: ffi.dialogManager.mobileActionsOverlayVisible.isTrue
  538. ? _ToolbarTheme.hoverBlueColor
  539. : _ToolbarTheme.hoverInactiveColor,
  540. ));
  541. }
  542. }
  543. class _MonitorMenu extends StatelessWidget {
  544. final String id;
  545. final FFI ffi;
  546. final Function(VoidCallback) setRemoteState;
  547. const _MonitorMenu({
  548. Key? key,
  549. required this.id,
  550. required this.ffi,
  551. required this.setRemoteState,
  552. }) : super(key: key);
  553. bool get showMonitorsToolbar =>
  554. bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y';
  555. bool get supportIndividualWindows =>
  556. !isWeb && ffi.ffiModel.pi.isSupportMultiDisplay;
  557. @override
  558. Widget build(BuildContext context) => showMonitorsToolbar
  559. ? buildMultiMonitorMenu(context)
  560. : Obx(() => buildMonitorMenu(context));
  561. Widget buildMonitorMenu(BuildContext context) {
  562. final width = SimpleWrapper<double>(0);
  563. final monitorsIcon =
  564. globalMonitorsWidget(width, Colors.white, Colors.black38);
  565. return _IconSubmenuButton(
  566. tooltip: 'Select Monitor',
  567. icon: monitorsIcon,
  568. ffi: ffi,
  569. width: width.value,
  570. color: _ToolbarTheme.blueColor,
  571. hoverColor: _ToolbarTheme.hoverBlueColor,
  572. menuStyle: MenuStyle(
  573. padding:
  574. MaterialStatePropertyAll(EdgeInsets.symmetric(horizontal: 6))),
  575. menuChildrenGetter: () => [buildMonitorSubmenuWidget(context)]);
  576. }
  577. Widget buildMultiMonitorMenu(BuildContext context) {
  578. return Row(children: buildMonitorList(context, true));
  579. }
  580. Widget buildMonitorSubmenuWidget(BuildContext context) {
  581. return Column(
  582. mainAxisSize: MainAxisSize.min,
  583. children: [
  584. Row(children: buildMonitorList(context, false)),
  585. supportIndividualWindows ? Divider() : Offstage(),
  586. supportIndividualWindows ? chooseDisplayBehavior() : Offstage(),
  587. ],
  588. );
  589. }
  590. Widget chooseDisplayBehavior() {
  591. final value =
  592. bind.sessionGetDisplaysAsIndividualWindows(sessionId: ffi.sessionId) ==
  593. 'Y';
  594. return CkbMenuButton(
  595. value: value,
  596. onChanged: (value) async {
  597. if (value == null) return;
  598. await bind.sessionSetDisplaysAsIndividualWindows(
  599. sessionId: ffi.sessionId, value: value ? 'Y' : 'N');
  600. },
  601. ffi: ffi,
  602. child: Text(translate('Show displays as individual windows')));
  603. }
  604. buildOneMonitorButton(i, curDisplay) => Text(
  605. '${i + 1}',
  606. style: TextStyle(
  607. color: i == curDisplay
  608. ? _ToolbarTheme.blueColor
  609. : _ToolbarTheme.inactiveColor,
  610. fontSize: 12,
  611. fontWeight: FontWeight.bold,
  612. ),
  613. );
  614. List<Widget> buildMonitorList(BuildContext context, bool isMulti) {
  615. final List<Widget> monitorList = [];
  616. final pi = ffi.ffiModel.pi;
  617. buildMonitorButton(int i) => Obx(() {
  618. RxInt display = CurrentDisplayState.find(id);
  619. final isAllMonitors = i == kAllDisplayValue;
  620. final width = SimpleWrapper<double>(0);
  621. Widget? monitorsIcon;
  622. if (isAllMonitors) {
  623. monitorsIcon = globalMonitorsWidget(
  624. width, Colors.white, _ToolbarTheme.blueColor);
  625. }
  626. return _IconMenuButton(
  627. tooltip: isMulti
  628. ? ''
  629. : isAllMonitors
  630. ? 'all monitors'
  631. : '#${i + 1} monitor',
  632. hMargin: isMulti ? null : 6,
  633. vMargin: isMulti ? null : 12,
  634. topLevel: false,
  635. color: i == display.value
  636. ? _ToolbarTheme.blueColor
  637. : _ToolbarTheme.inactiveColor,
  638. hoverColor: i == display.value
  639. ? _ToolbarTheme.hoverBlueColor
  640. : _ToolbarTheme.hoverInactiveColor,
  641. width: isAllMonitors ? width.value : null,
  642. icon: isAllMonitors
  643. ? monitorsIcon
  644. : Container(
  645. alignment: AlignmentDirectional.center,
  646. constraints:
  647. const BoxConstraints(minHeight: _ToolbarTheme.height),
  648. child: Stack(
  649. alignment: Alignment.center,
  650. children: [
  651. SvgPicture.asset(
  652. "assets/screen.svg",
  653. colorFilter:
  654. ColorFilter.mode(Colors.white, BlendMode.srcIn),
  655. ),
  656. Obx(() => buildOneMonitorButton(i, display.value)),
  657. ],
  658. ),
  659. ),
  660. onPressed: () => onPressed(i, pi, isMulti),
  661. );
  662. });
  663. for (int i = 0; i < pi.displays.length; i++) {
  664. monitorList.add(buildMonitorButton(i));
  665. }
  666. if (supportIndividualWindows && pi.displays.length > 1) {
  667. monitorList.add(buildMonitorButton(kAllDisplayValue));
  668. }
  669. return monitorList;
  670. }
  671. globalMonitorsWidget(
  672. SimpleWrapper<double> width, Color activeTextColor, Color activeBgColor) {
  673. getMonitors() {
  674. final pi = ffi.ffiModel.pi;
  675. RxInt display = CurrentDisplayState.find(id);
  676. final rect = ffi.ffiModel.globalDisplaysRect();
  677. if (rect == null) {
  678. return Offstage();
  679. }
  680. final scale = _ToolbarTheme.buttonSize / rect.height * 0.75;
  681. final startY = (_ToolbarTheme.buttonSize - rect.height * scale) * 0.5;
  682. final startX = startY;
  683. final children = <Widget>[];
  684. for (var i = 0; i < pi.displays.length; i++) {
  685. final d = pi.displays[i];
  686. double s = d.scale;
  687. int dWidth = d.width.toDouble() ~/ s;
  688. int dHeight = d.height.toDouble() ~/ s;
  689. final fontSize = (dWidth * scale < dHeight * scale
  690. ? dWidth * scale
  691. : dHeight * scale) *
  692. 0.65;
  693. children.add(Positioned(
  694. left: (d.x - rect.left) * scale + startX,
  695. top: (d.y - rect.top) * scale + startY,
  696. width: dWidth * scale,
  697. height: dHeight * scale,
  698. child: Container(
  699. decoration: BoxDecoration(
  700. border: Border.all(
  701. color: Colors.grey,
  702. width: 1.0,
  703. ),
  704. color: display.value == i ? activeBgColor : Colors.white,
  705. ),
  706. child: Center(
  707. child: Text(
  708. '${i + 1}',
  709. style: TextStyle(
  710. color: display.value == i
  711. ? activeTextColor
  712. : _ToolbarTheme.inactiveColor,
  713. fontSize: fontSize,
  714. fontWeight: FontWeight.bold,
  715. ),
  716. )),
  717. ),
  718. ));
  719. }
  720. width.value = rect.width * scale + startX * 2;
  721. return SizedBox(
  722. width: width.value,
  723. height: rect.height * scale + startY * 2,
  724. child: Stack(
  725. children: children,
  726. ),
  727. );
  728. }
  729. return Stack(
  730. alignment: Alignment.center,
  731. children: [
  732. SizedBox(height: _ToolbarTheme.buttonSize),
  733. getMonitors(),
  734. ],
  735. );
  736. }
  737. onPressed(int i, PeerInfo pi, bool isMulti) {
  738. if (!isMulti) {
  739. // If show monitors in toolbar(`buildMultiMonitorMenu()`), then the menu will dismiss automatically.
  740. _menuDismissCallback(ffi);
  741. }
  742. RxInt display = CurrentDisplayState.find(id);
  743. if (display.value != i) {
  744. final isChooseDisplayToOpenInNewWindow = pi.isSupportMultiDisplay &&
  745. bind.sessionGetDisplaysAsIndividualWindows(
  746. sessionId: ffi.sessionId) ==
  747. 'Y';
  748. if (isChooseDisplayToOpenInNewWindow) {
  749. openMonitorInNewTabOrWindow(i, ffi.id, pi);
  750. } else {
  751. openMonitorInTheSameTab(i, ffi, pi, updateCursorPos: !isMulti);
  752. }
  753. }
  754. }
  755. }
  756. class _ControlMenu extends StatelessWidget {
  757. final String id;
  758. final FFI ffi;
  759. final ToolbarState state;
  760. _ControlMenu(
  761. {Key? key, required this.id, required this.ffi, required this.state})
  762. : super(key: key);
  763. @override
  764. Widget build(BuildContext context) {
  765. return _IconSubmenuButton(
  766. tooltip: 'Control Actions',
  767. svg: "assets/actions.svg",
  768. color: _ToolbarTheme.blueColor,
  769. hoverColor: _ToolbarTheme.hoverBlueColor,
  770. ffi: ffi,
  771. menuChildrenGetter: () => toolbarControls(context, id, ffi).map((e) {
  772. if (e.divider) {
  773. return Divider();
  774. } else {
  775. return MenuButton(
  776. child: e.child,
  777. onPressed: e.onPressed,
  778. ffi: ffi,
  779. trailingIcon: e.trailingIcon);
  780. }
  781. }).toList());
  782. }
  783. }
  784. class ScreenAdjustor {
  785. final String id;
  786. final FFI ffi;
  787. final VoidCallback cbExitFullscreen;
  788. window_size.Screen? _screen;
  789. ScreenAdjustor({
  790. required this.id,
  791. required this.ffi,
  792. required this.cbExitFullscreen,
  793. });
  794. bool get isFullscreen => stateGlobal.fullscreen.isTrue;
  795. int get windowId => stateGlobal.windowId;
  796. adjustWindow(BuildContext context) {
  797. return futureBuilder(
  798. future: isWindowCanBeAdjusted(),
  799. hasData: (data) {
  800. final visible = data as bool;
  801. if (!visible) return Offstage();
  802. return Column(
  803. children: [
  804. MenuButton(
  805. child: Text(translate('Adjust Window')),
  806. onPressed: () => doAdjustWindow(context),
  807. ffi: ffi),
  808. Divider(),
  809. ],
  810. );
  811. });
  812. }
  813. doAdjustWindow(BuildContext context) async {
  814. await updateScreen();
  815. if (_screen != null) {
  816. cbExitFullscreen();
  817. double scale = _screen!.scaleFactor;
  818. final wndRect = await WindowController.fromWindowId(windowId).getFrame();
  819. final mediaSize = MediaQueryData.fromView(View.of(context)).size;
  820. // On windows, wndRect is equal to GetWindowRect and mediaSize is equal to GetClientRect.
  821. // https://stackoverflow.com/a/7561083
  822. double magicWidth =
  823. wndRect.right - wndRect.left - mediaSize.width * scale;
  824. double magicHeight =
  825. wndRect.bottom - wndRect.top - mediaSize.height * scale;
  826. final canvasModel = ffi.canvasModel;
  827. final width = (canvasModel.getDisplayWidth() * canvasModel.scale +
  828. CanvasModel.leftToEdge +
  829. CanvasModel.rightToEdge) *
  830. scale +
  831. magicWidth;
  832. final height = (canvasModel.getDisplayHeight() * canvasModel.scale +
  833. CanvasModel.topToEdge +
  834. CanvasModel.bottomToEdge) *
  835. scale +
  836. magicHeight;
  837. double left = wndRect.left + (wndRect.width - width) / 2;
  838. double top = wndRect.top + (wndRect.height - height) / 2;
  839. Rect frameRect = _screen!.frame;
  840. if (!isFullscreen) {
  841. frameRect = _screen!.visibleFrame;
  842. }
  843. if (left < frameRect.left) {
  844. left = frameRect.left;
  845. }
  846. if (top < frameRect.top) {
  847. top = frameRect.top;
  848. }
  849. if ((left + width) > frameRect.right) {
  850. left = frameRect.right - width;
  851. }
  852. if ((top + height) > frameRect.bottom) {
  853. top = frameRect.bottom - height;
  854. }
  855. await WindowController.fromWindowId(windowId)
  856. .setFrame(Rect.fromLTWH(left, top, width, height));
  857. stateGlobal.setMaximized(false);
  858. }
  859. }
  860. updateScreen() async {
  861. final String info =
  862. isWeb ? screenInfo : await _getScreenInfoDesktop() ?? '';
  863. if (info.isEmpty) {
  864. _screen = null;
  865. } else {
  866. final screenMap = jsonDecode(info);
  867. _screen = window_size.Screen(
  868. Rect.fromLTRB(screenMap['frame']['l'], screenMap['frame']['t'],
  869. screenMap['frame']['r'], screenMap['frame']['b']),
  870. Rect.fromLTRB(
  871. screenMap['visibleFrame']['l'],
  872. screenMap['visibleFrame']['t'],
  873. screenMap['visibleFrame']['r'],
  874. screenMap['visibleFrame']['b']),
  875. screenMap['scaleFactor']);
  876. }
  877. }
  878. _getScreenInfoDesktop() async {
  879. final v = await rustDeskWinManager.call(
  880. WindowType.Main, kWindowGetWindowInfo, '');
  881. return v.result;
  882. }
  883. Future<bool> isWindowCanBeAdjusted() async {
  884. final viewStyle =
  885. await bind.sessionGetViewStyle(sessionId: ffi.sessionId) ?? '';
  886. if (viewStyle != kRemoteViewStyleOriginal) {
  887. return false;
  888. }
  889. if (!isWeb) {
  890. final remoteCount = RemoteCountState.find().value;
  891. if (remoteCount != 1) {
  892. return false;
  893. }
  894. }
  895. if (_screen == null) {
  896. return false;
  897. }
  898. final scale = kIgnoreDpi ? 1.0 : _screen!.scaleFactor;
  899. double selfWidth = _screen!.visibleFrame.width;
  900. double selfHeight = _screen!.visibleFrame.height;
  901. if (isFullscreen) {
  902. selfWidth = _screen!.frame.width;
  903. selfHeight = _screen!.frame.height;
  904. }
  905. final canvasModel = ffi.canvasModel;
  906. final displayWidth = canvasModel.getDisplayWidth();
  907. final displayHeight = canvasModel.getDisplayHeight();
  908. final requiredWidth =
  909. CanvasModel.leftToEdge + displayWidth + CanvasModel.rightToEdge;
  910. final requiredHeight =
  911. CanvasModel.topToEdge + displayHeight + CanvasModel.bottomToEdge;
  912. return selfWidth > (requiredWidth * scale) &&
  913. selfHeight > (requiredHeight * scale);
  914. }
  915. }
  916. class _DisplayMenu extends StatefulWidget {
  917. final String id;
  918. final FFI ffi;
  919. final ToolbarState state;
  920. final Function(bool) setFullscreen;
  921. final Widget pluginItem;
  922. _DisplayMenu(
  923. {Key? key,
  924. required this.id,
  925. required this.ffi,
  926. required this.state,
  927. required this.setFullscreen})
  928. : pluginItem = LocationItem.createLocationItem(
  929. id,
  930. ffi,
  931. kLocationClientRemoteToolbarDisplay,
  932. true,
  933. ),
  934. super(key: key);
  935. @override
  936. State<_DisplayMenu> createState() => _DisplayMenuState();
  937. }
  938. class _DisplayMenuState extends State<_DisplayMenu> {
  939. late final ScreenAdjustor _screenAdjustor = ScreenAdjustor(
  940. id: widget.id,
  941. ffi: widget.ffi,
  942. cbExitFullscreen: () => widget.setFullscreen(false),
  943. );
  944. int get windowId => stateGlobal.windowId;
  945. Map<String, bool> get perms => widget.ffi.ffiModel.permissions;
  946. PeerInfo get pi => widget.ffi.ffiModel.pi;
  947. FfiModel get ffiModel => widget.ffi.ffiModel;
  948. FFI get ffi => widget.ffi;
  949. String get id => widget.id;
  950. @override
  951. Widget build(BuildContext context) {
  952. _screenAdjustor.updateScreen();
  953. menuChildrenGetter() {
  954. final menuChildren = <Widget>[
  955. _screenAdjustor.adjustWindow(context),
  956. viewStyle(),
  957. scrollStyle(),
  958. imageQuality(),
  959. codec(),
  960. if (ffi.connType == ConnType.defaultConn)
  961. _ResolutionsMenu(
  962. id: widget.id,
  963. ffi: widget.ffi,
  964. screenAdjustor: _screenAdjustor,
  965. ),
  966. if (showVirtualDisplayMenu(ffi) && ffi.connType == ConnType.defaultConn)
  967. _SubmenuButton(
  968. ffi: widget.ffi,
  969. menuChildren: getVirtualDisplayMenuChildren(ffi, id, null),
  970. child: Text(translate("Virtual display")),
  971. ),
  972. if (ffi.connType == ConnType.defaultConn) cursorToggles(),
  973. Divider(),
  974. toggles(),
  975. ];
  976. // privacy mode
  977. if (ffi.connType == ConnType.defaultConn &&
  978. ffiModel.keyboard &&
  979. pi.features.privacyMode) {
  980. final privacyModeState = PrivacyModeState.find(id);
  981. final privacyModeList =
  982. toolbarPrivacyMode(privacyModeState, context, id, ffi);
  983. if (privacyModeList.length == 1) {
  984. menuChildren.add(CkbMenuButton(
  985. value: privacyModeList[0].value,
  986. onChanged: privacyModeList[0].onChanged,
  987. child: privacyModeList[0].child,
  988. ffi: ffi));
  989. } else if (privacyModeList.length > 1) {
  990. menuChildren.addAll([
  991. Divider(),
  992. _SubmenuButton(
  993. ffi: widget.ffi,
  994. child: Text(translate('Privacy mode')),
  995. menuChildren: privacyModeList
  996. .map((e) => CkbMenuButton(
  997. value: e.value,
  998. onChanged: e.onChanged,
  999. child: e.child,
  1000. ffi: ffi))
  1001. .toList()),
  1002. ]);
  1003. }
  1004. }
  1005. if (ffi.connType == ConnType.defaultConn) {
  1006. menuChildren.add(widget.pluginItem);
  1007. }
  1008. return menuChildren;
  1009. }
  1010. return _IconSubmenuButton(
  1011. tooltip: 'Display Settings',
  1012. svg: "assets/display.svg",
  1013. ffi: widget.ffi,
  1014. color: _ToolbarTheme.blueColor,
  1015. hoverColor: _ToolbarTheme.hoverBlueColor,
  1016. menuChildrenGetter: menuChildrenGetter,
  1017. );
  1018. }
  1019. viewStyle() {
  1020. return futureBuilder(
  1021. future: toolbarViewStyle(context, widget.id, widget.ffi),
  1022. hasData: (data) {
  1023. final v = data as List<TRadioMenu<String>>;
  1024. return Column(children: [
  1025. ...v
  1026. .map((e) => RdoMenuButton<String>(
  1027. value: e.value,
  1028. groupValue: e.groupValue,
  1029. onChanged: e.onChanged,
  1030. child: e.child,
  1031. ffi: ffi))
  1032. .toList(),
  1033. Divider(),
  1034. ]);
  1035. });
  1036. }
  1037. scrollStyle() {
  1038. return futureBuilder(future: () async {
  1039. final viewStyle =
  1040. await bind.sessionGetViewStyle(sessionId: ffi.sessionId) ?? '';
  1041. final visible = viewStyle == kRemoteViewStyleOriginal;
  1042. final scrollStyle =
  1043. await bind.sessionGetScrollStyle(sessionId: ffi.sessionId) ?? '';
  1044. return {'visible': visible, 'scrollStyle': scrollStyle};
  1045. }(), hasData: (data) {
  1046. final visible = data['visible'] as bool;
  1047. if (!visible) return Offstage();
  1048. final groupValue = data['scrollStyle'] as String;
  1049. onChange(String? value) async {
  1050. if (value == null) return;
  1051. await bind.sessionSetScrollStyle(
  1052. sessionId: ffi.sessionId, value: value);
  1053. widget.ffi.canvasModel.updateScrollStyle();
  1054. }
  1055. final enabled = widget.ffi.canvasModel.imageOverflow.value;
  1056. return Column(children: [
  1057. RdoMenuButton<String>(
  1058. child: Text(translate('ScrollAuto')),
  1059. value: kRemoteScrollStyleAuto,
  1060. groupValue: groupValue,
  1061. onChanged: enabled ? (value) => onChange(value) : null,
  1062. ffi: widget.ffi,
  1063. ),
  1064. RdoMenuButton<String>(
  1065. child: Text(translate('Scrollbar')),
  1066. value: kRemoteScrollStyleBar,
  1067. groupValue: groupValue,
  1068. onChanged: enabled ? (value) => onChange(value) : null,
  1069. ffi: widget.ffi,
  1070. ),
  1071. Divider(),
  1072. ]);
  1073. });
  1074. }
  1075. imageQuality() {
  1076. return futureBuilder(
  1077. future: toolbarImageQuality(context, widget.id, widget.ffi),
  1078. hasData: (data) {
  1079. final v = data as List<TRadioMenu<String>>;
  1080. return _SubmenuButton(
  1081. ffi: widget.ffi,
  1082. child: Text(translate('Image Quality')),
  1083. menuChildren: v
  1084. .map((e) => RdoMenuButton<String>(
  1085. value: e.value,
  1086. groupValue: e.groupValue,
  1087. onChanged: e.onChanged,
  1088. child: e.child,
  1089. ffi: ffi))
  1090. .toList(),
  1091. );
  1092. });
  1093. }
  1094. codec() {
  1095. return futureBuilder(
  1096. future: toolbarCodec(context, id, ffi),
  1097. hasData: (data) {
  1098. final v = data as List<TRadioMenu<String>>;
  1099. if (v.isEmpty) return Offstage();
  1100. return _SubmenuButton(
  1101. ffi: widget.ffi,
  1102. child: Text(translate('Codec')),
  1103. menuChildren: v
  1104. .map((e) => RdoMenuButton(
  1105. value: e.value,
  1106. groupValue: e.groupValue,
  1107. onChanged: e.onChanged,
  1108. child: e.child,
  1109. ffi: ffi))
  1110. .toList());
  1111. });
  1112. }
  1113. cursorToggles() {
  1114. return futureBuilder(
  1115. future: toolbarCursor(context, id, ffi),
  1116. hasData: (data) {
  1117. final v = data as List<TToggleMenu>;
  1118. if (v.isEmpty) return Offstage();
  1119. return Column(children: [
  1120. Divider(),
  1121. ...v
  1122. .map((e) => CkbMenuButton(
  1123. value: e.value,
  1124. onChanged: e.onChanged,
  1125. child: e.child,
  1126. ffi: ffi))
  1127. .toList(),
  1128. ]);
  1129. });
  1130. }
  1131. toggles() {
  1132. return futureBuilder(
  1133. future: toolbarDisplayToggle(context, id, ffi),
  1134. hasData: (data) {
  1135. final v = data as List<TToggleMenu>;
  1136. if (v.isEmpty) return Offstage();
  1137. return Column(
  1138. children: v
  1139. .map((e) => CkbMenuButton(
  1140. value: e.value,
  1141. onChanged: e.onChanged,
  1142. child: e.child,
  1143. ffi: ffi))
  1144. .toList());
  1145. });
  1146. }
  1147. }
  1148. class _ResolutionsMenu extends StatefulWidget {
  1149. final String id;
  1150. final FFI ffi;
  1151. final ScreenAdjustor screenAdjustor;
  1152. _ResolutionsMenu({
  1153. Key? key,
  1154. required this.id,
  1155. required this.ffi,
  1156. required this.screenAdjustor,
  1157. }) : super(key: key);
  1158. @override
  1159. State<_ResolutionsMenu> createState() => _ResolutionsMenuState();
  1160. }
  1161. const double _kCustomResolutionEditingWidth = 42;
  1162. const _kCustomResolutionValue = 'custom';
  1163. class _ResolutionsMenuState extends State<_ResolutionsMenu> {
  1164. String _groupValue = '';
  1165. Resolution? _localResolution;
  1166. late final TextEditingController _customWidth =
  1167. TextEditingController(text: rect?.width.toInt().toString() ?? '');
  1168. late final TextEditingController _customHeight =
  1169. TextEditingController(text: rect?.height.toInt().toString() ?? '');
  1170. FFI get ffi => widget.ffi;
  1171. PeerInfo get pi => widget.ffi.ffiModel.pi;
  1172. FfiModel get ffiModel => widget.ffi.ffiModel;
  1173. Rect? get rect => scaledRect();
  1174. List<Resolution> get resolutions => pi.resolutions;
  1175. bool get isWayland => bind.mainCurrentIsWayland();
  1176. @override
  1177. void initState() {
  1178. super.initState();
  1179. WidgetsBinding.instance.addPostFrameCallback((_) {
  1180. _getLocalResolutionWayland();
  1181. });
  1182. }
  1183. Rect? scaledRect() {
  1184. final scale = pi.scaleOfDisplay(pi.currentDisplay);
  1185. final rect = ffiModel.rect;
  1186. if (rect == null) {
  1187. return null;
  1188. }
  1189. return Rect.fromLTWH(
  1190. rect.left,
  1191. rect.top,
  1192. rect.width / scale,
  1193. rect.height / scale,
  1194. );
  1195. }
  1196. @override
  1197. Widget build(BuildContext context) {
  1198. final isVirtualDisplay = ffiModel.isVirtualDisplayResolution;
  1199. final visible = ffiModel.keyboard &&
  1200. (isVirtualDisplay || resolutions.length > 1) &&
  1201. pi.currentDisplay != kAllDisplayValue;
  1202. if (!visible) return Offstage();
  1203. final showOriginalBtn =
  1204. ffiModel.isOriginalResolutionSet && !ffiModel.isOriginalResolution;
  1205. final showFitLocalBtn = !_isRemoteResolutionFitLocal();
  1206. _setGroupValue();
  1207. return _SubmenuButton(
  1208. ffi: widget.ffi,
  1209. menuChildren: <Widget>[
  1210. _OriginalResolutionMenuButton(context, showOriginalBtn),
  1211. _FitLocalResolutionMenuButton(context, showFitLocalBtn),
  1212. _customResolutionMenuButton(context, isVirtualDisplay),
  1213. _menuDivider(showOriginalBtn, showFitLocalBtn, isVirtualDisplay),
  1214. ] +
  1215. _supportedResolutionMenuButtons(),
  1216. child: Text(translate("Resolution")),
  1217. );
  1218. }
  1219. _setGroupValue() {
  1220. if (pi.currentDisplay == kAllDisplayValue) {
  1221. return;
  1222. }
  1223. final lastGroupValue =
  1224. stateGlobal.getLastResolutionGroupValue(widget.id, pi.currentDisplay);
  1225. if (lastGroupValue == _kCustomResolutionValue) {
  1226. _groupValue = _kCustomResolutionValue;
  1227. } else {
  1228. _groupValue =
  1229. '${(rect?.width ?? 0).toInt()}x${(rect?.height ?? 0).toInt()}';
  1230. }
  1231. }
  1232. _menuDivider(
  1233. bool showOriginalBtn, bool showFitLocalBtn, bool isVirtualDisplay) {
  1234. return Offstage(
  1235. offstage: !(showOriginalBtn || showFitLocalBtn || isVirtualDisplay),
  1236. child: Divider(),
  1237. );
  1238. }
  1239. Future<void> _getLocalResolutionWayland() async {
  1240. if (!isWayland) return _getLocalResolution();
  1241. final window = await window_size.getWindowInfo();
  1242. final screen = window.screen;
  1243. if (screen != null) {
  1244. setState(() {
  1245. _localResolution = Resolution(
  1246. screen.frame.width.toInt(),
  1247. screen.frame.height.toInt(),
  1248. );
  1249. });
  1250. }
  1251. }
  1252. _getLocalResolution() {
  1253. _localResolution = null;
  1254. final String mainDisplay = bind.mainGetMainDisplay();
  1255. if (mainDisplay.isNotEmpty) {
  1256. try {
  1257. final display = json.decode(mainDisplay);
  1258. if (display['w'] != null && display['h'] != null) {
  1259. _localResolution = Resolution(display['w'], display['h']);
  1260. if (isWeb) {
  1261. if (display['scaleFactor'] != null) {
  1262. _localResolution = Resolution(
  1263. (display['w'] / display['scaleFactor']).toInt(),
  1264. (display['h'] / display['scaleFactor']).toInt(),
  1265. );
  1266. }
  1267. }
  1268. }
  1269. } catch (e) {
  1270. debugPrint('Failed to decode $mainDisplay, $e');
  1271. }
  1272. }
  1273. }
  1274. // This widget has been unmounted, so the State no longer has a context
  1275. _onChanged(String? value) async {
  1276. if (pi.currentDisplay == kAllDisplayValue) {
  1277. return;
  1278. }
  1279. stateGlobal.setLastResolutionGroupValue(
  1280. widget.id, pi.currentDisplay, value);
  1281. if (value == null) return;
  1282. int? w;
  1283. int? h;
  1284. if (value == _kCustomResolutionValue) {
  1285. w = int.tryParse(_customWidth.text);
  1286. h = int.tryParse(_customHeight.text);
  1287. } else {
  1288. final list = value.split('x');
  1289. if (list.length == 2) {
  1290. w = int.tryParse(list[0]);
  1291. h = int.tryParse(list[1]);
  1292. }
  1293. }
  1294. if (w != null && h != null) {
  1295. if (w != rect?.width.toInt() || h != rect?.height.toInt()) {
  1296. await _changeResolution(w, h);
  1297. }
  1298. }
  1299. }
  1300. _changeResolution(int w, int h) async {
  1301. if (pi.currentDisplay == kAllDisplayValue) {
  1302. return;
  1303. }
  1304. await bind.sessionChangeResolution(
  1305. sessionId: ffi.sessionId,
  1306. display: pi.currentDisplay,
  1307. width: w,
  1308. height: h,
  1309. );
  1310. Future.delayed(Duration(seconds: 3), () async {
  1311. final rect = ffiModel.rect;
  1312. if (rect == null) {
  1313. return;
  1314. }
  1315. if (w == rect.width.toInt() && h == rect.height.toInt()) {
  1316. if (await widget.screenAdjustor.isWindowCanBeAdjusted()) {
  1317. widget.screenAdjustor.doAdjustWindow(context);
  1318. }
  1319. }
  1320. });
  1321. }
  1322. Widget _OriginalResolutionMenuButton(
  1323. BuildContext context, bool showOriginalBtn) {
  1324. final display = pi.tryGetDisplayIfNotAllDisplay();
  1325. if (display == null) {
  1326. return Offstage();
  1327. }
  1328. if (!resolutions.any((e) =>
  1329. e.width == display.originalWidth &&
  1330. e.height == display.originalHeight)) {
  1331. return Offstage();
  1332. }
  1333. return Offstage(
  1334. offstage: !showOriginalBtn,
  1335. child: MenuButton(
  1336. onPressed: () =>
  1337. _changeResolution(display.originalWidth, display.originalHeight),
  1338. ffi: widget.ffi,
  1339. child: Text(
  1340. '${translate('resolution_original_tip')} ${display.originalWidth}x${display.originalHeight}'),
  1341. ),
  1342. );
  1343. }
  1344. Widget _FitLocalResolutionMenuButton(
  1345. BuildContext context, bool showFitLocalBtn) {
  1346. return Offstage(
  1347. offstage: !showFitLocalBtn,
  1348. child: MenuButton(
  1349. onPressed: () {
  1350. final resolution = _getBestFitResolution();
  1351. if (resolution != null) {
  1352. _changeResolution(resolution.width, resolution.height);
  1353. }
  1354. },
  1355. ffi: widget.ffi,
  1356. child: Text(
  1357. '${translate('resolution_fit_local_tip')} ${_localResolution?.width ?? 0}x${_localResolution?.height ?? 0}'),
  1358. ),
  1359. );
  1360. }
  1361. Widget _customResolutionMenuButton(BuildContext context, isVirtualDisplay) {
  1362. return Offstage(
  1363. offstage: !isVirtualDisplay,
  1364. child: RdoMenuButton(
  1365. value: _kCustomResolutionValue,
  1366. groupValue: _groupValue,
  1367. onChanged: (String? value) => _onChanged(value),
  1368. ffi: widget.ffi,
  1369. child: Row(
  1370. children: [
  1371. Text('${translate('resolution_custom_tip')} '),
  1372. SizedBox(
  1373. width: _kCustomResolutionEditingWidth,
  1374. child: _resolutionInput(_customWidth),
  1375. ),
  1376. Text(' x '),
  1377. SizedBox(
  1378. width: _kCustomResolutionEditingWidth,
  1379. child: _resolutionInput(_customHeight),
  1380. ),
  1381. ],
  1382. ),
  1383. ),
  1384. );
  1385. }
  1386. Widget _resolutionInput(TextEditingController controller) {
  1387. return TextField(
  1388. decoration: InputDecoration(
  1389. border: InputBorder.none,
  1390. isDense: true,
  1391. contentPadding: EdgeInsets.fromLTRB(3, 3, 3, 3),
  1392. ),
  1393. keyboardType: TextInputType.number,
  1394. inputFormatters: <TextInputFormatter>[
  1395. FilteringTextInputFormatter.digitsOnly,
  1396. LengthLimitingTextInputFormatter(4),
  1397. FilteringTextInputFormatter.allow(RegExp(r'[0-9]')),
  1398. ],
  1399. controller: controller,
  1400. ).workaroundFreezeLinuxMint();
  1401. }
  1402. List<Widget> _supportedResolutionMenuButtons() => resolutions
  1403. .map((e) => RdoMenuButton(
  1404. value: '${e.width}x${e.height}',
  1405. groupValue: _groupValue,
  1406. onChanged: (String? value) => _onChanged(value),
  1407. ffi: widget.ffi,
  1408. child: Text('${e.width}x${e.height}')))
  1409. .toList();
  1410. Resolution? _getBestFitResolution() {
  1411. if (_localResolution == null) {
  1412. return null;
  1413. }
  1414. if (ffiModel.isVirtualDisplayResolution) {
  1415. return _localResolution!;
  1416. }
  1417. for (final r in resolutions) {
  1418. if (r.width == _localResolution!.width &&
  1419. r.height == _localResolution!.height) {
  1420. return r;
  1421. }
  1422. }
  1423. return null;
  1424. }
  1425. bool _isRemoteResolutionFitLocal() {
  1426. if (_localResolution == null) {
  1427. return true;
  1428. }
  1429. final bestFitResolution = _getBestFitResolution();
  1430. if (bestFitResolution == null) {
  1431. return true;
  1432. }
  1433. return bestFitResolution.width == rect?.width.toInt() &&
  1434. bestFitResolution.height == rect?.height.toInt();
  1435. }
  1436. }
  1437. class _KeyboardMenu extends StatelessWidget {
  1438. final String id;
  1439. final FFI ffi;
  1440. _KeyboardMenu({
  1441. Key? key,
  1442. required this.id,
  1443. required this.ffi,
  1444. }) : super(key: key);
  1445. PeerInfo get pi => ffi.ffiModel.pi;
  1446. @override
  1447. Widget build(BuildContext context) {
  1448. var ffiModel = Provider.of<FfiModel>(context);
  1449. if (!ffiModel.keyboard) return Offstage();
  1450. toolbarToggles() => toolbarKeyboardToggles(ffi)
  1451. .map((e) => CkbMenuButton(
  1452. value: e.value, onChanged: e.onChanged, child: e.child, ffi: ffi))
  1453. .toList();
  1454. return _IconSubmenuButton(
  1455. tooltip: 'Keyboard Settings',
  1456. svg: "assets/keyboard.svg",
  1457. ffi: ffi,
  1458. color: _ToolbarTheme.blueColor,
  1459. hoverColor: _ToolbarTheme.hoverBlueColor,
  1460. menuChildrenGetter: () => [
  1461. keyboardMode(),
  1462. localKeyboardType(),
  1463. inputSource(),
  1464. Divider(),
  1465. viewMode(),
  1466. Divider(),
  1467. ...toolbarToggles(),
  1468. ...mouseSpeed(),
  1469. ...mobileActions(),
  1470. ]);
  1471. }
  1472. mouseSpeed() {
  1473. final speedWidgets = [];
  1474. final sessionId = ffi.sessionId;
  1475. if (isDesktop) {
  1476. if (ffi.ffiModel.keyboard) {
  1477. final enabled = !ffi.ffiModel.viewOnly;
  1478. final trackpad = MenuButton(
  1479. child: Text(translate('Trackpad speed')).paddingOnly(left: 26.0),
  1480. onPressed: enabled ? () => trackpadSpeedDialog(sessionId, ffi) : null,
  1481. ffi: ffi,
  1482. );
  1483. speedWidgets.add(trackpad);
  1484. }
  1485. }
  1486. return speedWidgets;
  1487. }
  1488. keyboardMode() {
  1489. return futureBuilder(future: () async {
  1490. return await bind.sessionGetKeyboardMode(sessionId: ffi.sessionId) ??
  1491. kKeyLegacyMode;
  1492. }(), hasData: (data) {
  1493. final groupValue = data as String;
  1494. List<InputModeMenu> modes = [
  1495. InputModeMenu(key: kKeyLegacyMode, menu: 'Legacy mode'),
  1496. InputModeMenu(key: kKeyMapMode, menu: 'Map mode'),
  1497. InputModeMenu(key: kKeyTranslateMode, menu: 'Translate mode'),
  1498. ];
  1499. List<RdoMenuButton> list = [];
  1500. final enabled = !ffi.ffiModel.viewOnly;
  1501. onChanged(String? value) async {
  1502. if (value == null) return;
  1503. await bind.sessionSetKeyboardMode(
  1504. sessionId: ffi.sessionId, value: value);
  1505. await ffi.inputModel.updateKeyboardMode();
  1506. }
  1507. // If use flutter to grab keys, we can only use one mode.
  1508. // Map mode and Legacy mode, at least one of them is supported.
  1509. String? modeOnly;
  1510. // Keep both map and legacy mode on web at the moment.
  1511. // TODO: Remove legacy mode after web supports translate mode on web.
  1512. if (isInputSourceFlutter && isDesktop) {
  1513. if (bind.sessionIsKeyboardModeSupported(
  1514. sessionId: ffi.sessionId, mode: kKeyMapMode)) {
  1515. modeOnly = kKeyMapMode;
  1516. } else if (bind.sessionIsKeyboardModeSupported(
  1517. sessionId: ffi.sessionId, mode: kKeyLegacyMode)) {
  1518. modeOnly = kKeyLegacyMode;
  1519. }
  1520. }
  1521. for (InputModeMenu mode in modes) {
  1522. if (modeOnly != null && mode.key != modeOnly) {
  1523. continue;
  1524. } else if (!bind.sessionIsKeyboardModeSupported(
  1525. sessionId: ffi.sessionId, mode: mode.key)) {
  1526. continue;
  1527. }
  1528. if (pi.isWayland && mode.key != kKeyMapMode) {
  1529. continue;
  1530. }
  1531. var text = translate(mode.menu);
  1532. if (mode.key == kKeyTranslateMode) {
  1533. text = '$text beta';
  1534. }
  1535. list.add(RdoMenuButton<String>(
  1536. child: Text(text),
  1537. value: mode.key,
  1538. groupValue: groupValue,
  1539. onChanged: enabled ? onChanged : null,
  1540. ffi: ffi,
  1541. ));
  1542. }
  1543. return Column(children: list);
  1544. });
  1545. }
  1546. localKeyboardType() {
  1547. final localPlatform = getLocalPlatformForKBLayoutType(pi.platform);
  1548. final visible = localPlatform != '';
  1549. if (!visible) return Offstage();
  1550. final enabled = !ffi.ffiModel.viewOnly;
  1551. return Column(
  1552. children: [
  1553. Divider(),
  1554. MenuButton(
  1555. child: Text(
  1556. '${translate('Local keyboard type')}: ${KBLayoutType.value}'),
  1557. trailingIcon: const Icon(Icons.settings),
  1558. ffi: ffi,
  1559. onPressed: enabled
  1560. ? () => showKBLayoutTypeChooser(localPlatform, ffi.dialogManager)
  1561. : null,
  1562. )
  1563. ],
  1564. );
  1565. }
  1566. inputSource() {
  1567. final supportedInputSource = bind.mainSupportedInputSource();
  1568. if (supportedInputSource.isEmpty) return Offstage();
  1569. late final List<dynamic> supportedInputSourceList;
  1570. try {
  1571. supportedInputSourceList = jsonDecode(supportedInputSource);
  1572. } catch (e) {
  1573. debugPrint('Failed to decode $supportedInputSource, $e');
  1574. return;
  1575. }
  1576. if (supportedInputSourceList.length < 2) return Offstage();
  1577. final inputSource = stateGlobal.getInputSource();
  1578. final enabled = !ffi.ffiModel.viewOnly;
  1579. final children = <Widget>[Divider()];
  1580. children.addAll(supportedInputSourceList.map((e) {
  1581. final d = e as List<dynamic>;
  1582. return RdoMenuButton<String>(
  1583. child: Text(translate(d[1] as String)),
  1584. value: d[0] as String,
  1585. groupValue: inputSource,
  1586. onChanged: enabled
  1587. ? (v) async {
  1588. if (v != null) {
  1589. await stateGlobal.setInputSource(ffi.sessionId, v);
  1590. await ffi.ffiModel.checkDesktopKeyboardMode();
  1591. await ffi.inputModel.updateKeyboardMode();
  1592. }
  1593. }
  1594. : null,
  1595. ffi: ffi,
  1596. );
  1597. }));
  1598. return Column(children: children);
  1599. }
  1600. viewMode() {
  1601. final ffiModel = ffi.ffiModel;
  1602. final enabled = versionCmp(pi.version, '1.2.0') >= 0 && ffiModel.keyboard;
  1603. return CkbMenuButton(
  1604. value: ffiModel.viewOnly,
  1605. onChanged: enabled
  1606. ? (value) async {
  1607. if (value == null) return;
  1608. await bind.sessionToggleOption(
  1609. sessionId: ffi.sessionId, value: kOptionToggleViewOnly);
  1610. final viewOnly = await bind.sessionGetToggleOption(
  1611. sessionId: ffi.sessionId, arg: kOptionToggleViewOnly);
  1612. ffiModel.setViewOnly(id, viewOnly ?? value);
  1613. }
  1614. : null,
  1615. ffi: ffi,
  1616. child: Text(translate('View Mode')));
  1617. }
  1618. mobileActions() {
  1619. if (pi.platform != kPeerPlatformAndroid) return [];
  1620. final enabled = versionCmp(pi.version, '1.2.7') >= 0;
  1621. if (!enabled) return [];
  1622. return [
  1623. Divider(),
  1624. MenuButton(
  1625. child: Text(translate('Back')),
  1626. onPressed: () => ffi.inputModel.onMobileBack(),
  1627. ffi: ffi),
  1628. MenuButton(
  1629. child: Text(translate('Home')),
  1630. onPressed: () => ffi.inputModel.onMobileHome(),
  1631. ffi: ffi),
  1632. MenuButton(
  1633. child: Text(translate('Apps')),
  1634. onPressed: () => ffi.inputModel.onMobileApps(),
  1635. ffi: ffi),
  1636. MenuButton(
  1637. child: Text(translate('Volume up')),
  1638. onPressed: () => ffi.inputModel.onMobileVolumeUp(),
  1639. ffi: ffi),
  1640. MenuButton(
  1641. child: Text(translate('Volume down')),
  1642. onPressed: () => ffi.inputModel.onMobileVolumeDown(),
  1643. ffi: ffi),
  1644. MenuButton(
  1645. child: Text(translate('Power')),
  1646. onPressed: () => ffi.inputModel.onMobilePower(),
  1647. ffi: ffi),
  1648. ];
  1649. }
  1650. }
  1651. class _ChatMenu extends StatefulWidget {
  1652. final String id;
  1653. final FFI ffi;
  1654. _ChatMenu({
  1655. Key? key,
  1656. required this.id,
  1657. required this.ffi,
  1658. }) : super(key: key);
  1659. @override
  1660. State<_ChatMenu> createState() => _ChatMenuState();
  1661. }
  1662. class _ChatMenuState extends State<_ChatMenu> {
  1663. // Using in StatelessWidget got `Looking up a deactivated widget's ancestor is unsafe`.
  1664. final chatButtonKey = GlobalKey();
  1665. @override
  1666. Widget build(BuildContext context) {
  1667. if (isWeb) {
  1668. return buildTextChatButton();
  1669. } else {
  1670. return _IconSubmenuButton(
  1671. tooltip: 'Chat',
  1672. key: chatButtonKey,
  1673. svg: 'assets/chat.svg',
  1674. ffi: widget.ffi,
  1675. color: _ToolbarTheme.blueColor,
  1676. hoverColor: _ToolbarTheme.hoverBlueColor,
  1677. menuChildrenGetter: () => [textChat(), voiceCall()]);
  1678. }
  1679. }
  1680. buildTextChatButton() {
  1681. return _IconMenuButton(
  1682. assetName: 'assets/message_24dp_5F6368.svg',
  1683. tooltip: 'Text chat',
  1684. key: chatButtonKey,
  1685. onPressed: _textChatOnPressed,
  1686. color: _ToolbarTheme.blueColor,
  1687. hoverColor: _ToolbarTheme.hoverBlueColor,
  1688. );
  1689. }
  1690. textChat() {
  1691. return MenuButton(
  1692. child: Text(translate('Text chat')),
  1693. ffi: widget.ffi,
  1694. onPressed: _textChatOnPressed);
  1695. }
  1696. _textChatOnPressed() {
  1697. RenderBox? renderBox =
  1698. chatButtonKey.currentContext?.findRenderObject() as RenderBox?;
  1699. Offset? initPos;
  1700. if (renderBox != null) {
  1701. final pos = renderBox.localToGlobal(Offset.zero);
  1702. initPos = Offset(pos.dx, pos.dy + _ToolbarTheme.dividerHeight);
  1703. }
  1704. widget.ffi.chatModel
  1705. .changeCurrentKey(MessageKey(widget.ffi.id, ChatModel.clientModeID));
  1706. widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos);
  1707. }
  1708. voiceCall() {
  1709. return MenuButton(
  1710. child: Text(translate('Voice call')),
  1711. ffi: widget.ffi,
  1712. onPressed: () =>
  1713. bind.sessionRequestVoiceCall(sessionId: widget.ffi.sessionId),
  1714. );
  1715. }
  1716. }
  1717. class _VoiceCallMenu extends StatelessWidget {
  1718. final String id;
  1719. final FFI ffi;
  1720. _VoiceCallMenu({
  1721. Key? key,
  1722. required this.id,
  1723. required this.ffi,
  1724. }) : super(key: key);
  1725. @override
  1726. Widget build(BuildContext context) {
  1727. menuChildrenGetter() {
  1728. final audioInput = AudioInput(
  1729. builder: (devices, currentDevice, setDevice) {
  1730. return Column(
  1731. children: devices
  1732. .map((d) => RdoMenuButton<String>(
  1733. child: Container(
  1734. child: Text(
  1735. d,
  1736. overflow: TextOverflow.ellipsis,
  1737. ),
  1738. constraints: BoxConstraints(maxWidth: 250),
  1739. ),
  1740. value: d,
  1741. groupValue: currentDevice,
  1742. onChanged: (v) {
  1743. if (v != null) setDevice(v);
  1744. },
  1745. ffi: ffi,
  1746. ))
  1747. .toList(),
  1748. );
  1749. },
  1750. isCm: false,
  1751. isVoiceCall: true,
  1752. );
  1753. return [
  1754. audioInput,
  1755. Divider(),
  1756. MenuButton(
  1757. child: Text(translate('End call')),
  1758. onPressed: () => bind.sessionCloseVoiceCall(sessionId: ffi.sessionId),
  1759. ffi: ffi,
  1760. ),
  1761. ];
  1762. }
  1763. return Obx(
  1764. () {
  1765. switch (ffi.chatModel.voiceCallStatus.value) {
  1766. case VoiceCallStatus.waitingForResponse:
  1767. return buildCallWaiting(context);
  1768. case VoiceCallStatus.connected:
  1769. return _IconSubmenuButton(
  1770. tooltip: 'Voice call',
  1771. svg: 'assets/voice_call.svg',
  1772. color: _ToolbarTheme.blueColor,
  1773. hoverColor: _ToolbarTheme.hoverBlueColor,
  1774. menuChildrenGetter: menuChildrenGetter,
  1775. ffi: ffi,
  1776. );
  1777. default:
  1778. return Offstage();
  1779. }
  1780. },
  1781. );
  1782. }
  1783. Widget buildCallWaiting(BuildContext context) {
  1784. return _IconMenuButton(
  1785. assetName: "assets/call_wait.svg",
  1786. tooltip: "Waiting",
  1787. onPressed: () => bind.sessionCloseVoiceCall(sessionId: ffi.sessionId),
  1788. color: _ToolbarTheme.redColor,
  1789. hoverColor: _ToolbarTheme.hoverRedColor,
  1790. );
  1791. }
  1792. }
  1793. class _RecordMenu extends StatelessWidget {
  1794. const _RecordMenu({Key? key}) : super(key: key);
  1795. @override
  1796. Widget build(BuildContext context) {
  1797. var ffi = Provider.of<FfiModel>(context);
  1798. var recordingModel = Provider.of<RecordingModel>(context);
  1799. final visible =
  1800. (recordingModel.start || ffi.permissions['recording'] != false);
  1801. if (!visible) return Offstage();
  1802. return _IconMenuButton(
  1803. assetName: 'assets/rec.svg',
  1804. tooltip: recordingModel.start
  1805. ? 'Stop session recording'
  1806. : 'Start session recording',
  1807. onPressed: () => recordingModel.toggle(),
  1808. color: recordingModel.start
  1809. ? _ToolbarTheme.redColor
  1810. : _ToolbarTheme.blueColor,
  1811. hoverColor: recordingModel.start
  1812. ? _ToolbarTheme.hoverRedColor
  1813. : _ToolbarTheme.hoverBlueColor,
  1814. );
  1815. }
  1816. }
  1817. class _CloseMenu extends StatelessWidget {
  1818. final String id;
  1819. final FFI ffi;
  1820. const _CloseMenu({Key? key, required this.id, required this.ffi})
  1821. : super(key: key);
  1822. @override
  1823. Widget build(BuildContext context) {
  1824. return _IconMenuButton(
  1825. assetName: 'assets/close.svg',
  1826. tooltip: 'Close',
  1827. onPressed: () => closeConnection(id: id),
  1828. color: _ToolbarTheme.redColor,
  1829. hoverColor: _ToolbarTheme.hoverRedColor,
  1830. );
  1831. }
  1832. }
  1833. class _IconMenuButton extends StatefulWidget {
  1834. final String? assetName;
  1835. final Widget? icon;
  1836. final String tooltip;
  1837. final Color color;
  1838. final Color hoverColor;
  1839. final VoidCallback? onPressed;
  1840. final double? hMargin;
  1841. final double? vMargin;
  1842. final bool topLevel;
  1843. final double? width;
  1844. const _IconMenuButton({
  1845. Key? key,
  1846. this.assetName,
  1847. this.icon,
  1848. required this.tooltip,
  1849. required this.color,
  1850. required this.hoverColor,
  1851. required this.onPressed,
  1852. this.hMargin,
  1853. this.vMargin,
  1854. this.topLevel = true,
  1855. this.width,
  1856. }) : super(key: key);
  1857. @override
  1858. State<_IconMenuButton> createState() => _IconMenuButtonState();
  1859. }
  1860. class _IconMenuButtonState extends State<_IconMenuButton> {
  1861. bool hover = false;
  1862. @override
  1863. Widget build(BuildContext context) {
  1864. assert(widget.assetName != null || widget.icon != null);
  1865. final icon = widget.icon ??
  1866. SvgPicture.asset(
  1867. widget.assetName!,
  1868. colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn),
  1869. width: _ToolbarTheme.buttonSize,
  1870. height: _ToolbarTheme.buttonSize,
  1871. );
  1872. var button = SizedBox(
  1873. width: widget.width ?? _ToolbarTheme.buttonSize,
  1874. height: _ToolbarTheme.buttonSize,
  1875. child: MenuItemButton(
  1876. style: ButtonStyle(
  1877. backgroundColor: MaterialStatePropertyAll(Colors.transparent),
  1878. padding: MaterialStatePropertyAll(EdgeInsets.zero),
  1879. overlayColor: MaterialStatePropertyAll(Colors.transparent)),
  1880. onHover: (value) => setState(() {
  1881. hover = value;
  1882. }),
  1883. onPressed: widget.onPressed,
  1884. child: Tooltip(
  1885. message: translate(widget.tooltip),
  1886. child: Material(
  1887. type: MaterialType.transparency,
  1888. child: Ink(
  1889. decoration: BoxDecoration(
  1890. borderRadius:
  1891. BorderRadius.circular(_ToolbarTheme.iconRadius),
  1892. color: hover ? widget.hoverColor : widget.color,
  1893. ),
  1894. child: icon)),
  1895. )),
  1896. ).marginSymmetric(
  1897. horizontal: widget.hMargin ?? _ToolbarTheme.buttonHMargin,
  1898. vertical: widget.vMargin ?? _ToolbarTheme.buttonVMargin);
  1899. button = Tooltip(
  1900. message: widget.tooltip,
  1901. child: button,
  1902. );
  1903. if (widget.topLevel) {
  1904. return MenuBar(children: [button]);
  1905. } else {
  1906. return button;
  1907. }
  1908. }
  1909. }
  1910. class _IconSubmenuButton extends StatefulWidget {
  1911. final String tooltip;
  1912. final String? svg;
  1913. final Widget? icon;
  1914. final Color color;
  1915. final Color hoverColor;
  1916. final List<Widget> Function() menuChildrenGetter;
  1917. final MenuStyle? menuStyle;
  1918. final FFI? ffi;
  1919. final double? width;
  1920. _IconSubmenuButton({
  1921. Key? key,
  1922. this.svg,
  1923. this.icon,
  1924. required this.tooltip,
  1925. required this.color,
  1926. required this.hoverColor,
  1927. required this.menuChildrenGetter,
  1928. this.ffi,
  1929. this.menuStyle,
  1930. this.width,
  1931. }) : super(key: key);
  1932. @override
  1933. State<_IconSubmenuButton> createState() => _IconSubmenuButtonState();
  1934. }
  1935. class _IconSubmenuButtonState extends State<_IconSubmenuButton> {
  1936. bool hover = false;
  1937. @override
  1938. Widget build(BuildContext context) {
  1939. assert(widget.svg != null || widget.icon != null);
  1940. final icon = widget.icon ??
  1941. SvgPicture.asset(
  1942. widget.svg!,
  1943. colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn),
  1944. width: _ToolbarTheme.buttonSize,
  1945. height: _ToolbarTheme.buttonSize,
  1946. );
  1947. final button = SizedBox(
  1948. width: widget.width ?? _ToolbarTheme.buttonSize,
  1949. height: _ToolbarTheme.buttonSize,
  1950. child: SubmenuButton(
  1951. menuStyle:
  1952. widget.menuStyle ?? _ToolbarTheme.defaultMenuStyle(context),
  1953. style: _ToolbarTheme.defaultMenuButtonStyle,
  1954. onHover: (value) => setState(() {
  1955. hover = value;
  1956. }),
  1957. child: Tooltip(
  1958. message: translate(widget.tooltip),
  1959. child: Material(
  1960. type: MaterialType.transparency,
  1961. child: Ink(
  1962. decoration: BoxDecoration(
  1963. borderRadius:
  1964. BorderRadius.circular(_ToolbarTheme.iconRadius),
  1965. color: hover ? widget.hoverColor : widget.color,
  1966. ),
  1967. child: icon))),
  1968. menuChildren: widget
  1969. .menuChildrenGetter()
  1970. .map((e) => _buildPointerTrackWidget(e, widget.ffi))
  1971. .toList()));
  1972. return MenuBar(children: [
  1973. button.marginSymmetric(
  1974. horizontal: _ToolbarTheme.buttonHMargin,
  1975. vertical: _ToolbarTheme.buttonVMargin)
  1976. ]);
  1977. }
  1978. }
  1979. class _SubmenuButton extends StatelessWidget {
  1980. final List<Widget> menuChildren;
  1981. final Widget? child;
  1982. final FFI ffi;
  1983. const _SubmenuButton({
  1984. Key? key,
  1985. required this.menuChildren,
  1986. required this.child,
  1987. required this.ffi,
  1988. }) : super(key: key);
  1989. @override
  1990. Widget build(BuildContext context) {
  1991. return SubmenuButton(
  1992. key: key,
  1993. child: child,
  1994. menuChildren:
  1995. menuChildren.map((e) => _buildPointerTrackWidget(e, ffi)).toList(),
  1996. menuStyle: _ToolbarTheme.defaultMenuStyle(context),
  1997. );
  1998. }
  1999. }
  2000. class MenuButton extends StatelessWidget {
  2001. final VoidCallback? onPressed;
  2002. final Widget? trailingIcon;
  2003. final Widget? child;
  2004. final FFI? ffi;
  2005. MenuButton(
  2006. {Key? key,
  2007. this.onPressed,
  2008. this.trailingIcon,
  2009. required this.child,
  2010. this.ffi})
  2011. : super(key: key);
  2012. @override
  2013. Widget build(BuildContext context) {
  2014. return MenuItemButton(
  2015. key: key,
  2016. onPressed: onPressed != null
  2017. ? () {
  2018. if (ffi != null) {
  2019. _menuDismissCallback(ffi!);
  2020. }
  2021. onPressed?.call();
  2022. }
  2023. : null,
  2024. trailingIcon: trailingIcon,
  2025. child: child);
  2026. }
  2027. }
  2028. class CkbMenuButton extends StatelessWidget {
  2029. final bool? value;
  2030. final ValueChanged<bool?>? onChanged;
  2031. final Widget? child;
  2032. final FFI? ffi;
  2033. const CkbMenuButton(
  2034. {Key? key,
  2035. required this.value,
  2036. required this.onChanged,
  2037. required this.child,
  2038. this.ffi})
  2039. : super(key: key);
  2040. @override
  2041. Widget build(BuildContext context) {
  2042. return CheckboxMenuButton(
  2043. key: key,
  2044. value: value,
  2045. child: child,
  2046. onChanged: onChanged != null
  2047. ? (bool? value) {
  2048. if (ffi != null) {
  2049. _menuDismissCallback(ffi!);
  2050. }
  2051. onChanged?.call(value);
  2052. }
  2053. : null,
  2054. );
  2055. }
  2056. }
  2057. class RdoMenuButton<T> extends StatelessWidget {
  2058. final T value;
  2059. final T? groupValue;
  2060. final ValueChanged<T?>? onChanged;
  2061. final Widget? child;
  2062. final FFI? ffi;
  2063. const RdoMenuButton({
  2064. Key? key,
  2065. required this.value,
  2066. required this.groupValue,
  2067. required this.child,
  2068. this.ffi,
  2069. this.onChanged,
  2070. }) : super(key: key);
  2071. @override
  2072. Widget build(BuildContext context) {
  2073. return RadioMenuButton(
  2074. value: value,
  2075. groupValue: groupValue,
  2076. child: child,
  2077. onChanged: onChanged != null
  2078. ? (T? value) {
  2079. if (ffi != null) {
  2080. _menuDismissCallback(ffi!);
  2081. }
  2082. onChanged?.call(value);
  2083. }
  2084. : null,
  2085. );
  2086. }
  2087. }
  2088. class _DraggableShowHide extends StatefulWidget {
  2089. final String id;
  2090. final SessionID sessionId;
  2091. final RxDouble fractionX;
  2092. final RxBool dragging;
  2093. final ToolbarState toolbarState;
  2094. final BorderRadius borderRadius;
  2095. final Function(bool) setFullscreen;
  2096. final Function() setMinimize;
  2097. const _DraggableShowHide({
  2098. Key? key,
  2099. required this.id,
  2100. required this.sessionId,
  2101. required this.fractionX,
  2102. required this.dragging,
  2103. required this.toolbarState,
  2104. required this.setFullscreen,
  2105. required this.setMinimize,
  2106. required this.borderRadius,
  2107. }) : super(key: key);
  2108. @override
  2109. State<_DraggableShowHide> createState() => _DraggableShowHideState();
  2110. }
  2111. class _DraggableShowHideState extends State<_DraggableShowHide> {
  2112. Offset position = Offset.zero;
  2113. Size size = Size.zero;
  2114. double left = 0.0;
  2115. double right = 1.0;
  2116. RxBool get show => widget.toolbarState.show;
  2117. @override
  2118. initState() {
  2119. super.initState();
  2120. final confLeft = double.tryParse(
  2121. bind.mainGetLocalOption(key: kOptionRemoteMenubarDragLeft));
  2122. if (confLeft == null) {
  2123. bind.mainSetLocalOption(
  2124. key: kOptionRemoteMenubarDragLeft, value: left.toString());
  2125. } else {
  2126. left = confLeft;
  2127. }
  2128. final confRight = double.tryParse(
  2129. bind.mainGetLocalOption(key: kOptionRemoteMenubarDragRight));
  2130. if (confRight == null) {
  2131. bind.mainSetLocalOption(
  2132. key: kOptionRemoteMenubarDragRight, value: right.toString());
  2133. } else {
  2134. right = confRight;
  2135. }
  2136. }
  2137. Widget _buildDraggable(BuildContext context) {
  2138. return Draggable(
  2139. axis: Axis.horizontal,
  2140. child: Icon(
  2141. Icons.drag_indicator,
  2142. size: 20,
  2143. color: MyTheme.color(context).drag_indicator,
  2144. ),
  2145. feedback: widget,
  2146. onDragStarted: (() {
  2147. final RenderObject? renderObj = context.findRenderObject();
  2148. if (renderObj != null) {
  2149. final RenderBox renderBox = renderObj as RenderBox;
  2150. size = renderBox.size;
  2151. position = renderBox.localToGlobal(Offset.zero);
  2152. }
  2153. widget.dragging.value = true;
  2154. }),
  2155. onDragEnd: (details) {
  2156. final mediaSize = MediaQueryData.fromView(View.of(context)).size;
  2157. widget.fractionX.value +=
  2158. (details.offset.dx - position.dx) / (mediaSize.width - size.width);
  2159. if (widget.fractionX.value < left) {
  2160. widget.fractionX.value = left;
  2161. }
  2162. if (widget.fractionX.value > right) {
  2163. widget.fractionX.value = right;
  2164. }
  2165. bind.sessionPeerOption(
  2166. sessionId: widget.sessionId,
  2167. name: 'remote-menubar-drag-x',
  2168. value: widget.fractionX.value.toString(),
  2169. );
  2170. widget.dragging.value = false;
  2171. },
  2172. );
  2173. }
  2174. @override
  2175. Widget build(BuildContext context) {
  2176. final ButtonStyle buttonStyle = ButtonStyle(
  2177. minimumSize: MaterialStateProperty.all(const Size(0, 0)),
  2178. padding: MaterialStateProperty.all(EdgeInsets.zero),
  2179. );
  2180. final isFullscreen = stateGlobal.fullscreen;
  2181. const double iconSize = 20;
  2182. buttonWrapper(VoidCallback? onPressed, Widget child,
  2183. {Color hoverColor = _ToolbarTheme.blueColor}) {
  2184. final bgColor = buttonStyle.backgroundColor?.resolve({});
  2185. return TextButton(
  2186. onPressed: onPressed,
  2187. child: child,
  2188. style: buttonStyle.copyWith(
  2189. backgroundColor: MaterialStateProperty.resolveWith((states) {
  2190. if (states.contains(MaterialState.hovered)) {
  2191. return (bgColor ?? hoverColor).withOpacity(0.15);
  2192. }
  2193. return bgColor;
  2194. }),
  2195. ),
  2196. );
  2197. }
  2198. final child = Row(
  2199. mainAxisSize: MainAxisSize.min,
  2200. children: [
  2201. _buildDraggable(context),
  2202. Obx(() => buttonWrapper(
  2203. () {
  2204. widget.setFullscreen(!isFullscreen.value);
  2205. },
  2206. Tooltip(
  2207. message: translate(
  2208. isFullscreen.isTrue ? 'Exit Fullscreen' : 'Fullscreen'),
  2209. child: Icon(
  2210. isFullscreen.isTrue
  2211. ? Icons.fullscreen_exit
  2212. : Icons.fullscreen,
  2213. size: iconSize,
  2214. ),
  2215. ),
  2216. )),
  2217. if (!isMacOS && !isWebDesktop)
  2218. Obx(() => Offstage(
  2219. offstage: isFullscreen.isFalse,
  2220. child: buttonWrapper(
  2221. widget.setMinimize,
  2222. Tooltip(
  2223. message: translate('Minimize'),
  2224. child: Icon(
  2225. Icons.remove,
  2226. size: iconSize,
  2227. ),
  2228. ),
  2229. ),
  2230. )),
  2231. buttonWrapper(
  2232. () => setState(() {
  2233. widget.toolbarState.switchShow(widget.sessionId);
  2234. }),
  2235. Obx((() => Tooltip(
  2236. message:
  2237. translate(show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
  2238. child: Icon(
  2239. show.isTrue ? Icons.expand_less : Icons.expand_more,
  2240. size: iconSize,
  2241. ),
  2242. ))),
  2243. ),
  2244. if (isWebDesktop)
  2245. Obx(() {
  2246. if (show.isTrue) {
  2247. return Offstage();
  2248. } else {
  2249. return buttonWrapper(
  2250. () => closeConnection(id: widget.id),
  2251. Tooltip(
  2252. message: translate('Close'),
  2253. child: Icon(
  2254. Icons.close,
  2255. size: iconSize,
  2256. color: _ToolbarTheme.redColor,
  2257. ),
  2258. ),
  2259. hoverColor: _ToolbarTheme.redColor,
  2260. ).paddingOnly(left: iconSize / 2);
  2261. }
  2262. })
  2263. ],
  2264. );
  2265. return TextButtonTheme(
  2266. data: TextButtonThemeData(style: buttonStyle),
  2267. child: Container(
  2268. decoration: BoxDecoration(
  2269. color: Theme.of(context)
  2270. .menuBarTheme
  2271. .style
  2272. ?.backgroundColor
  2273. ?.resolve(MaterialState.values.toSet()),
  2274. border: Border.all(
  2275. color: _ToolbarTheme.borderColor(context),
  2276. width: 1,
  2277. ),
  2278. borderRadius: widget.borderRadius,
  2279. ),
  2280. child: SizedBox(
  2281. height: 20,
  2282. child: child,
  2283. ),
  2284. ),
  2285. );
  2286. }
  2287. }
  2288. class InputModeMenu {
  2289. final String key;
  2290. final String menu;
  2291. InputModeMenu({required this.key, required this.menu});
  2292. }
  2293. _menuDismissCallback(FFI ffi) => ffi.inputModel.refreshMousePos();
  2294. Widget _buildPointerTrackWidget(Widget child, FFI? ffi) {
  2295. return Listener(
  2296. onPointerHover: (PointerHoverEvent e) => {
  2297. if (ffi != null) {ffi.inputModel.lastMousePos = e.position}
  2298. },
  2299. child: MouseRegion(
  2300. child: child,
  2301. ),
  2302. );
  2303. }