remote_toolbar.dart 73 KB

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