mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-04-10 15:41:28 +03:00
flutter_desktop: remote tab menu
Signed-off-by: fufesou <shuanglongchen@yeah.net>
This commit is contained in:
@@ -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<Size> 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<Size> onLayout;
|
||||
|
||||
@@ -240,9 +237,7 @@ class PopupMenuItem<T> extends PopupMenuEntry<T> {
|
||||
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<T, W extends PopupMenuItem<T>> extends State<W> {
|
||||
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<T> extends PopupMenuItem<T> {
|
||||
EdgeInsets? padding,
|
||||
double height = kMinInteractiveDimension,
|
||||
Widget? child,
|
||||
}) : assert(checked != null),
|
||||
super(
|
||||
}) : super(
|
||||
key: key,
|
||||
value: value,
|
||||
enabled: enabled,
|
||||
@@ -524,10 +522,11 @@ class _CheckedPopupMenuItemState<T>
|
||||
@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<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`.
|
||||
///
|
||||
/// `items` should be non-null and not empty.
|
||||
@@ -948,10 +1043,7 @@ Future<T?> showMenu<T>({
|
||||
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<T> 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<T> extends State<PopupMenuButton<T>> {
|
||||
// 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);
|
||||
|
||||
|
||||
@@ -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<RemoteMenubar> {
|
||||
final RxBool _show = false.obs;
|
||||
final Rx<Color> _hideColor = Colors.white12.obs;
|
||||
final _rxHideReplay = rxdart.ReplaySubject<int>();
|
||||
final _pinMenubar = false.obs;
|
||||
@@ -62,6 +63,8 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
RxBool get show => widget.show;
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
@@ -79,8 +82,8 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
.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<RemoteMenubar> {
|
||||
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<RemoteMenubar> {
|
||||
_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<RemoteMenubar> {
|
||||
);
|
||||
}
|
||||
displayMenu.add(MenuEntryDivider());
|
||||
|
||||
if (perms['keyboard'] != false) {
|
||||
if (pi.platform == 'Linux' || pi.sasEnabled) {
|
||||
displayMenu.add(MenuEntryButton<String>(
|
||||
|
||||
@@ -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<String> 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<bool> 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<DesktopTabState> 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,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user