From c2d843ace6c4d80bc28348a08805fc0ad4d093eb Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 2 Nov 2022 22:23:23 +0800 Subject: [PATCH 1/2] refactor main runApp() Signed-off-by: fufesou --- flutter/lib/main.dart | 206 ++++++++++++---------------- flutter/lib/models/input_model.dart | 2 +- flutter/pubspec.yaml | 1 + 3 files changed, 86 insertions(+), 123 deletions(-) diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 52eacf5f5..3621a3bf8 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -16,6 +16,7 @@ import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:window_manager/window_manager.dart'; +import 'package:bot_toast/bot_toast.dart'; // import 'package:window_manager/window_manager.dart'; @@ -53,15 +54,27 @@ Future main(List args) async { switch (wType) { case WindowType.RemoteDesktop: desktopType = DesktopType.remote; - runRemoteScreen(argument); + runMultiWindow( + argument, + kAppTypeDesktopRemote, + 'RustDesk - Remote Desktop', + ); break; case WindowType.FileTransfer: desktopType = DesktopType.fileTransfer; - runFileTransferScreen(argument); + runMultiWindow( + argument, + kAppTypeDesktopFileTransfer, + 'RustDesk - File Transfer', + ); break; case WindowType.PortForward: desktopType = DesktopType.portForward; - runPortForwardScreen(argument); + runMultiWindow( + argument, + kAppTypeDesktopPortForward, + 'RustDesk - Port Forward', + ); break; default: break; @@ -120,84 +133,18 @@ void runMobileApp() async { runApp(App()); } -void runRemoteScreen(Map argument) async { - await initEnv(kAppTypeDesktopRemote); - runApp(RefreshWrapper( - builder: (context) => GetMaterialApp( - navigatorKey: globalKey, - debugShowCheckedModeBanner: false, - title: 'RustDesk - Remote Desktop', - theme: MyTheme.lightTheme, - darkTheme: MyTheme.darkTheme, - themeMode: MyTheme.currentThemeMode(), - home: DesktopRemoteScreen( - params: argument, - ), - localizationsDelegates: const [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: supportedLocales, - navigatorObservers: const [ - // FirebaseAnalyticsObserver(analytics: analytics), - ], - builder: _keepScaleBuilder(), +void runMultiWindow( + Map argument, + String appType, + String title, +) async { + await initEnv(appType); + _runApp( + title, + DesktopRemoteScreen( + params: argument, ), - )); -} - -void runFileTransferScreen(Map argument) async { - await initEnv(kAppTypeDesktopFileTransfer); - runApp( - RefreshWrapper( - builder: (context) => GetMaterialApp( - navigatorKey: globalKey, - debugShowCheckedModeBanner: false, - title: 'RustDesk - File Transfer', - theme: MyTheme.lightTheme, - darkTheme: MyTheme.darkTheme, - themeMode: MyTheme.currentThemeMode(), - home: DesktopFileTransferScreen(params: argument), - localizationsDelegates: const [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: supportedLocales, - navigatorObservers: const [ - // FirebaseAnalyticsObserver(analytics: analytics), - ], - builder: _keepScaleBuilder(), - ), - ), - ); -} - -void runPortForwardScreen(Map argument) async { - await initEnv(kAppTypeDesktopPortForward); - runApp( - RefreshWrapper(builder: (context) { - return GetMaterialApp( - navigatorKey: globalKey, - debugShowCheckedModeBanner: false, - title: 'RustDesk - Port Forward', - theme: MyTheme.lightTheme, - darkTheme: MyTheme.darkTheme, - themeMode: MyTheme.currentThemeMode(), - home: DesktopPortForwardScreen(params: argument), - localizationsDelegates: const [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: supportedLocales, - navigatorObservers: const [ - // FirebaseAnalyticsObserver(analytics: analytics), - ], - builder: _keepScaleBuilder(), - ); - }), + MyTheme.currentThemeMode(), ); } @@ -206,21 +153,11 @@ void runConnectionManagerScreen() async { // initialize window WindowOptions windowOptions = getHiddenTitleBarWindowOptions(size: kConnectionManagerWindowSize); - runApp(RefreshWrapper(builder: (context) { - return GetMaterialApp( - debugShowCheckedModeBanner: false, - theme: MyTheme.lightTheme, - darkTheme: MyTheme.darkTheme, - themeMode: MyTheme.currentThemeMode(), - localizationsDelegates: const [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: supportedLocales, - home: const DesktopServerPage(), - builder: _keepScaleBuilder()); - })); + _runApp( + '', + const DesktopServerPage(), + MyTheme.currentThemeMode(), + ); windowManager.waitUntilReadyToShow(windowOptions, () async { await windowManager.show(); // ensure initial window size to be changed @@ -235,23 +172,44 @@ void runConnectionManagerScreen() async { }); } +void _runApp( + String title, + Widget home, + ThemeMode themeMode, +) { + final botToastBuilder = BotToastInit(); + runApp(RefreshWrapper( + builder: (context) => GetMaterialApp( + navigatorKey: globalKey, + debugShowCheckedModeBanner: false, + title: title, + theme: MyTheme.lightTheme, + darkTheme: MyTheme.darkTheme, + themeMode: themeMode, + home: home, + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: supportedLocales, + navigatorObservers: [ + // FirebaseAnalyticsObserver(analytics: analytics), + BotToastNavigatorObserver(), + ], + builder: (context, child) { + child = _keepScaleBuilder(context, child); + child = botToastBuilder(context, child); + return child; + }, + ), + )); +} + void runInstallPage() async { await windowManager.ensureInitialized(); await initEnv(kAppTypeMain); - runApp(RefreshWrapper( - builder: (context) => GetMaterialApp( - debugShowCheckedModeBanner: false, - theme: MyTheme.lightTheme, - themeMode: ThemeMode.light, - localizationsDelegates: const [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: supportedLocales, - home: const InstallPage(), - builder: _keepScaleBuilder()), - )); + _runApp('', const InstallPage(), ThemeMode.light); windowManager.waitUntilReadyToShow( WindowOptions(size: Size(800, 600), center: true), () async { windowManager.show(); @@ -303,6 +261,7 @@ class _AppState extends State { @override Widget build(BuildContext context) { // final analytics = FirebaseAnalytics.instance; + final botToastBuilder = BotToastInit(); return RefreshWrapper(builder: (context) { return MultiProvider( providers: [ @@ -325,15 +284,16 @@ class _AppState extends State { : !isAndroid ? WebHomePage() : HomePage(), - navigatorObservers: const [ - // FirebaseAnalyticsObserver(analytics: analytics), - ], localizationsDelegates: const [ GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], supportedLocales: supportedLocales, + navigatorObservers: [ + // FirebaseAnalyticsObserver(analytics: analytics), + BotToastNavigatorObserver(), + ], builder: isAndroid ? (context, child) => AccessibilityListener( child: MediaQuery( @@ -343,22 +303,24 @@ class _AppState extends State { child: child ?? Container(), ), ) - : _keepScaleBuilder(), + : (context, child) { + child = _keepScaleBuilder(context, child); + child = botToastBuilder(context, child); + return child; + }, ), ); }); } } -_keepScaleBuilder() { - return (BuildContext context, Widget? child) { - return MediaQuery( - data: MediaQuery.of(context).copyWith( - textScaleFactor: 1.0, - ), - child: child ?? Container(), - ); - }; +Widget _keepScaleBuilder(BuildContext context, Widget? child) { + return MediaQuery( + data: MediaQuery.of(context).copyWith( + textScaleFactor: 1.0, + ), + child: child ?? Container(), + ); } _registerEventHandler() { diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 2469ee0d9..280c72e79 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:math'; +import 'dart:ui' as ui; import 'package:flutter/gestures.dart'; import 'package:flutter/services.dart'; @@ -11,7 +12,6 @@ import '../../models/platform_model.dart'; import '../common.dart'; import '../consts.dart'; import './state_model.dart'; -import 'dart:ui' as ui; /// Mouse button enum. enum MouseButtons { left, right, wheel } diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 43eba9976..7dfb53538 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -102,6 +102,7 @@ dependencies: ref: 5be5113d59c753989dbf1106241379e3fd4c9b18 path: ^1.8.1 auto_size_text: ^3.0.0 + bot_toast: ^4.0.3 dev_dependencies: icons_launcher: ^2.0.4 From 911436379fbc419f2f634e9850c27d678b6ac9d0 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 3 Nov 2022 21:58:25 +0800 Subject: [PATCH 2/2] flutter_desktop: remote tab menu Signed-off-by: fufesou --- flutter/lib/common.dart | 9 +- flutter/lib/desktop/pages/remote_page.dart | 15 +- .../lib/desktop/pages/remote_tab_page.dart | 171 +++++++++++++++++- .../widgets/material_mod_popup_menu.dart | 143 ++++++++++++--- .../lib/desktop/widgets/remote_menubar.dart | 18 +- .../lib/desktop/widgets/tabbar_widget.dart | 149 ++++++++++----- flutter/lib/models/chat_model.dart | 4 +- flutter/lib/models/server_model.dart | 14 +- flutter/lib/models/web_model.dart | 14 +- 9 files changed, 433 insertions(+), 104 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 752de582c..e370fa9e4 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -86,7 +86,7 @@ class IconFont { static const IconData add = IconData(0xe664, fontFamily: _family1); static const IconData menu = IconData(0xe628, fontFamily: _family1); static const IconData search = IconData(0xe6a4, fontFamily: _family2); - static const IconData round_close = IconData(0xe6ed, fontFamily: _family2); + static const IconData roundClose = IconData(0xe6ed, fontFamily: _family2); } class ColorThemeExtension extends ThemeExtension { @@ -1330,11 +1330,8 @@ Future> getHttpHeaders() async { // Simple wrapper of built-in types for refrence use. class SimpleWrapper { - T t; - SimpleWrapper(this.t); - - T get value => t; - set value(T t) => this.t = t; + T value; + SimpleWrapper(this.value); } /// call this to reload current window. diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 89050458d..76395be9d 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -25,15 +25,24 @@ bool _isCustomCursorInited = false; final SimpleWrapper _firstEnterImage = SimpleWrapper(false); class RemotePage extends StatefulWidget { - const RemotePage({ + RemotePage({ Key? key, required this.id, }) : super(key: key); final String id; + final SimpleWrapper?> _lastState = SimpleWrapper(null); + + FFI get ffi => (_lastState.value! as _RemotePageState)._ffi; + RxBool get showMenubar => + (_lastState.value! as _RemotePageState)._showMenubar; @override - State createState() => _RemotePageState(); + State createState() { + final state = _RemotePageState(); + _lastState.value = state; + return state; + } } class _RemotePageState extends State @@ -41,6 +50,7 @@ class _RemotePageState extends State Timer? _timer; String keyboardMode = "legacy"; final _cursorOverImage = false.obs; + final _showMenubar = false.obs; late RxBool _showRemoteCursor; late RxBool _remoteCursorMoved; late RxBool _keyboardEnabled; @@ -229,6 +239,7 @@ class _RemotePageState extends State paints.add(RemoteMenubar( id: widget.id, ffi: _ffi, + show: _showMenubar, onEnterOrLeaveImageSetter: (func) => _onEnterOrLeaveImage4Menubar = func, onEnterOrLeaveImageCleaner: () => _onEnterOrLeaveImage4Menubar = null, )); diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 29253d537..b486c5fca 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:io'; +import 'dart:ui' as ui; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; @@ -9,12 +10,23 @@ import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/desktop/pages/remote_page.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart' + as mod_menu; +import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; +import 'package:bot_toast/bot_toast.dart'; import '../../models/platform_model.dart'; +class _MenuTheme { + static const Color commonColor = MyTheme.accent; + // kMinInteractiveDimension + static const double height = 20.0; + static const double dividerHeight = 12.0; +} + class ConnectionTabPage extends StatefulWidget { final Map params; @@ -123,7 +135,7 @@ class _ConnectionTabPageState extends State { connectionType.secure.value == ConnectionType.strSecure ? 'Secure Connection' : 'Insecure Connection'); - return Row( + final tab = Row( mainAxisAlignment: MainAxisAlignment.center, children: [ icon, @@ -138,6 +150,23 @@ class _ConnectionTabPageState extends State { label, ], ); + + return Listener( + onPointerDown: (e) { + if (e.kind != ui.PointerDeviceKind.mouse) { + return; + } + if (e.buttons == 2) { + showRightMenu( + (CancelFunc cancelFunc) { + return _tabMenuBuilder(key, cancelFunc); + }, + target: e.position, + ); + } + }, + child: tab, + ); } }), )), @@ -151,6 +180,146 @@ class _ConnectionTabPageState extends State { ); } + // to-do: some dup code to ../widgets/remote_menubar + Widget _tabMenuBuilder(String key, CancelFunc cancelFunc) { + final List> menu = []; + const EdgeInsets padding = EdgeInsets.only(left: 8.0, right: 5.0); + final remotePage = tabController.state.value.tabs + .firstWhere((tab) => tab.key == key) + .page as RemotePage; + final ffi = remotePage.ffi; + final pi = ffi.ffiModel.pi; + final perms = ffi.ffiModel.permissions; + final showMenuBar = remotePage.showMenubar; + menu.addAll([ + MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Close'), + style: style, + ), + proc: () { + tabController.closeBy(key); + cancelFunc(); + }, + padding: padding, + ), + MenuEntryButton( + childBuilder: (TextStyle? style) => Obx(() => Text( + translate(showMenuBar.isTrue ? 'Hide Menubar' : 'Show Menubar'), + style: style, + )), + proc: () { + showMenuBar.value = !showMenuBar.value; + cancelFunc(); + }, + padding: padding, + ), + MenuEntryDivider(), + MenuEntryRadios( + text: translate('Ratio'), + optionsGetter: () => [ + MenuEntryRadioOption( + text: translate('Scale original'), + value: 'original', + dismissOnClicked: true, + ), + MenuEntryRadioOption( + text: translate('Scale adaptive'), + value: 'adaptive', + dismissOnClicked: true, + ), + ], + curOptionGetter: () async { + return await bind.sessionGetOption(id: key, arg: 'view-style') ?? + 'adaptive'; + }, + optionSetter: (String oldValue, String newValue) async { + await bind.sessionPeerOption( + id: key, name: "view-style", value: newValue); + ffi.canvasModel.updateViewStyle(); + cancelFunc(); + }, + padding: padding, + ), + MenuEntryDivider(), + MenuEntryRadios( + text: translate('Scroll Style'), + optionsGetter: () => [ + MenuEntryRadioOption( + text: translate('ScrollAuto'), + value: 'scrollauto', + dismissOnClicked: true, + ), + MenuEntryRadioOption( + text: translate('Scrollbar'), + value: 'scrollbar', + dismissOnClicked: true, + ), + ], + curOptionGetter: () async { + return await bind.sessionGetOption(id: key, arg: 'scroll-style') ?? + ''; + }, + optionSetter: (String oldValue, String newValue) async { + await bind.sessionPeerOption( + id: key, name: "scroll-style", value: newValue); + ffi.canvasModel.updateScrollStyle(); + cancelFunc(); + }, + padding: padding, + dismissOnClicked: true, + ), + MenuEntryDivider(), + () { + final state = ShowRemoteCursorState.find(key); + return MenuEntrySwitch2( + switchType: SwitchType.scheckbox, + text: translate('Show remote cursor'), + getter: () { + return state; + }, + setter: (bool v) async { + state.value = v; + await bind.sessionToggleOption( + id: key, value: 'show-remote-cursor'); + cancelFunc(); + }, + padding: padding, + ); + }() + ]); + + if (perms['keyboard'] != false) { + if (pi.platform == 'Linux' || pi.sasEnabled) { + menu.add(MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + '${translate("Insert")} Ctrl + Alt + Del', + style: style, + ), + proc: () { + bind.sessionCtrlAltDel(id: key); + cancelFunc(); + }, + padding: padding, + dismissOnClicked: true, + )); + } + } + + return mod_menu.PopupMenu( + items: menu + .map((entry) => entry.build( + context, + const MenuConfig( + commonColor: _MenuTheme.commonColor, + height: _MenuTheme.height, + dividerHeight: _MenuTheme.dividerHeight, + ))) + .expand((i) => i) + .toList(), + ); + } + void onRemoveId(String id) { if (tabController.state.value.tabs.isEmpty) { WindowController.fromWindowId(windowId()).hide(); diff --git a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart index 776a2b756..a371e8f52 100644 --- a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart +++ b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart @@ -139,8 +139,7 @@ class _MenuItem extends SingleChildRenderObjectWidget { Key? key, required this.onLayout, required Widget? child, - }) : assert(onLayout != null), - super(key: key, child: child); + }) : super(key: key, child: child); final ValueChanged onLayout; @@ -157,9 +156,7 @@ class _MenuItem extends SingleChildRenderObjectWidget { } class _RenderMenuItem extends RenderShiftedBox { - _RenderMenuItem(this.onLayout, [RenderBox? child]) - : assert(onLayout != null), - super(child); + _RenderMenuItem(this.onLayout, [RenderBox? child]) : super(child); ValueChanged onLayout; @@ -240,9 +237,7 @@ class PopupMenuItem extends PopupMenuEntry { this.textStyle, this.mouseCursor, required this.child, - }) : assert(enabled != null), - assert(height != null), - super(key: key); + }) : super(key: key); /// The value that will be returned by [showMenu] if this entry is selected. final T? value; @@ -382,11 +377,15 @@ class PopupMenuItemState> extends State { child: Semantics( enabled: widget.enabled, button: true, - child: InkWell( - onTap: widget.enabled ? handleTap : null, - canRequestFocus: widget.enabled, - mouseCursor: _EffectiveMouseCursor( - widget.mouseCursor, popupMenuTheme.mouseCursor), + // child: InkWell( + // onTap: widget.enabled ? handleTap : null, + // canRequestFocus: widget.enabled, + // mouseCursor: _EffectiveMouseCursor( + // widget.mouseCursor, popupMenuTheme.mouseCursor), + // child: item, + // ), + child: TextButton( + onPressed: widget.enabled ? handleTap : null, child: item, ), ), @@ -471,8 +470,7 @@ class CheckedPopupMenuItem extends PopupMenuItem { EdgeInsets? padding, double height = kMinInteractiveDimension, Widget? child, - }) : assert(checked != null), - super( + }) : super( key: key, value: value, enabled: enabled, @@ -524,10 +522,11 @@ class _CheckedPopupMenuItemState @override void handleTap() { // This fades the checkmark in or out when tapped. - if (widget.checked) + if (widget.checked) { _controller.reverse(); - else + } else { _controller.forward(); + } super.handleTap(); } @@ -699,7 +698,7 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate { final double buttonHeight = size.height - position.top - position.bottom; // Find the ideal vertical position. double y = position.top; - if (selectedItemIndex != null && itemSizes != null) { + if (selectedItemIndex != null) { double selectedItemOffset = _kMenuVerticalPadding; for (int index = 0; index < selectedItemIndex!; index += 1) { selectedItemOffset += itemSizes[index]!.height; @@ -718,7 +717,6 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate { // x = position.left; // } else { // Menu button is equidistant from both edges, so grow in reading direction. - assert(textDirection != null); switch (textDirection) { case TextDirection.rtl: x = size.width - position.right - childSize.width; @@ -881,6 +879,103 @@ class _PopupMenuRoute extends PopupRoute { } } +class PopupMenu extends StatelessWidget { + PopupMenu({ + Key? key, + required this.items, + this.initialValue, + this.semanticLabel, + this.constraints, + }) : itemSizes = List.filled(items.length, null), + super(key: key); + + final List> items; + final List itemSizes; + final T? initialValue; + final String? semanticLabel; + final BoxConstraints? constraints; + + Widget _buildMenu(BuildContext context) { + final List children = []; + for (int i = 0; i < items.length; i += 1) { + Widget item = items[i]; + if (initialValue != null && items[i].represents(initialValue)) { + item = Container( + color: Theme.of(context).highlightColor, + child: item, + ); + } + children.add( + _MenuItem( + onLayout: (Size size) { + itemSizes[i] = size; + }, + child: item, + ), + ); + } + + final child = ConstrainedBox( + constraints: constraints ?? + const BoxConstraints( + minWidth: _kMenuMinWidth, + maxWidth: _kMenuMaxWidth, + ), + child: IntrinsicWidth( + stepWidth: _kMenuWidthStep, + child: Semantics( + scopesRoute: true, + namesRoute: true, + explicitChildNodes: true, + label: semanticLabel, + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric( + vertical: _kMenuVerticalPadding, + ), + controller: ScrollController(), + child: ListBody(children: children), + ), + ), + ), + ); + + final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); + return Material( + shape: popupMenuTheme.shape, + color: popupMenuTheme.color, + type: MaterialType.card, + elevation: popupMenuTheme.elevation ?? 8.0, + child: child, + ); + } + + @override + Widget build(BuildContext context) { + int? selectedItemIndex; + if (initialValue != null) { + for (int index = 0; + selectedItemIndex == null && index < items.length; + index += 1) { + if (items[index].represents(initialValue)) selectedItemIndex = index; + } + } + + return MediaQuery.removePadding( + context: context, + removeTop: true, + removeBottom: true, + removeLeft: true, + removeRight: true, + child: Builder( + builder: (BuildContext context) { + return InheritedTheme.capture(from: context, to: context) + .wrap(_buildMenu(context)); + }, + ), + ); + } +} + /// Show a popup menu that contains the `items` at `position`. /// /// `items` should be non-null and not empty. @@ -948,10 +1043,7 @@ Future showMenu({ bool useRootNavigator = false, BoxConstraints? constraints, }) { - assert(context != null); - assert(position != null); - assert(useRootNavigator != null); - assert(items != null && items.isNotEmpty); + assert(items.isNotEmpty); assert(debugCheckHasMaterialLocalizations(context)); switch (Theme.of(context).platform) { @@ -1050,9 +1142,7 @@ class PopupMenuButton extends StatefulWidget { this.enableFeedback, this.constraints, this.position = PopupMenuPosition.over, - }) : assert(itemBuilder != null), - assert(enabled != null), - assert( + }) : assert( !(child != null && icon != null), 'You can only pass [child] or [icon], not both.', ), @@ -1310,6 +1400,7 @@ class PopupMenuButtonState extends State> { // This MaterialStateProperty is passed along to the menu item's InkWell which // resolves the property against MaterialState.disabled, MaterialState.hovered, // MaterialState.focused. +// ignore: unused_element class _EffectiveMouseCursor extends MaterialStateMouseCursor { const _EffectiveMouseCursor(this.widgetCursor, this.themeCursor); diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 6a9bbeb1a..26cc26ddd 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -31,6 +31,7 @@ class _MenubarTheme { class RemoteMenubar extends StatefulWidget { final String id; final FFI ffi; + final RxBool show; final Function(Function(bool)) onEnterOrLeaveImageSetter; final Function() onEnterOrLeaveImageCleaner; @@ -38,6 +39,7 @@ class RemoteMenubar extends StatefulWidget { Key? key, required this.id, required this.ffi, + required this.show, required this.onEnterOrLeaveImageSetter, required this.onEnterOrLeaveImageCleaner, }) : super(key: key); @@ -47,7 +49,6 @@ class RemoteMenubar extends StatefulWidget { } class _RemoteMenubarState extends State { - final RxBool _show = false.obs; final Rx _hideColor = Colors.white12.obs; final _rxHideReplay = rxdart.ReplaySubject(); final _pinMenubar = false.obs; @@ -62,6 +63,8 @@ class _RemoteMenubarState extends State { setState(() {}); } + RxBool get show => widget.show; + @override initState() { super.initState(); @@ -79,8 +82,8 @@ class _RemoteMenubarState extends State { .throttleTime(const Duration(milliseconds: 5000), trailing: true, leading: false) .listen((int v) { - if (_pinMenubar.isFalse && _show.isTrue && _isCursorOverImage) { - _show.value = false; + if (_pinMenubar.isFalse && show.isTrue && _isCursorOverImage) { + show.value = false; } }); } @@ -97,13 +100,13 @@ class _RemoteMenubarState extends State { return Align( alignment: Alignment.topCenter, child: Obx( - () => _show.value ? _buildMenubar(context) : _buildShowHide(context)), + () => show.value ? _buildMenubar(context) : _buildShowHide(context)), ); } Widget _buildShowHide(BuildContext context) { return Obx(() => Tooltip( - message: translate(_show.value ? "Hide Menubar" : "Show Menubar"), + message: translate(show.value ? 'Hide Menubar' : 'Show Menubar'), child: SizedBox( width: 100, height: 13, @@ -112,9 +115,9 @@ class _RemoteMenubarState extends State { _hideColor.value = v ? Colors.white60 : Colors.white24; }, onPressed: () { - _show.value = !_show.value; + show.value = !show.value; _hideColor.value = Colors.white24; - if (_show.isTrue) { + if (show.isTrue) { _updateScreen(); } }, @@ -517,7 +520,6 @@ class _RemoteMenubarState extends State { ); } displayMenu.add(MenuEntryDivider()); - if (perms['keyboard'] != false) { if (pi.platform == 'Linux' || pi.sasEnabled) { displayMenu.add(MenuEntryButton( diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index c21f58e4b..8b7ee4d0e 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'dart:async'; import 'dart:math'; +import 'dart:ui' as ui; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/gestures.dart'; @@ -15,6 +16,7 @@ import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart'; import 'package:scroll_pos/scroll_pos.dart'; import 'package:window_manager/window_manager.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:bot_toast/bot_toast.dart'; import '../../utils/multi_window_manager.dart'; @@ -66,6 +68,26 @@ class DesktopTabState { } } +CancelFunc showRightMenu(ToastBuilder builder, + {BuildContext? context, Offset? target}) { + return BotToast.showAttachedWidget( + target: target, + targetContext: context, + verticalOffset: 0, + horizontalOffset: 0, + duration: Duration(seconds: 4), + animationDuration: Duration(milliseconds: 0), + animationReverseDuration: Duration(milliseconds: 0), + preferDirection: PreferDirection.rightTop, + ignoreContentClick: false, + onlyOne: true, + allowClick: true, + enableSafeArea: true, + backgroundColor: Color(0x00000000), + attachedBuilder: builder, + ); +} + class DesktopTabController { final state = DesktopTabState().obs; final DesktopTabType tabType; @@ -174,6 +196,7 @@ class TabThemeConf { typedef TabBuilder = Widget Function( String key, Widget icon, Widget label, TabThemeConf themeConf); +typedef TabMenuBuilder = Widget Function(String key); typedef LabelGetter = Rx Function(String key); /// [_lastClickTime], help to handle double click @@ -187,6 +210,8 @@ class DesktopTab extends StatelessWidget { final bool showMaximize; final bool showClose; final Widget Function(Widget pageView)? pageViewBuilder; + // Right click tab menu + final TabMenuBuilder? tabMenuBuilder; final Widget? tail; final Future Function()? onWindowCloseButton; final TabBuilder? tabBuilder; @@ -213,6 +238,7 @@ class DesktopTab extends StatelessWidget { this.showMaximize = true, this.showClose = true, this.pageViewBuilder, + this.tabMenuBuilder, this.tail, this.onWindowCloseButton, this.tabBuilder, @@ -362,6 +388,7 @@ class DesktopTab extends StatelessWidget { child: _ListView( controller: controller, tabBuilder: tabBuilder, + tabMenuBuilder: tabMenuBuilder, labelGetter: labelGetter, maxLabelWidth: maxLabelWidth, selectedTabBackgroundColor: @@ -619,6 +646,7 @@ class _ListView extends StatelessWidget { final DesktopTabController controller; final TabBuilder? tabBuilder; + final TabMenuBuilder? tabMenuBuilder; final LabelGetter? labelGetter; final double? maxLabelWidth; final Color? selectedTabBackgroundColor; @@ -626,13 +654,15 @@ class _ListView extends StatelessWidget { Rx get state => controller.state; - const _ListView( - {required this.controller, - this.tabBuilder, - this.labelGetter, - this.maxLabelWidth, - this.selectedTabBackgroundColor, - this.unSelectedTabBackgroundColor}); + const _ListView({ + required this.controller, + this.tabBuilder, + this.tabMenuBuilder, + this.labelGetter, + this.maxLabelWidth, + this.selectedTabBackgroundColor, + this.unSelectedTabBackgroundColor, + }); /// Check whether to show ListView /// @@ -678,6 +708,7 @@ class _ListView extends StatelessWidget { tab.onTap?.call(); }, tabBuilder: tabBuilder, + tabMenuBuilder: tabMenuBuilder, maxLabelWidth: maxLabelWidth, selectedTabBackgroundColor: selectedTabBackgroundColor, unSelectedTabBackgroundColor: unSelectedTabBackgroundColor, @@ -697,6 +728,7 @@ class _Tab extends StatefulWidget { final Function() onClose; final Function() onTap; final TabBuilder? tabBuilder; + final TabMenuBuilder? tabMenuBuilder; final double? maxLabelWidth; final Color? selectedTabBackgroundColor; final Color? unSelectedTabBackgroundColor; @@ -709,6 +741,7 @@ class _Tab extends StatefulWidget { this.selectedIcon, this.unselectedIcon, this.tabBuilder, + this.tabMenuBuilder, required this.closable, required this.selected, required this.onClose, @@ -753,18 +786,43 @@ class _TabState extends State<_Tab> with RestorationMixin { )); }); - if (widget.tabBuilder == null) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ + Widget getWidgetWithBuilder() { + if (widget.tabBuilder == null) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + icon, + labelWidget, + ], + ); + } else { + return widget.tabBuilder!( + widget.tabInfoKey, icon, labelWidget, - ], - ); - } else { - return widget.tabBuilder!(widget.tabInfoKey, icon, labelWidget, - TabThemeConf(iconSize: _kIconSize)); + TabThemeConf(iconSize: _kIconSize), + ); + } } + + return Listener( + onPointerDown: (e) { + if (e.kind != ui.PointerDeviceKind.mouse) { + return; + } + if (e.buttons == 2) { + if (widget.tabMenuBuilder != null) { + showRightMenu( + (cacel) { + return widget.tabMenuBuilder!(widget.tabInfoKey); + }, + target: e.position, + ); + } + } + }, + child: getWidgetWithBuilder(), + ); } @override @@ -781,35 +839,36 @@ class _TabState extends State<_Tab> with RestorationMixin { }, onTap: () => widget.onTap(), child: Container( - color: isSelected - ? widget.selectedTabBackgroundColor - : widget.unSelectedTabBackgroundColor, - child: Row( - children: [ - SizedBox( - height: _kTabBarHeight, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - _buildTabContent(), - Obx((() => _CloseButton( - visiable: hover.value && widget.closable, - tabSelected: isSelected, - onClose: () => widget.onClose(), - ))) - ])).paddingSymmetric(horizontal: 10), - Offstage( - offstage: !showDivider, - child: VerticalDivider( - width: 1, - indent: _kDividerIndent, - endIndent: _kDividerIndent, - color: MyTheme.tabbar(context).dividerColor, - thickness: 1, - ), - ) - ], - )), + color: isSelected + ? widget.selectedTabBackgroundColor + : widget.unSelectedTabBackgroundColor, + child: Row( + children: [ + SizedBox( + height: _kTabBarHeight, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _buildTabContent(), + Obx((() => _CloseButton( + visiable: hover.value && widget.closable, + tabSelected: isSelected, + onClose: () => widget.onClose(), + ))) + ])).paddingSymmetric(horizontal: 10), + Offstage( + offstage: !showDivider, + child: VerticalDivider( + width: 1, + indent: _kDividerIndent, + endIndent: _kDividerIndent, + color: MyTheme.tabbar(context).dividerColor, + thickness: 1, + ), + ) + ], + ), + ), ), ); } diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index aaa5e1da5..9aa1a23ce 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -38,8 +38,8 @@ class ChatModel with ChangeNotifier { firstName: "Me", ); - late final Map _messages = Map() - ..[clientModeID] = MessageBody(me, []); + late final Map _messages = {}..[clientModeID] = + MessageBody(me, []); var _currentID = clientModeID; late bool _isShowChatPage = false; diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index f74ca620d..6671ec669 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -10,7 +10,7 @@ import 'package:window_manager/window_manager.dart'; import '../common.dart'; import '../common/formatter/id_formatter.dart'; -import '../desktop/pages/server_page.dart' as Desktop; +import '../desktop/pages/server_page.dart' as desktop; import '../desktop/widgets/tabbar_widget.dart'; import '../mobile/pages/server_page.dart'; import 'model.dart'; @@ -261,7 +261,7 @@ class ServerModel with ChangeNotifier { } /// Start the screen sharing service. - Future startService() async { + Future startService() async { _isStart = true; notifyListeners(); parent.target?.ffiModel.updateEventListener(""); @@ -276,7 +276,7 @@ class ServerModel with ChangeNotifier { } /// Stop the screen sharing service. - Future stopService() async { + Future stopService() async { _isStart = false; closeAll(); await parent.target?.invokeMethod("stop_service"); @@ -288,7 +288,7 @@ class ServerModel with ChangeNotifier { } } - Future initInput() async { + Future initInput() async { await parent.target?.invokeMethod("init_input"); } @@ -412,7 +412,7 @@ class ServerModel with ChangeNotifier { } } }, - page: Desktop.buildConnectionCard(client))); + page: desktop.buildConnectionCard(client))); Future.delayed(Duration.zero, () async { window_on_top(null); }); @@ -521,9 +521,9 @@ class ServerModel with ChangeNotifier { } closeAll() { - _clients.forEach((client) { + for (var client in _clients) { bind.cmCloseConnection(connId: client.id); - }); + } _clients.clear(); tabController.state.value.tabs.clear(); } diff --git a/flutter/lib/models/web_model.dart b/flutter/lib/models/web_model.dart index d3f1bacad..291e21a9c 100644 --- a/flutter/lib/models/web_model.dart +++ b/flutter/lib/models/web_model.dart @@ -25,7 +25,7 @@ class PlatformFFI { static get localeName => window.navigator.language; - static Future init(String _appType) async { + static Future init(String _appType) async { isWeb = true; isWebDesktop = !context.callMethod('isMobile'); context.callMethod('init'); @@ -57,13 +57,13 @@ class PlatformFFI { } static void stopDesktopWebListener() { - mouseListeners.forEach((l) { - l.cancel(); - }); + for (var ml in mouseListeners) { + ml.cancel(); + } mouseListeners.clear(); - keyListeners.forEach((l) { - l.cancel(); - }); + for (var kl in keyListeners) { + kl.cancel(); + } keyListeners.clear(); }