Merge pull request #1944 from fufesou/flutter_desktop_tab_menu

Flutter desktop tab menu
This commit is contained in:
RustDesk 2022-11-04 01:59:15 +08:00 committed by GitHub
commit 42849e3759
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 519 additions and 227 deletions

View File

@ -86,7 +86,7 @@ class IconFont {
static const IconData add = IconData(0xe664, fontFamily: _family1); static const IconData add = IconData(0xe664, fontFamily: _family1);
static const IconData menu = IconData(0xe628, fontFamily: _family1); static const IconData menu = IconData(0xe628, fontFamily: _family1);
static const IconData search = IconData(0xe6a4, fontFamily: _family2); 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<ColorThemeExtension> { class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
@ -1330,11 +1330,8 @@ Future<Map<String, String>> getHttpHeaders() async {
// Simple wrapper of built-in types for refrence use. // Simple wrapper of built-in types for refrence use.
class SimpleWrapper<T> { class SimpleWrapper<T> {
T t; T value;
SimpleWrapper(this.t); SimpleWrapper(this.value);
T get value => t;
set value(T t) => this.t = t;
} }
/// call this to reload current window. /// call this to reload current window.

View File

@ -25,15 +25,24 @@ bool _isCustomCursorInited = false;
final SimpleWrapper<bool> _firstEnterImage = SimpleWrapper(false); final SimpleWrapper<bool> _firstEnterImage = SimpleWrapper(false);
class RemotePage extends StatefulWidget { class RemotePage extends StatefulWidget {
const RemotePage({ RemotePage({
Key? key, Key? key,
required this.id, required this.id,
}) : super(key: key); }) : super(key: key);
final String id; final String id;
final SimpleWrapper<State<RemotePage>?> _lastState = SimpleWrapper(null);
FFI get ffi => (_lastState.value! as _RemotePageState)._ffi;
RxBool get showMenubar =>
(_lastState.value! as _RemotePageState)._showMenubar;
@override @override
State<RemotePage> createState() => _RemotePageState(); State<RemotePage> createState() {
final state = _RemotePageState();
_lastState.value = state;
return state;
}
} }
class _RemotePageState extends State<RemotePage> class _RemotePageState extends State<RemotePage>
@ -41,6 +50,7 @@ class _RemotePageState extends State<RemotePage>
Timer? _timer; Timer? _timer;
String keyboardMode = "legacy"; String keyboardMode = "legacy";
final _cursorOverImage = false.obs; final _cursorOverImage = false.obs;
final _showMenubar = false.obs;
late RxBool _showRemoteCursor; late RxBool _showRemoteCursor;
late RxBool _remoteCursorMoved; late RxBool _remoteCursorMoved;
late RxBool _keyboardEnabled; late RxBool _keyboardEnabled;
@ -229,6 +239,7 @@ class _RemotePageState extends State<RemotePage>
paints.add(RemoteMenubar( paints.add(RemoteMenubar(
id: widget.id, id: widget.id,
ffi: _ffi, ffi: _ffi,
show: _showMenubar,
onEnterOrLeaveImageSetter: (func) => _onEnterOrLeaveImage4Menubar = func, onEnterOrLeaveImageSetter: (func) => _onEnterOrLeaveImage4Menubar = func,
onEnterOrLeaveImageCleaner: () => _onEnterOrLeaveImage4Menubar = null, onEnterOrLeaveImageCleaner: () => _onEnterOrLeaveImage4Menubar = null,
)); ));

View File

@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:ui' as ui;
import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.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/models/state_model.dart';
import 'package:flutter_hbb/desktop/pages/remote_page.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/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_hbb/utils/multi_window_manager.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:bot_toast/bot_toast.dart';
import '../../models/platform_model.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 { class ConnectionTabPage extends StatefulWidget {
final Map<String, dynamic> params; final Map<String, dynamic> params;
@ -123,7 +135,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
connectionType.secure.value == ConnectionType.strSecure connectionType.secure.value == ConnectionType.strSecure
? 'Secure Connection' ? 'Secure Connection'
: 'Insecure Connection'); : 'Insecure Connection');
return Row( final tab = Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
icon, icon,
@ -138,6 +150,23 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
label, 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<ConnectionTabPage> {
); );
} }
// to-do: some dup code to ../widgets/remote_menubar
Widget _tabMenuBuilder(String key, CancelFunc cancelFunc) {
final List<MenuEntryBase<String>> 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<String>(
childBuilder: (TextStyle? style) => Text(
translate('Close'),
style: style,
),
proc: () {
tabController.closeBy(key);
cancelFunc();
},
padding: padding,
),
MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Obx(() => Text(
translate(showMenuBar.isTrue ? 'Hide Menubar' : 'Show Menubar'),
style: style,
)),
proc: () {
showMenuBar.value = !showMenuBar.value;
cancelFunc();
},
padding: padding,
),
MenuEntryDivider<String>(),
MenuEntryRadios<String>(
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<String>(),
MenuEntryRadios<String>(
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<String>(),
() {
final state = ShowRemoteCursorState.find(key);
return MenuEntrySwitch2<String>(
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<String>(
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<String>(
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) { void onRemoveId(String id) {
if (tabController.state.value.tabs.isEmpty) { if (tabController.state.value.tabs.isEmpty) {
WindowController.fromWindowId(windowId()).hide(); WindowController.fromWindowId(windowId()).hide();

View File

@ -139,8 +139,7 @@ class _MenuItem extends SingleChildRenderObjectWidget {
Key? key, Key? key,
required this.onLayout, required this.onLayout,
required Widget? child, required Widget? child,
}) : assert(onLayout != null), }) : super(key: key, child: child);
super(key: key, child: child);
final ValueChanged<Size> onLayout; final ValueChanged<Size> onLayout;
@ -157,9 +156,7 @@ class _MenuItem extends SingleChildRenderObjectWidget {
} }
class _RenderMenuItem extends RenderShiftedBox { class _RenderMenuItem extends RenderShiftedBox {
_RenderMenuItem(this.onLayout, [RenderBox? child]) _RenderMenuItem(this.onLayout, [RenderBox? child]) : super(child);
: assert(onLayout != null),
super(child);
ValueChanged<Size> onLayout; ValueChanged<Size> onLayout;
@ -240,9 +237,7 @@ class PopupMenuItem<T> extends PopupMenuEntry<T> {
this.textStyle, this.textStyle,
this.mouseCursor, this.mouseCursor,
required this.child, required this.child,
}) : assert(enabled != null), }) : super(key: key);
assert(height != null),
super(key: key);
/// The value that will be returned by [showMenu] if this entry is selected. /// The value that will be returned by [showMenu] if this entry is selected.
final T? value; final T? value;
@ -382,11 +377,15 @@ class PopupMenuItemState<T, W extends PopupMenuItem<T>> extends State<W> {
child: Semantics( child: Semantics(
enabled: widget.enabled, enabled: widget.enabled,
button: true, button: true,
child: InkWell( // child: InkWell(
onTap: widget.enabled ? handleTap : null, // onTap: widget.enabled ? handleTap : null,
canRequestFocus: widget.enabled, // canRequestFocus: widget.enabled,
mouseCursor: _EffectiveMouseCursor( // mouseCursor: _EffectiveMouseCursor(
widget.mouseCursor, popupMenuTheme.mouseCursor), // widget.mouseCursor, popupMenuTheme.mouseCursor),
// child: item,
// ),
child: TextButton(
onPressed: widget.enabled ? handleTap : null,
child: item, child: item,
), ),
), ),
@ -471,8 +470,7 @@ class CheckedPopupMenuItem<T> extends PopupMenuItem<T> {
EdgeInsets? padding, EdgeInsets? padding,
double height = kMinInteractiveDimension, double height = kMinInteractiveDimension,
Widget? child, Widget? child,
}) : assert(checked != null), }) : super(
super(
key: key, key: key,
value: value, value: value,
enabled: enabled, enabled: enabled,
@ -524,10 +522,11 @@ class _CheckedPopupMenuItemState<T>
@override @override
void handleTap() { void handleTap() {
// This fades the checkmark in or out when tapped. // This fades the checkmark in or out when tapped.
if (widget.checked) if (widget.checked) {
_controller.reverse(); _controller.reverse();
else } else {
_controller.forward(); _controller.forward();
}
super.handleTap(); super.handleTap();
} }
@ -699,7 +698,7 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate {
final double buttonHeight = size.height - position.top - position.bottom; final double buttonHeight = size.height - position.top - position.bottom;
// Find the ideal vertical position. // Find the ideal vertical position.
double y = position.top; double y = position.top;
if (selectedItemIndex != null && itemSizes != null) { if (selectedItemIndex != null) {
double selectedItemOffset = _kMenuVerticalPadding; double selectedItemOffset = _kMenuVerticalPadding;
for (int index = 0; index < selectedItemIndex!; index += 1) { for (int index = 0; index < selectedItemIndex!; index += 1) {
selectedItemOffset += itemSizes[index]!.height; selectedItemOffset += itemSizes[index]!.height;
@ -718,7 +717,6 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate {
// x = position.left; // x = position.left;
// } else { // } else {
// Menu button is equidistant from both edges, so grow in reading direction. // Menu button is equidistant from both edges, so grow in reading direction.
assert(textDirection != null);
switch (textDirection) { switch (textDirection) {
case TextDirection.rtl: case TextDirection.rtl:
x = size.width - position.right - childSize.width; x = size.width - position.right - childSize.width;
@ -881,6 +879,103 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
} }
} }
class PopupMenu<T> extends StatelessWidget {
PopupMenu({
Key? key,
required this.items,
this.initialValue,
this.semanticLabel,
this.constraints,
}) : itemSizes = List<Size?>.filled(items.length, null),
super(key: key);
final List<PopupMenuEntry<T>> items;
final List<Size?> itemSizes;
final T? initialValue;
final String? semanticLabel;
final BoxConstraints? constraints;
Widget _buildMenu(BuildContext context) {
final List<Widget> children = <Widget>[];
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`. /// Show a popup menu that contains the `items` at `position`.
/// ///
/// `items` should be non-null and not empty. /// `items` should be non-null and not empty.
@ -948,10 +1043,7 @@ Future<T?> showMenu<T>({
bool useRootNavigator = false, bool useRootNavigator = false,
BoxConstraints? constraints, BoxConstraints? constraints,
}) { }) {
assert(context != null); assert(items.isNotEmpty);
assert(position != null);
assert(useRootNavigator != null);
assert(items != null && items.isNotEmpty);
assert(debugCheckHasMaterialLocalizations(context)); assert(debugCheckHasMaterialLocalizations(context));
switch (Theme.of(context).platform) { switch (Theme.of(context).platform) {
@ -1050,9 +1142,7 @@ class PopupMenuButton<T> extends StatefulWidget {
this.enableFeedback, this.enableFeedback,
this.constraints, this.constraints,
this.position = PopupMenuPosition.over, this.position = PopupMenuPosition.over,
}) : assert(itemBuilder != null), }) : assert(
assert(enabled != null),
assert(
!(child != null && icon != null), !(child != null && icon != null),
'You can only pass [child] or [icon], not both.', 'You can only pass [child] or [icon], not both.',
), ),
@ -1310,6 +1400,7 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
// This MaterialStateProperty is passed along to the menu item's InkWell which // This MaterialStateProperty is passed along to the menu item's InkWell which
// resolves the property against MaterialState.disabled, MaterialState.hovered, // resolves the property against MaterialState.disabled, MaterialState.hovered,
// MaterialState.focused. // MaterialState.focused.
// ignore: unused_element
class _EffectiveMouseCursor extends MaterialStateMouseCursor { class _EffectiveMouseCursor extends MaterialStateMouseCursor {
const _EffectiveMouseCursor(this.widgetCursor, this.themeCursor); const _EffectiveMouseCursor(this.widgetCursor, this.themeCursor);

View File

@ -31,6 +31,7 @@ class _MenubarTheme {
class RemoteMenubar extends StatefulWidget { class RemoteMenubar extends StatefulWidget {
final String id; final String id;
final FFI ffi; final FFI ffi;
final RxBool show;
final Function(Function(bool)) onEnterOrLeaveImageSetter; final Function(Function(bool)) onEnterOrLeaveImageSetter;
final Function() onEnterOrLeaveImageCleaner; final Function() onEnterOrLeaveImageCleaner;
@ -38,6 +39,7 @@ class RemoteMenubar extends StatefulWidget {
Key? key, Key? key,
required this.id, required this.id,
required this.ffi, required this.ffi,
required this.show,
required this.onEnterOrLeaveImageSetter, required this.onEnterOrLeaveImageSetter,
required this.onEnterOrLeaveImageCleaner, required this.onEnterOrLeaveImageCleaner,
}) : super(key: key); }) : super(key: key);
@ -47,7 +49,6 @@ class RemoteMenubar extends StatefulWidget {
} }
class _RemoteMenubarState extends State<RemoteMenubar> { class _RemoteMenubarState extends State<RemoteMenubar> {
final RxBool _show = false.obs;
final Rx<Color> _hideColor = Colors.white12.obs; final Rx<Color> _hideColor = Colors.white12.obs;
final _rxHideReplay = rxdart.ReplaySubject<int>(); final _rxHideReplay = rxdart.ReplaySubject<int>();
final _pinMenubar = false.obs; final _pinMenubar = false.obs;
@ -62,6 +63,8 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
setState(() {}); setState(() {});
} }
RxBool get show => widget.show;
@override @override
initState() { initState() {
super.initState(); super.initState();
@ -79,8 +82,8 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
.throttleTime(const Duration(milliseconds: 5000), .throttleTime(const Duration(milliseconds: 5000),
trailing: true, leading: false) trailing: true, leading: false)
.listen((int v) { .listen((int v) {
if (_pinMenubar.isFalse && _show.isTrue && _isCursorOverImage) { if (_pinMenubar.isFalse && show.isTrue && _isCursorOverImage) {
_show.value = false; show.value = false;
} }
}); });
} }
@ -97,13 +100,13 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
return Align( return Align(
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
child: Obx( child: Obx(
() => _show.value ? _buildMenubar(context) : _buildShowHide(context)), () => show.value ? _buildMenubar(context) : _buildShowHide(context)),
); );
} }
Widget _buildShowHide(BuildContext context) { Widget _buildShowHide(BuildContext context) {
return Obx(() => Tooltip( return Obx(() => Tooltip(
message: translate(_show.value ? "Hide Menubar" : "Show Menubar"), message: translate(show.value ? 'Hide Menubar' : 'Show Menubar'),
child: SizedBox( child: SizedBox(
width: 100, width: 100,
height: 13, height: 13,
@ -112,9 +115,9 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
_hideColor.value = v ? Colors.white60 : Colors.white24; _hideColor.value = v ? Colors.white60 : Colors.white24;
}, },
onPressed: () { onPressed: () {
_show.value = !_show.value; show.value = !show.value;
_hideColor.value = Colors.white24; _hideColor.value = Colors.white24;
if (_show.isTrue) { if (show.isTrue) {
_updateScreen(); _updateScreen();
} }
}, },
@ -517,7 +520,6 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
); );
} }
displayMenu.add(MenuEntryDivider()); displayMenu.add(MenuEntryDivider());
if (perms['keyboard'] != false) { if (perms['keyboard'] != false) {
if (pi.platform == 'Linux' || pi.sasEnabled) { if (pi.platform == 'Linux' || pi.sasEnabled) {
displayMenu.add(MenuEntryButton<String>( displayMenu.add(MenuEntryButton<String>(

View File

@ -1,6 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'dart:async'; import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'dart:ui' as ui;
import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/gestures.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:scroll_pos/scroll_pos.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:bot_toast/bot_toast.dart';
import '../../utils/multi_window_manager.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 { class DesktopTabController {
final state = DesktopTabState().obs; final state = DesktopTabState().obs;
final DesktopTabType tabType; final DesktopTabType tabType;
@ -174,6 +196,7 @@ class TabThemeConf {
typedef TabBuilder = Widget Function( typedef TabBuilder = Widget Function(
String key, Widget icon, Widget label, TabThemeConf themeConf); String key, Widget icon, Widget label, TabThemeConf themeConf);
typedef TabMenuBuilder = Widget Function(String key);
typedef LabelGetter = Rx<String> Function(String key); typedef LabelGetter = Rx<String> Function(String key);
/// [_lastClickTime], help to handle double click /// [_lastClickTime], help to handle double click
@ -187,6 +210,8 @@ class DesktopTab extends StatelessWidget {
final bool showMaximize; final bool showMaximize;
final bool showClose; final bool showClose;
final Widget Function(Widget pageView)? pageViewBuilder; final Widget Function(Widget pageView)? pageViewBuilder;
// Right click tab menu
final TabMenuBuilder? tabMenuBuilder;
final Widget? tail; final Widget? tail;
final Future<bool> Function()? onWindowCloseButton; final Future<bool> Function()? onWindowCloseButton;
final TabBuilder? tabBuilder; final TabBuilder? tabBuilder;
@ -213,6 +238,7 @@ class DesktopTab extends StatelessWidget {
this.showMaximize = true, this.showMaximize = true,
this.showClose = true, this.showClose = true,
this.pageViewBuilder, this.pageViewBuilder,
this.tabMenuBuilder,
this.tail, this.tail,
this.onWindowCloseButton, this.onWindowCloseButton,
this.tabBuilder, this.tabBuilder,
@ -362,6 +388,7 @@ class DesktopTab extends StatelessWidget {
child: _ListView( child: _ListView(
controller: controller, controller: controller,
tabBuilder: tabBuilder, tabBuilder: tabBuilder,
tabMenuBuilder: tabMenuBuilder,
labelGetter: labelGetter, labelGetter: labelGetter,
maxLabelWidth: maxLabelWidth, maxLabelWidth: maxLabelWidth,
selectedTabBackgroundColor: selectedTabBackgroundColor:
@ -619,6 +646,7 @@ class _ListView extends StatelessWidget {
final DesktopTabController controller; final DesktopTabController controller;
final TabBuilder? tabBuilder; final TabBuilder? tabBuilder;
final TabMenuBuilder? tabMenuBuilder;
final LabelGetter? labelGetter; final LabelGetter? labelGetter;
final double? maxLabelWidth; final double? maxLabelWidth;
final Color? selectedTabBackgroundColor; final Color? selectedTabBackgroundColor;
@ -626,13 +654,15 @@ class _ListView extends StatelessWidget {
Rx<DesktopTabState> get state => controller.state; Rx<DesktopTabState> get state => controller.state;
const _ListView( const _ListView({
{required this.controller, required this.controller,
this.tabBuilder, this.tabBuilder,
this.labelGetter, this.tabMenuBuilder,
this.maxLabelWidth, this.labelGetter,
this.selectedTabBackgroundColor, this.maxLabelWidth,
this.unSelectedTabBackgroundColor}); this.selectedTabBackgroundColor,
this.unSelectedTabBackgroundColor,
});
/// Check whether to show ListView /// Check whether to show ListView
/// ///
@ -678,6 +708,7 @@ class _ListView extends StatelessWidget {
tab.onTap?.call(); tab.onTap?.call();
}, },
tabBuilder: tabBuilder, tabBuilder: tabBuilder,
tabMenuBuilder: tabMenuBuilder,
maxLabelWidth: maxLabelWidth, maxLabelWidth: maxLabelWidth,
selectedTabBackgroundColor: selectedTabBackgroundColor, selectedTabBackgroundColor: selectedTabBackgroundColor,
unSelectedTabBackgroundColor: unSelectedTabBackgroundColor, unSelectedTabBackgroundColor: unSelectedTabBackgroundColor,
@ -697,6 +728,7 @@ class _Tab extends StatefulWidget {
final Function() onClose; final Function() onClose;
final Function() onTap; final Function() onTap;
final TabBuilder? tabBuilder; final TabBuilder? tabBuilder;
final TabMenuBuilder? tabMenuBuilder;
final double? maxLabelWidth; final double? maxLabelWidth;
final Color? selectedTabBackgroundColor; final Color? selectedTabBackgroundColor;
final Color? unSelectedTabBackgroundColor; final Color? unSelectedTabBackgroundColor;
@ -709,6 +741,7 @@ class _Tab extends StatefulWidget {
this.selectedIcon, this.selectedIcon,
this.unselectedIcon, this.unselectedIcon,
this.tabBuilder, this.tabBuilder,
this.tabMenuBuilder,
required this.closable, required this.closable,
required this.selected, required this.selected,
required this.onClose, required this.onClose,
@ -753,18 +786,43 @@ class _TabState extends State<_Tab> with RestorationMixin {
)); ));
}); });
if (widget.tabBuilder == null) { Widget getWidgetWithBuilder() {
return Row( if (widget.tabBuilder == null) {
mainAxisAlignment: MainAxisAlignment.center, return Row(
children: [ mainAxisAlignment: MainAxisAlignment.center,
children: [
icon,
labelWidget,
],
);
} else {
return widget.tabBuilder!(
widget.tabInfoKey,
icon, icon,
labelWidget, labelWidget,
], TabThemeConf(iconSize: _kIconSize),
); );
} else { }
return widget.tabBuilder!(widget.tabInfoKey, icon, labelWidget,
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 @override
@ -781,35 +839,36 @@ class _TabState extends State<_Tab> with RestorationMixin {
}, },
onTap: () => widget.onTap(), onTap: () => widget.onTap(),
child: Container( child: Container(
color: isSelected color: isSelected
? widget.selectedTabBackgroundColor ? widget.selectedTabBackgroundColor
: widget.unSelectedTabBackgroundColor, : widget.unSelectedTabBackgroundColor,
child: Row( child: Row(
children: [ children: [
SizedBox( SizedBox(
height: _kTabBarHeight, height: _kTabBarHeight,
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
_buildTabContent(), _buildTabContent(),
Obx((() => _CloseButton( Obx((() => _CloseButton(
visiable: hover.value && widget.closable, visiable: hover.value && widget.closable,
tabSelected: isSelected, tabSelected: isSelected,
onClose: () => widget.onClose(), onClose: () => widget.onClose(),
))) )))
])).paddingSymmetric(horizontal: 10), ])).paddingSymmetric(horizontal: 10),
Offstage( Offstage(
offstage: !showDivider, offstage: !showDivider,
child: VerticalDivider( child: VerticalDivider(
width: 1, width: 1,
indent: _kDividerIndent, indent: _kDividerIndent,
endIndent: _kDividerIndent, endIndent: _kDividerIndent,
color: MyTheme.tabbar(context).dividerColor, color: MyTheme.tabbar(context).dividerColor,
thickness: 1, thickness: 1,
), ),
) )
], ],
)), ),
),
), ),
); );
} }

View File

@ -16,6 +16,7 @@ import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'package:bot_toast/bot_toast.dart';
// import 'package:window_manager/window_manager.dart'; // import 'package:window_manager/window_manager.dart';
@ -53,15 +54,27 @@ Future<void> main(List<String> args) async {
switch (wType) { switch (wType) {
case WindowType.RemoteDesktop: case WindowType.RemoteDesktop:
desktopType = DesktopType.remote; desktopType = DesktopType.remote;
runRemoteScreen(argument); runMultiWindow(
argument,
kAppTypeDesktopRemote,
'RustDesk - Remote Desktop',
);
break; break;
case WindowType.FileTransfer: case WindowType.FileTransfer:
desktopType = DesktopType.fileTransfer; desktopType = DesktopType.fileTransfer;
runFileTransferScreen(argument); runMultiWindow(
argument,
kAppTypeDesktopFileTransfer,
'RustDesk - File Transfer',
);
break; break;
case WindowType.PortForward: case WindowType.PortForward:
desktopType = DesktopType.portForward; desktopType = DesktopType.portForward;
runPortForwardScreen(argument); runMultiWindow(
argument,
kAppTypeDesktopPortForward,
'RustDesk - Port Forward',
);
break; break;
default: default:
break; break;
@ -120,84 +133,18 @@ void runMobileApp() async {
runApp(App()); runApp(App());
} }
void runRemoteScreen(Map<String, dynamic> argument) async { void runMultiWindow(
await initEnv(kAppTypeDesktopRemote); Map<String, dynamic> argument,
runApp(RefreshWrapper( String appType,
builder: (context) => GetMaterialApp( String title,
navigatorKey: globalKey, ) async {
debugShowCheckedModeBanner: false, await initEnv(appType);
title: 'RustDesk - Remote Desktop', _runApp(
theme: MyTheme.lightTheme, title,
darkTheme: MyTheme.darkTheme, DesktopRemoteScreen(
themeMode: MyTheme.currentThemeMode(), params: argument,
home: DesktopRemoteScreen(
params: argument,
),
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: supportedLocales,
navigatorObservers: const [
// FirebaseAnalyticsObserver(analytics: analytics),
],
builder: _keepScaleBuilder(),
), ),
)); MyTheme.currentThemeMode(),
}
void runFileTransferScreen(Map<String, dynamic> 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<String, dynamic> 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(),
);
}),
); );
} }
@ -206,21 +153,11 @@ void runConnectionManagerScreen() async {
// initialize window // initialize window
WindowOptions windowOptions = WindowOptions windowOptions =
getHiddenTitleBarWindowOptions(size: kConnectionManagerWindowSize); getHiddenTitleBarWindowOptions(size: kConnectionManagerWindowSize);
runApp(RefreshWrapper(builder: (context) { _runApp(
return GetMaterialApp( '',
debugShowCheckedModeBanner: false, const DesktopServerPage(),
theme: MyTheme.lightTheme, MyTheme.currentThemeMode(),
darkTheme: MyTheme.darkTheme, );
themeMode: MyTheme.currentThemeMode(),
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: supportedLocales,
home: const DesktopServerPage(),
builder: _keepScaleBuilder());
}));
windowManager.waitUntilReadyToShow(windowOptions, () async { windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show(); await windowManager.show();
// ensure initial window size to be changed // 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 { void runInstallPage() async {
await windowManager.ensureInitialized(); await windowManager.ensureInitialized();
await initEnv(kAppTypeMain); await initEnv(kAppTypeMain);
runApp(RefreshWrapper( _runApp('', const InstallPage(), ThemeMode.light);
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()),
));
windowManager.waitUntilReadyToShow( windowManager.waitUntilReadyToShow(
WindowOptions(size: Size(800, 600), center: true), () async { WindowOptions(size: Size(800, 600), center: true), () async {
windowManager.show(); windowManager.show();
@ -303,6 +261,7 @@ class _AppState extends State<App> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// final analytics = FirebaseAnalytics.instance; // final analytics = FirebaseAnalytics.instance;
final botToastBuilder = BotToastInit();
return RefreshWrapper(builder: (context) { return RefreshWrapper(builder: (context) {
return MultiProvider( return MultiProvider(
providers: [ providers: [
@ -325,15 +284,16 @@ class _AppState extends State<App> {
: !isAndroid : !isAndroid
? WebHomePage() ? WebHomePage()
: HomePage(), : HomePage(),
navigatorObservers: const [
// FirebaseAnalyticsObserver(analytics: analytics),
],
localizationsDelegates: const [ localizationsDelegates: const [
GlobalMaterialLocalizations.delegate, GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate, GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
], ],
supportedLocales: supportedLocales, supportedLocales: supportedLocales,
navigatorObservers: [
// FirebaseAnalyticsObserver(analytics: analytics),
BotToastNavigatorObserver(),
],
builder: isAndroid builder: isAndroid
? (context, child) => AccessibilityListener( ? (context, child) => AccessibilityListener(
child: MediaQuery( child: MediaQuery(
@ -343,22 +303,24 @@ class _AppState extends State<App> {
child: child ?? Container(), child: child ?? Container(),
), ),
) )
: _keepScaleBuilder(), : (context, child) {
child = _keepScaleBuilder(context, child);
child = botToastBuilder(context, child);
return child;
},
), ),
); );
}); });
} }
} }
_keepScaleBuilder() { Widget _keepScaleBuilder(BuildContext context, Widget? child) {
return (BuildContext context, Widget? child) { return MediaQuery(
return MediaQuery( data: MediaQuery.of(context).copyWith(
data: MediaQuery.of(context).copyWith( textScaleFactor: 1.0,
textScaleFactor: 1.0, ),
), child: child ?? Container(),
child: child ?? Container(), );
);
};
} }
_registerEventHandler() { _registerEventHandler() {

View File

@ -38,8 +38,8 @@ class ChatModel with ChangeNotifier {
firstName: "Me", firstName: "Me",
); );
late final Map<int, MessageBody> _messages = Map() late final Map<int, MessageBody> _messages = {}..[clientModeID] =
..[clientModeID] = MessageBody(me, []); MessageBody(me, []);
var _currentID = clientModeID; var _currentID = clientModeID;
late bool _isShowChatPage = false; late bool _isShowChatPage = false;

View File

@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'dart:ui' as ui;
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -11,7 +12,6 @@ import '../../models/platform_model.dart';
import '../common.dart'; import '../common.dart';
import '../consts.dart'; import '../consts.dart';
import './state_model.dart'; import './state_model.dart';
import 'dart:ui' as ui;
/// Mouse button enum. /// Mouse button enum.
enum MouseButtons { left, right, wheel } enum MouseButtons { left, right, wheel }

View File

@ -10,7 +10,7 @@ import 'package:window_manager/window_manager.dart';
import '../common.dart'; import '../common.dart';
import '../common/formatter/id_formatter.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 '../desktop/widgets/tabbar_widget.dart';
import '../mobile/pages/server_page.dart'; import '../mobile/pages/server_page.dart';
import 'model.dart'; import 'model.dart';
@ -261,7 +261,7 @@ class ServerModel with ChangeNotifier {
} }
/// Start the screen sharing service. /// Start the screen sharing service.
Future<Null> startService() async { Future<void> startService() async {
_isStart = true; _isStart = true;
notifyListeners(); notifyListeners();
parent.target?.ffiModel.updateEventListener(""); parent.target?.ffiModel.updateEventListener("");
@ -276,7 +276,7 @@ class ServerModel with ChangeNotifier {
} }
/// Stop the screen sharing service. /// Stop the screen sharing service.
Future<Null> stopService() async { Future<void> stopService() async {
_isStart = false; _isStart = false;
closeAll(); closeAll();
await parent.target?.invokeMethod("stop_service"); await parent.target?.invokeMethod("stop_service");
@ -288,7 +288,7 @@ class ServerModel with ChangeNotifier {
} }
} }
Future<Null> initInput() async { Future<void> initInput() async {
await parent.target?.invokeMethod("init_input"); 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 { Future.delayed(Duration.zero, () async {
window_on_top(null); window_on_top(null);
}); });
@ -521,9 +521,9 @@ class ServerModel with ChangeNotifier {
} }
closeAll() { closeAll() {
_clients.forEach((client) { for (var client in _clients) {
bind.cmCloseConnection(connId: client.id); bind.cmCloseConnection(connId: client.id);
}); }
_clients.clear(); _clients.clear();
tabController.state.value.tabs.clear(); tabController.state.value.tabs.clear();
} }

View File

@ -25,7 +25,7 @@ class PlatformFFI {
static get localeName => window.navigator.language; static get localeName => window.navigator.language;
static Future<Null> init(String _appType) async { static Future<void> init(String _appType) async {
isWeb = true; isWeb = true;
isWebDesktop = !context.callMethod('isMobile'); isWebDesktop = !context.callMethod('isMobile');
context.callMethod('init'); context.callMethod('init');
@ -57,13 +57,13 @@ class PlatformFFI {
} }
static void stopDesktopWebListener() { static void stopDesktopWebListener() {
mouseListeners.forEach((l) { for (var ml in mouseListeners) {
l.cancel(); ml.cancel();
}); }
mouseListeners.clear(); mouseListeners.clear();
keyListeners.forEach((l) { for (var kl in keyListeners) {
l.cancel(); kl.cancel();
}); }
keyListeners.clear(); keyListeners.clear();
} }

View File

@ -102,6 +102,7 @@ dependencies:
ref: 5be5113d59c753989dbf1106241379e3fd4c9b18 ref: 5be5113d59c753989dbf1106241379e3fd4c9b18
path: ^1.8.1 path: ^1.8.1
auto_size_text: ^3.0.0 auto_size_text: ^3.0.0
bot_toast: ^4.0.3
dev_dependencies: dev_dependencies:
icons_launcher: ^2.0.4 icons_launcher: ^2.0.4