rustdesk/flutter/lib/desktop/widgets/remote_toolbar.dart

2093 lines
61 KiB
Dart
Raw Normal View History

2022-09-16 19:43:28 +08:00
import 'dart:convert';
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/widgets/toolbar.dart';
import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:flutter_hbb/plugin/widgets/desc_ui.dart';
import 'package:flutter_hbb/plugin/common.dart';
2023-01-31 22:49:17 +08:00
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:debounce_throttle/debounce_throttle.dart';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:window_size/window_size.dart' as window_size;
import '../../common.dart';
import '../../common/widgets/dialog.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
import '../../common/shared_state.dart';
import './popup_menu.dart';
import './kb_layout_type_chooser.dart';
const _kKeyLegacyMode = 'legacy';
const _kKeyMapMode = 'map';
const _kKeyTranslateMode = 'translate';
class ToolbarState {
final kStoreKey = 'remoteMenubarState';
late RxBool show;
late RxBool _pin;
ToolbarState() {
final s = bind.getLocalFlutterOption(k: kStoreKey);
if (s.isEmpty) {
_initSet(false, false);
return;
}
try {
final m = jsonDecode(s);
if (m == null) {
_initSet(false, false);
} else {
_initSet(m['pin'] ?? false, m['pin'] ?? false);
}
} catch (e) {
debugPrint('Failed to decode toolbar state ${e.toString()}');
_initSet(false, false);
}
}
_initSet(bool s, bool p) {
// Show remubar when connection is established.
show =
RxBool(bind.mainGetUserDefaultOption(key: 'collapse_toolbar') != 'Y');
_pin = RxBool(p);
}
bool get pin => _pin.value;
switchShow() async {
show.value = !show.value;
}
setShow(bool v) async {
if (show.value != v) {
show.value = v;
}
}
switchPin() async {
_pin.value = !_pin.value;
// Save everytime changed, as this func will not be called frequently
await _savePin();
}
setPin(bool v) async {
if (_pin.value != v) {
_pin.value = v;
// Save everytime changed, as this func will not be called frequently
await _savePin();
}
}
_savePin() async {
bind.setLocalFlutterOption(
k: kStoreKey, v: jsonEncode({'pin': _pin.value}));
}
save() async {
await _savePin();
}
}
class _ToolbarTheme {
2023-02-15 18:40:17 +08:00
static const Color blueColor = MyTheme.button;
static const Color hoverBlueColor = MyTheme.accent;
static Color inactiveColor = Colors.grey[800]!;
static Color hoverInactiveColor = Colors.grey[850]!;
2023-02-15 18:40:17 +08:00
static const Color redColor = Colors.redAccent;
static const Color hoverRedColor = Colors.red;
// kMinInteractiveDimension
static const double height = 20.0;
static const double dividerHeight = 12.0;
static const double buttonSize = 32;
static const double buttonHMargin = 3;
static const double buttonVMargin = 6;
static const double iconRadius = 8;
static const double elevation = 3;
static const Color bordDark = MyTheme.bordDark;
static const Color bordLight = MyTheme.bordLight;
static const Color dividerDark = MyTheme.dividerDark;
static const Color dividerLight = MyTheme.dividerLight;
static double dividerSpaceToAction = Platform.isWindows ? 8 : 14;
static double menuBorderRadius = Platform.isWindows ? 5.0 : 7.0;
static EdgeInsets menuPadding = Platform.isWindows
? EdgeInsets.fromLTRB(4, 12, 4, 12)
: EdgeInsets.fromLTRB(6, 14, 6, 14);
static const double menuButtonBorderRadius = 3.0;
static final defaultMenuStyle = MenuStyle(
side: MaterialStateProperty.all(BorderSide(
width: 1,
color: MyTheme.currentThemeMode() == ThemeMode.light
? _ToolbarTheme.bordLight
: _ToolbarTheme.bordDark,
)),
shape: MaterialStatePropertyAll(RoundedRectangleBorder(
borderRadius: BorderRadius.circular(_ToolbarTheme.menuBorderRadius))),
padding: MaterialStateProperty.all(_ToolbarTheme.menuPadding),
);
static final defaultMenuButtonStyle = ButtonStyle(
backgroundColor: MaterialStatePropertyAll(Colors.transparent),
padding: MaterialStatePropertyAll(EdgeInsets.zero),
overlayColor: MaterialStatePropertyAll(Colors.transparent),
);
}
typedef DismissFunc = void Function();
class RemoteMenuEntry {
static MenuEntryRadios<String> viewStyle(
String remoteId,
FFI ffi,
EdgeInsets padding, {
DismissFunc? dismissFunc,
DismissCallback? dismissCallback,
RxString? rxViewStyle,
}) {
return MenuEntryRadios<String>(
text: translate('Ratio'),
optionsGetter: () => [
MenuEntryRadioOption(
text: translate('Scale original'),
value: kRemoteViewStyleOriginal,
dismissOnClicked: true,
dismissCallback: dismissCallback,
),
MenuEntryRadioOption(
text: translate('Scale adaptive'),
value: kRemoteViewStyleAdaptive,
dismissOnClicked: true,
dismissCallback: dismissCallback,
),
],
curOptionGetter: () async {
// null means peer id is not found, which there's no need to care about
final viewStyle =
await bind.sessionGetViewStyle(sessionId: ffi.sessionId) ?? '';
if (rxViewStyle != null) {
rxViewStyle.value = viewStyle;
}
return viewStyle;
},
optionSetter: (String oldValue, String newValue) async {
await bind.sessionSetViewStyle(
sessionId: ffi.sessionId, value: newValue);
if (rxViewStyle != null) {
rxViewStyle.value = newValue;
}
ffi.canvasModel.updateViewStyle();
if (dismissFunc != null) {
dismissFunc();
}
},
padding: padding,
dismissOnClicked: true,
dismissCallback: dismissCallback,
);
}
static MenuEntrySwitch2<String> showRemoteCursor(
String remoteId,
SessionID sessionId,
EdgeInsets padding, {
DismissFunc? dismissFunc,
DismissCallback? dismissCallback,
}) {
final state = ShowRemoteCursorState.find(remoteId);
final optKey = 'show-remote-cursor';
return MenuEntrySwitch2<String>(
switchType: SwitchType.scheckbox,
text: translate('Show remote cursor'),
getter: () {
return state;
},
setter: (bool v) async {
await bind.sessionToggleOption(sessionId: sessionId, value: optKey);
state.value =
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: optKey);
if (dismissFunc != null) {
dismissFunc();
}
},
padding: padding,
dismissOnClicked: true,
dismissCallback: dismissCallback,
);
}
static MenuEntrySwitch<String> disableClipboard(
SessionID sessionId,
EdgeInsets? padding, {
DismissFunc? dismissFunc,
DismissCallback? dismissCallback,
}) {
return createSwitchMenuEntry(
sessionId,
'Disable clipboard',
'disable-clipboard',
padding,
true,
dismissCallback: dismissCallback,
);
}
static MenuEntrySwitch<String> createSwitchMenuEntry(
SessionID sessionId,
String text,
String option,
EdgeInsets? padding,
bool dismissOnClicked, {
DismissFunc? dismissFunc,
DismissCallback? dismissCallback,
}) {
return MenuEntrySwitch<String>(
switchType: SwitchType.scheckbox,
text: translate(text),
getter: () async {
return bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: option);
},
setter: (bool v) async {
await bind.sessionToggleOption(sessionId: sessionId, value: option);
if (dismissFunc != null) {
dismissFunc();
}
},
padding: padding,
dismissOnClicked: dismissOnClicked,
dismissCallback: dismissCallback,
);
}
static MenuEntryButton<String> insertLock(
SessionID sessionId,
EdgeInsets? padding, {
DismissFunc? dismissFunc,
DismissCallback? dismissCallback,
}) {
return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Insert Lock'),
style: style,
),
proc: () {
bind.sessionLockScreen(sessionId: sessionId);
if (dismissFunc != null) {
dismissFunc();
}
},
padding: padding,
dismissOnClicked: true,
dismissCallback: dismissCallback,
);
}
static insertCtrlAltDel(
SessionID sessionId,
EdgeInsets? padding, {
DismissFunc? dismissFunc,
DismissCallback? dismissCallback,
}) {
return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
'${translate("Insert")} Ctrl + Alt + Del',
style: style,
),
proc: () {
bind.sessionCtrlAltDel(sessionId: sessionId);
if (dismissFunc != null) {
dismissFunc();
}
},
padding: padding,
dismissOnClicked: true,
dismissCallback: dismissCallback,
);
}
}
class RemoteToolbar extends StatefulWidget {
final String id;
final FFI ffi;
final ToolbarState state;
final Function(Function(bool)) onEnterOrLeaveImageSetter;
final Function() onEnterOrLeaveImageCleaner;
RemoteToolbar({
Key? key,
required this.id,
required this.ffi,
required this.state,
required this.onEnterOrLeaveImageSetter,
required this.onEnterOrLeaveImageCleaner,
}) : super(key: key);
@override
State<RemoteToolbar> createState() => _RemoteToolbarState();
}
class _RemoteToolbarState extends State<RemoteToolbar> {
late Debouncer<int> _debouncerHide;
bool _isCursorOverImage = false;
final _fractionX = 0.5.obs;
final _dragging = false.obs;
int get windowId => stateGlobal.windowId;
bool get isFullscreen => stateGlobal.fullscreen;
void _setFullscreen(bool v) {
stateGlobal.setFullscreen(v);
setState(() {});
}
RxBool get show => widget.state.show;
bool get pin => widget.state.pin;
PeerInfo get pi => widget.ffi.ffiModel.pi;
FfiModel get ffiModel => widget.ffi.ffiModel;
triggerAutoHide() => _debouncerHide.value = _debouncerHide.value + 1;
void _minimize() async =>
await WindowController.fromWindowId(windowId).minimize();
@override
initState() {
super.initState();
Future.delayed(Duration.zero, () async {
_fractionX.value = double.tryParse(await bind.sessionGetOption(
sessionId: widget.ffi.sessionId,
arg: 'remote-menubar-drag-x') ??
'0.5') ??
0.5;
});
_debouncerHide = Debouncer<int>(
Duration(milliseconds: 5000),
onChanged: _debouncerHideProc,
initialValue: 0,
);
widget.onEnterOrLeaveImageSetter((enter) {
if (enter) {
triggerAutoHide();
_isCursorOverImage = true;
} else {
_isCursorOverImage = false;
}
});
}
_debouncerHideProc(int v) {
if (!pin && show.isTrue && _isCursorOverImage && _dragging.isFalse) {
show.value = false;
}
}
@override
dispose() {
super.dispose();
widget.onEnterOrLeaveImageCleaner();
}
@override
Widget build(BuildContext context) {
// No need to use future builder here.
return Align(
alignment: Alignment.topCenter,
child: Obx(() => show.value
2023-03-16 01:31:53 +08:00
? _buildToolbar(context)
: _buildDraggableShowHide(context)),
);
}
Widget _buildDraggableShowHide(BuildContext context) {
return Obx(() {
if (show.isTrue && _dragging.isFalse) {
triggerAutoHide();
}
return Align(
alignment: FractionalOffset(_fractionX.value, 0),
child: Offstage(
offstage: _dragging.isTrue,
child: Material(
elevation: _ToolbarTheme.elevation,
shadowColor: MyTheme.color(context).shadow,
child: _DraggableShowHide(
sessionId: widget.ffi.sessionId,
dragging: _dragging,
fractionX: _fractionX,
show: show,
setFullscreen: _setFullscreen,
setMinimize: _minimize,
),
),
),
);
});
}
2023-03-16 01:31:53 +08:00
Widget _buildToolbar(BuildContext context) {
final List<Widget> toolbarItems = [];
if (!isWebDesktop) {
2023-03-16 01:31:53 +08:00
toolbarItems.add(_PinMenu(state: widget.state));
toolbarItems.add(_MobileActionMenu(ffi: widget.ffi));
}
if (PrivacyModeState.find(widget.id).isFalse && pi.displays.length > 1) {
2023-03-16 01:31:53 +08:00
toolbarItems.add(
bind.mainGetUserDefaultOption(key: 'show_monitors_toolbar') == 'Y'
? _MultiMonitorMenu(id: widget.id, ffi: widget.ffi)
: _MonitorMenu(id: widget.id, ffi: widget.ffi),
);
}
2023-03-16 01:31:53 +08:00
toolbarItems
.add(_ControlMenu(id: widget.id, ffi: widget.ffi, state: widget.state));
2023-03-16 01:31:53 +08:00
toolbarItems.add(_DisplayMenu(
id: widget.id,
ffi: widget.ffi,
state: widget.state,
setFullscreen: _setFullscreen,
));
toolbarItems.add(_KeyboardMenu(id: widget.id, ffi: widget.ffi));
if (!isWeb) {
2023-03-16 01:31:53 +08:00
toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi));
toolbarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi));
}
toolbarItems.add(_RecordMenu(ffi: widget.ffi));
2023-03-16 01:31:53 +08:00
toolbarItems.add(_CloseMenu(id: widget.id, ffi: widget.ffi));
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Material(
elevation: _ToolbarTheme.elevation,
shadowColor: MyTheme.color(context).shadow,
borderRadius: BorderRadius.all(Radius.circular(4.0)),
color: Theme.of(context)
.menuBarTheme
.style
?.backgroundColor
?.resolve(MaterialState.values.toSet()),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Theme(
data: themeData(),
child: Row(
children: [
SizedBox(width: _ToolbarTheme.buttonHMargin * 2),
2023-03-16 01:31:53 +08:00
...toolbarItems,
SizedBox(width: _ToolbarTheme.buttonHMargin * 2)
],
),
),
),
),
_buildDraggableShowHide(context),
],
2023-02-14 20:57:33 +08:00
);
}
ThemeData themeData() {
return Theme.of(context).copyWith(
menuButtonTheme: MenuButtonThemeData(
style: ButtonStyle(
minimumSize: MaterialStatePropertyAll(Size(64, 32)),
textStyle: MaterialStatePropertyAll(
TextStyle(fontWeight: FontWeight.normal),
),
shape: MaterialStatePropertyAll(RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(_ToolbarTheme.menuButtonBorderRadius))),
),
),
dividerTheme: DividerThemeData(
space: _ToolbarTheme.dividerSpaceToAction,
color: MyTheme.currentThemeMode() == ThemeMode.light
? _ToolbarTheme.dividerLight
: _ToolbarTheme.dividerDark,
),
menuBarTheme: MenuBarThemeData(
style: MenuStyle(
padding: MaterialStatePropertyAll(EdgeInsets.zero),
elevation: MaterialStatePropertyAll(0),
shape: MaterialStatePropertyAll(BeveledRectangleBorder()),
).copyWith(
backgroundColor:
Theme.of(context).menuBarTheme.style?.backgroundColor)),
);
}
}
class _PinMenu extends StatelessWidget {
final ToolbarState state;
const _PinMenu({Key? key, required this.state}) : super(key: key);
@override
Widget build(BuildContext context) {
2023-02-14 20:57:33 +08:00
return Obx(
() => _IconMenuButton(
assetName: state.pin ? "assets/pinned.svg" : "assets/unpinned.svg",
tooltip: state.pin ? 'Unpin Toolbar' : 'Pin Toolbar',
onPressed: state.switchPin,
color:
state.pin ? _ToolbarTheme.blueColor : _ToolbarTheme.inactiveColor,
hoverColor: state.pin
? _ToolbarTheme.hoverBlueColor
: _ToolbarTheme.hoverInactiveColor,
2023-02-14 20:57:33 +08:00
),
);
}
}
class _MobileActionMenu extends StatelessWidget {
final FFI ffi;
const _MobileActionMenu({Key? key, required this.ffi}) : super(key: key);
@override
Widget build(BuildContext context) {
if (!ffi.ffiModel.isPeerAndroid) return Offstage();
return Obx(() => _IconMenuButton(
assetName: 'assets/actions_mobile.svg',
tooltip: 'Mobile Actions',
onPressed: () =>
ffi.dialogManager.toggleMobileActionsOverlay(ffi: ffi),
color: ffi.dialogManager.mobileActionsOverlayVisible.isTrue
? _ToolbarTheme.blueColor
: _ToolbarTheme.inactiveColor,
hoverColor: ffi.dialogManager.mobileActionsOverlayVisible.isTrue
? _ToolbarTheme.hoverBlueColor
: _ToolbarTheme.hoverInactiveColor,
));
}
}
class _MonitorMenu extends StatelessWidget {
final String id;
final FFI ffi;
const _MonitorMenu({Key? key, required this.id, required this.ffi})
: super(key: key);
@override
Widget build(BuildContext context) {
return _IconSubmenuButton(
tooltip: 'Select Monitor',
icon: icon(),
ffi: ffi,
color: _ToolbarTheme.blueColor,
hoverColor: _ToolbarTheme.hoverBlueColor,
menuStyle: MenuStyle(
padding:
MaterialStatePropertyAll(EdgeInsets.symmetric(horizontal: 6))),
menuChildren: [Row(children: displays(context))]);
}
icon() {
final pi = ffi.ffiModel.pi;
return Stack(
alignment: Alignment.center,
children: [
SvgPicture.asset(
"assets/screen.svg",
colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn),
),
Obx(() {
RxInt display = CurrentDisplayState.find(id);
return Text(
'${display.value + 1}/${pi.displays.length}',
style: const TextStyle(
color: _ToolbarTheme.blueColor,
fontSize: 8,
fontWeight: FontWeight.bold,
),
);
}),
],
2022-09-05 22:18:29 +08:00
);
}
List<Widget> displays(BuildContext context) {
final List<Widget> rowChildren = [];
final pi = ffi.ffiModel.pi;
for (int i = 0; i < pi.displays.length; i++) {
rowChildren.add(_IconMenuButton(
topLevel: false,
color: _ToolbarTheme.blueColor,
hoverColor: _ToolbarTheme.hoverBlueColor,
2023-08-11 08:34:14 +08:00
tooltip: "#${i + 1} monitor",
hMargin: 6,
vMargin: 12,
icon: Container(
alignment: AlignmentDirectional.center,
constraints: const BoxConstraints(minHeight: _ToolbarTheme.height),
child: Stack(
alignment: Alignment.center,
children: [
SvgPicture.asset(
"assets/screen.svg",
colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn),
),
Text(
(i + 1).toString(),
style: TextStyle(
color: _ToolbarTheme.blueColor,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
2023-02-14 20:57:33 +08:00
),
),
onPressed: () {
_menuDismissCallback(ffi);
RxInt display = CurrentDisplayState.find(id);
if (display.value != i) {
bind.sessionSwitchDisplay(sessionId: ffi.sessionId, value: i);
}
},
));
}
return rowChildren;
}
}
class _ControlMenu extends StatelessWidget {
final String id;
final FFI ffi;
final ToolbarState state;
_ControlMenu(
{Key? key, required this.id, required this.ffi, required this.state})
: super(key: key);
@override
Widget build(BuildContext context) {
return _IconSubmenuButton(
tooltip: 'Control Actions',
svg: "assets/actions.svg",
color: _ToolbarTheme.blueColor,
hoverColor: _ToolbarTheme.hoverBlueColor,
ffi: ffi,
menuChildren: toolbarControls(context, id, ffi).map((e) {
if (e.divider) {
return Divider();
} else {
return MenuButton(
child: e.child,
onPressed: e.onPressed,
ffi: ffi,
trailingIcon: e.trailingIcon);
}
}).toList());
}
}
class ScreenAdjustor {
final String id;
final FFI ffi;
final VoidCallback cbExitFullscreen;
window_size.Screen? _screen;
ScreenAdjustor({
required this.id,
required this.ffi,
required this.cbExitFullscreen,
});
bool get isFullscreen => stateGlobal.fullscreen;
int get windowId => stateGlobal.windowId;
adjustWindow(BuildContext context) {
return futureBuilder(
future: isWindowCanBeAdjusted(),
hasData: (data) {
final visible = data as bool;
if (!visible) return Offstage();
return Column(
children: [
MenuButton(
child: Text(translate('Adjust Window')),
onPressed: () => doAdjustWindow(context),
ffi: ffi),
Divider(),
],
);
});
}
doAdjustWindow(BuildContext context) async {
await updateScreen();
if (_screen != null) {
cbExitFullscreen();
double scale = _screen!.scaleFactor;
final wndRect = await WindowController.fromWindowId(windowId).getFrame();
final mediaSize = MediaQueryData.fromView(View.of(context)).size;
// On windows, wndRect is equal to GetWindowRect and mediaSize is equal to GetClientRect.
// https://stackoverflow.com/a/7561083
double magicWidth =
wndRect.right - wndRect.left - mediaSize.width * scale;
double magicHeight =
wndRect.bottom - wndRect.top - mediaSize.height * scale;
final canvasModel = ffi.canvasModel;
final width = (canvasModel.getDisplayWidth() * canvasModel.scale +
CanvasModel.leftToEdge +
CanvasModel.rightToEdge) *
scale +
magicWidth;
final height = (canvasModel.getDisplayHeight() * canvasModel.scale +
CanvasModel.topToEdge +
CanvasModel.bottomToEdge) *
scale +
magicHeight;
double left = wndRect.left + (wndRect.width - width) / 2;
double top = wndRect.top + (wndRect.height - height) / 2;
Rect frameRect = _screen!.frame;
if (!isFullscreen) {
frameRect = _screen!.visibleFrame;
}
if (left < frameRect.left) {
left = frameRect.left;
}
if (top < frameRect.top) {
top = frameRect.top;
}
if ((left + width) > frameRect.right) {
left = frameRect.right - width;
}
if ((top + height) > frameRect.bottom) {
top = frameRect.bottom - height;
}
await WindowController.fromWindowId(windowId)
.setFrame(Rect.fromLTWH(left, top, width, height));
stateGlobal.setMaximized(false);
}
}
updateScreen() async {
final v = await rustDeskWinManager.call(
WindowType.Main, kWindowGetWindowInfo, '');
final String valueStr = v.result;
if (valueStr.isEmpty) {
_screen = null;
} else {
final screenMap = jsonDecode(valueStr);
_screen = window_size.Screen(
Rect.fromLTRB(screenMap['frame']['l'], screenMap['frame']['t'],
screenMap['frame']['r'], screenMap['frame']['b']),
Rect.fromLTRB(
screenMap['visibleFrame']['l'],
screenMap['visibleFrame']['t'],
screenMap['visibleFrame']['r'],
screenMap['visibleFrame']['b']),
screenMap['scaleFactor']);
}
}
Future<bool> isWindowCanBeAdjusted() async {
final viewStyle =
await bind.sessionGetViewStyle(sessionId: ffi.sessionId) ?? '';
if (viewStyle != kRemoteViewStyleOriginal) {
return false;
}
final remoteCount = RemoteCountState.find().value;
if (remoteCount != 1) {
return false;
}
if (_screen == null) {
return false;
}
final scale = kIgnoreDpi ? 1.0 : _screen!.scaleFactor;
double selfWidth = _screen!.visibleFrame.width;
double selfHeight = _screen!.visibleFrame.height;
if (isFullscreen) {
selfWidth = _screen!.frame.width;
selfHeight = _screen!.frame.height;
}
final canvasModel = ffi.canvasModel;
final displayWidth = canvasModel.getDisplayWidth();
final displayHeight = canvasModel.getDisplayHeight();
final requiredWidth =
CanvasModel.leftToEdge + displayWidth + CanvasModel.rightToEdge;
final requiredHeight =
CanvasModel.topToEdge + displayHeight + CanvasModel.bottomToEdge;
return selfWidth > (requiredWidth * scale) &&
selfHeight > (requiredHeight * scale);
}
}
class _DisplayMenu extends StatefulWidget {
final String id;
final FFI ffi;
final ToolbarState state;
final Function(bool) setFullscreen;
final Widget pluginItem;
_DisplayMenu(
{Key? key,
required this.id,
required this.ffi,
required this.state,
required this.setFullscreen})
: pluginItem = LocationItem.createLocationItem(
id,
ffi,
kLocationClientRemoteToolbarDisplay,
true,
),
super(key: key);
@override
State<_DisplayMenu> createState() => _DisplayMenuState();
}
class _DisplayMenuState extends State<_DisplayMenu> {
late final ScreenAdjustor _screenAdjustor = ScreenAdjustor(
id: widget.id,
ffi: widget.ffi,
cbExitFullscreen: () => widget.setFullscreen(false),
);
bool get isFullscreen => stateGlobal.fullscreen;
int get windowId => stateGlobal.windowId;
Map<String, bool> get perms => widget.ffi.ffiModel.permissions;
PeerInfo get pi => widget.ffi.ffiModel.pi;
FfiModel get ffiModel => widget.ffi.ffiModel;
FFI get ffi => widget.ffi;
String get id => widget.id;
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
_screenAdjustor.updateScreen();
return _IconSubmenuButton(
tooltip: 'Display Settings',
svg: "assets/display.svg",
ffi: widget.ffi,
color: _ToolbarTheme.blueColor,
hoverColor: _ToolbarTheme.hoverBlueColor,
menuChildren: [
_screenAdjustor.adjustWindow(context),
viewStyle(),
scrollStyle(),
imageQuality(),
codec(),
_ResolutionsMenu(
id: widget.id,
ffi: widget.ffi,
screenAdjustor: _screenAdjustor,
),
Divider(),
toggles(),
widget.pluginItem,
]);
}
viewStyle() {
return futureBuilder(
future: toolbarViewStyle(context, widget.id, widget.ffi),
hasData: (data) {
final v = data as List<TRadioMenu<String>>;
return Column(children: [
...v
.map((e) => RdoMenuButton<String>(
value: e.value,
groupValue: e.groupValue,
onChanged: e.onChanged,
child: e.child,
ffi: ffi))
.toList(),
Divider(),
]);
});
}
scrollStyle() {
return futureBuilder(future: () async {
final viewStyle =
await bind.sessionGetViewStyle(sessionId: ffi.sessionId) ?? '';
final visible = viewStyle == kRemoteViewStyleOriginal;
final scrollStyle =
await bind.sessionGetScrollStyle(sessionId: ffi.sessionId) ?? '';
return {'visible': visible, 'scrollStyle': scrollStyle};
}(), hasData: (data) {
final visible = data['visible'] as bool;
if (!visible) return Offstage();
final groupValue = data['scrollStyle'] as String;
onChange(String? value) async {
if (value == null) return;
await bind.sessionSetScrollStyle(
sessionId: ffi.sessionId, value: value);
widget.ffi.canvasModel.updateScrollStyle();
}
final enabled = widget.ffi.canvasModel.imageOverflow.value;
return Column(children: [
RdoMenuButton<String>(
child: Text(translate('ScrollAuto')),
value: kRemoteScrollStyleAuto,
groupValue: groupValue,
onChanged: enabled ? (value) => onChange(value) : null,
ffi: widget.ffi,
),
RdoMenuButton<String>(
child: Text(translate('Scrollbar')),
value: kRemoteScrollStyleBar,
groupValue: groupValue,
onChanged: enabled ? (value) => onChange(value) : null,
ffi: widget.ffi,
),
Divider(),
]);
});
}
imageQuality() {
return futureBuilder(
future: toolbarImageQuality(context, widget.id, widget.ffi),
hasData: (data) {
final v = data as List<TRadioMenu<String>>;
return _SubmenuButton(
ffi: widget.ffi,
child: Text(translate('Image Quality')),
menuChildren: v
.map((e) => RdoMenuButton<String>(
value: e.value,
groupValue: e.groupValue,
onChanged: e.onChanged,
child: e.child,
ffi: ffi))
.toList(),
);
});
}
codec() {
return futureBuilder(
future: toolbarCodec(context, id, ffi),
hasData: (data) {
final v = data as List<TRadioMenu<String>>;
if (v.isEmpty) return Offstage();
2022-09-16 19:43:28 +08:00
return _SubmenuButton(
ffi: widget.ffi,
child: Text(translate('Codec')),
menuChildren: v
.map((e) => RdoMenuButton(
value: e.value,
groupValue: e.groupValue,
onChanged: e.onChanged,
child: e.child,
ffi: ffi))
.toList());
});
}
toggles() {
return futureBuilder(
future: toolbarDisplayToggle(context, id, ffi),
hasData: (data) {
final v = data as List<TToggleMenu>;
if (v.isEmpty) return Offstage();
return Column(
children: v
.map((e) => CkbMenuButton(
value: e.value,
onChanged: e.onChanged,
child: e.child,
ffi: ffi))
.toList());
});
}
}
class _ResolutionsMenu extends StatefulWidget {
final String id;
final FFI ffi;
final ScreenAdjustor screenAdjustor;
_ResolutionsMenu({
Key? key,
required this.id,
required this.ffi,
required this.screenAdjustor,
}) : super(key: key);
@override
State<_ResolutionsMenu> createState() => _ResolutionsMenuState();
}
const double _kCustomResolutionEditingWidth = 42;
const _kCustomResolutionValue = 'custom';
class _ResolutionsMenuState extends State<_ResolutionsMenu> {
String _groupValue = '';
Resolution? _localResolution;
late final TextEditingController _customWidth =
TextEditingController(text: display.width.toString());
late final TextEditingController _customHeight =
TextEditingController(text: display.height.toString());
FFI get ffi => widget.ffi;
PeerInfo get pi => widget.ffi.ffiModel.pi;
FfiModel get ffiModel => widget.ffi.ffiModel;
Display get display => ffiModel.display;
List<Resolution> get resolutions => pi.resolutions;
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
final isVirtualDisplay = display.isVirtualDisplayResolution;
final visible =
ffiModel.keyboard && (isVirtualDisplay || resolutions.length > 1);
if (!visible) return Offstage();
_getLocalResolution();
final showOriginalBtn =
display.isOriginalResolutionSet && !display.isOriginalResolution;
final showFitLocalBtn = !_isRemoteResolutionFitLocal();
_setGroupValue();
return _SubmenuButton(
ffi: widget.ffi,
menuChildren: <Widget>[
_OriginalResolutionMenuButton(context, showOriginalBtn),
_FitLocalResolutionMenuButton(context, showFitLocalBtn),
_customResolutionMenuButton(context, isVirtualDisplay),
_menuDivider(showOriginalBtn, showFitLocalBtn, isVirtualDisplay),
] +
_supportedResolutionMenuButtons(),
child: Text(translate("Resolution")),
);
}
_setGroupValue() {
final lastGroupValue =
stateGlobal.getLastResolutionGroupValue(widget.id, pi.currentDisplay);
if (lastGroupValue == _kCustomResolutionValue) {
_groupValue = _kCustomResolutionValue;
} else {
_groupValue = '${display.width}x${display.height}';
}
}
_menuDivider(
bool showOriginalBtn, bool showFitLocalBtn, bool isVirtualDisplay) {
return Offstage(
offstage: !(showOriginalBtn || showFitLocalBtn || isVirtualDisplay),
child: Divider(),
);
}
_getLocalResolution() {
_localResolution = null;
final String currentDisplay = bind.mainGetCurrentDisplay();
if (currentDisplay.isNotEmpty) {
try {
final display = json.decode(currentDisplay);
if (display['w'] != null && display['h'] != null) {
_localResolution = Resolution(display['w'], display['h']);
}
} catch (e) {
debugPrint('Failed to decode $currentDisplay, $e');
}
}
}
_onChanged(BuildContext context, String? value) async {
stateGlobal.setLastResolutionGroupValue(
widget.id, pi.currentDisplay, value);
if (value == null) return;
int? w;
int? h;
if (value == _kCustomResolutionValue) {
w = int.tryParse(_customWidth.text);
h = int.tryParse(_customHeight.text);
} else {
final list = value.split('x');
if (list.length == 2) {
w = int.tryParse(list[0]);
h = int.tryParse(list[1]);
}
}
if (w != null && h != null) {
if (w != display.width || h != display.height) {
await _changeResolution(context, w, h);
}
}
}
_changeResolution(BuildContext context, int w, int h) async {
await bind.sessionChangeResolution(
sessionId: ffi.sessionId,
display: pi.currentDisplay,
width: w,
height: h,
);
Future.delayed(Duration(seconds: 3), () async {
final display = ffiModel.display;
if (w == display.width && h == display.height) {
if (await widget.screenAdjustor.isWindowCanBeAdjusted()) {
widget.screenAdjustor.doAdjustWindow(context);
}
}
});
}
Widget _OriginalResolutionMenuButton(
BuildContext context, bool showOriginalBtn) {
return Offstage(
offstage: !showOriginalBtn,
child: MenuButton(
onPressed: () => _changeResolution(
context, display.originalWidth, display.originalHeight),
ffi: widget.ffi,
child: Text(
'${translate('resolution_original_tip')} ${display.originalWidth}x${display.originalHeight}'),
),
);
}
Widget _FitLocalResolutionMenuButton(
BuildContext context, bool showFitLocalBtn) {
return Offstage(
offstage: !showFitLocalBtn,
child: MenuButton(
onPressed: () {
final resolution = _getBestFitResolution();
if (resolution != null) {
_changeResolution(context, resolution.width, resolution.height);
}
},
ffi: widget.ffi,
child: Text(
'${translate('resolution_fit_local_tip')} ${_localResolution?.width ?? 0}x${_localResolution?.height ?? 0}'),
),
);
}
Widget _customResolutionMenuButton(BuildContext context, isVirtualDisplay) {
return Offstage(
offstage: !isVirtualDisplay,
child: RdoMenuButton(
value: _kCustomResolutionValue,
groupValue: _groupValue,
onChanged: (String? value) => _onChanged(context, value),
ffi: widget.ffi,
child: Row(
children: [
Text('${translate('resolution_custom_tip')} '),
SizedBox(
width: _kCustomResolutionEditingWidth,
child: _resolutionInput(_customWidth),
),
Text(' x '),
SizedBox(
width: _kCustomResolutionEditingWidth,
child: _resolutionInput(_customHeight),
),
],
),
),
);
}
TextField _resolutionInput(TextEditingController controller) {
return TextField(
decoration: InputDecoration(
border: InputBorder.none,
isDense: true,
contentPadding: EdgeInsets.fromLTRB(3, 3, 3, 3),
),
keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(4),
FilteringTextInputFormatter.allow(RegExp(r'[0-9]')),
],
controller: controller,
);
}
List<Widget> _supportedResolutionMenuButtons() => resolutions
.map((e) => RdoMenuButton(
value: '${e.width}x${e.height}',
groupValue: _groupValue,
onChanged: (String? value) => _onChanged(context, value),
ffi: widget.ffi,
child: Text('${e.width}x${e.height}')))
.toList();
Resolution? _getBestFitResolution() {
if (_localResolution == null) {
return null;
}
if (display.isVirtualDisplayResolution) {
return _localResolution!;
}
for (final r in resolutions) {
if (r.width == _localResolution!.width &&
r.height == _localResolution!.height) {
return r;
}
}
return null;
}
bool _isRemoteResolutionFitLocal() {
if (_localResolution == null) {
return true;
}
final bestFitResolution = _getBestFitResolution();
if (bestFitResolution == null) {
return true;
}
return bestFitResolution.width == display.width &&
bestFitResolution.height == display.height;
}
}
class _KeyboardMenu extends StatelessWidget {
final String id;
final FFI ffi;
_KeyboardMenu({
Key? key,
required this.id,
required this.ffi,
}) : super(key: key);
PeerInfo get pi => ffi.ffiModel.pi;
@override
Widget build(BuildContext context) {
var ffiModel = Provider.of<FfiModel>(context);
if (!ffiModel.keyboard) return Offstage();
String? modeOnly;
if (stateGlobal.grabKeyboard) {
if (bind.sessionIsKeyboardModeSupported(
sessionId: ffi.sessionId, mode: _kKeyMapMode)) {
bind.sessionSetKeyboardMode(
sessionId: ffi.sessionId, value: _kKeyMapMode);
modeOnly = _kKeyMapMode;
} else if (bind.sessionIsKeyboardModeSupported(
sessionId: ffi.sessionId, mode: _kKeyLegacyMode)) {
bind.sessionSetKeyboardMode(
sessionId: ffi.sessionId, value: _kKeyLegacyMode);
modeOnly = _kKeyLegacyMode;
}
}
return _IconSubmenuButton(
tooltip: 'Keyboard Settings',
svg: "assets/keyboard.svg",
ffi: ffi,
color: _ToolbarTheme.blueColor,
hoverColor: _ToolbarTheme.hoverBlueColor,
menuChildren: [
keyboardMode(modeOnly),
localKeyboardType(),
Divider(),
viewMode(),
Divider(),
reverseMouseWheel(),
]);
}
keyboardMode(String? modeOnly) {
return futureBuilder(future: () async {
return await bind.sessionGetKeyboardMode(sessionId: ffi.sessionId) ??
_kKeyLegacyMode;
}(), hasData: (data) {
final groupValue = data as String;
List<InputModeMenu> modes = [
InputModeMenu(key: _kKeyLegacyMode, menu: 'Legacy mode'),
InputModeMenu(key: _kKeyMapMode, menu: 'Map mode'),
InputModeMenu(key: _kKeyTranslateMode, menu: 'Translate mode'),
];
List<RdoMenuButton> list = [];
final enabled = !ffi.ffiModel.viewOnly;
onChanged(String? value) async {
if (value == null) return;
await bind.sessionSetKeyboardMode(
sessionId: ffi.sessionId, value: value);
}
for (InputModeMenu mode in modes) {
if (modeOnly != null && mode.key != modeOnly) {
continue;
} else if (!bind.sessionIsKeyboardModeSupported(
sessionId: ffi.sessionId, mode: mode.key)) {
continue;
}
if (pi.is_wayland && mode.key != _kKeyMapMode) {
continue;
}
var text = translate(mode.menu);
if (mode.key == _kKeyTranslateMode) {
text = '$text beta';
}
list.add(RdoMenuButton<String>(
child: Text(text),
value: mode.key,
groupValue: groupValue,
onChanged: enabled ? onChanged : null,
ffi: ffi,
));
}
return Column(children: list);
});
}
localKeyboardType() {
final localPlatform = getLocalPlatformForKBLayoutType(pi.platform);
final visible = localPlatform != '';
if (!visible) return Offstage();
final enabled = !ffi.ffiModel.viewOnly;
return Column(
children: [
Divider(),
MenuButton(
child: Text(
'${translate('Local keyboard type')}: ${KBLayoutType.value}'),
trailingIcon: const Icon(Icons.settings),
ffi: ffi,
onPressed: enabled
? () => showKBLayoutTypeChooser(localPlatform, ffi.dialogManager)
: null,
)
],
);
}
viewMode() {
final ffiModel = ffi.ffiModel;
final enabled = version_cmp(pi.version, '1.2.0') >= 0 && ffiModel.keyboard;
return CkbMenuButton(
value: ffiModel.viewOnly,
onChanged: enabled
? (value) async {
if (value == null) return;
await bind.sessionToggleOption(
sessionId: ffi.sessionId, value: 'view-only');
ffiModel.setViewOnly(id, value);
}
: null,
ffi: ffi,
child: Text(translate('View Mode')));
}
reverseMouseWheel() {
return futureBuilder(future: () async {
final v =
await bind.sessionGetReverseMouseWheel(sessionId: ffi.sessionId);
if (v != null && v != '') {
return v;
}
return bind.mainGetUserDefaultOption(key: 'reverse_mouse_wheel');
}(), hasData: (data) {
final enabled = !ffi.ffiModel.viewOnly;
onChanged(bool? value) async {
if (value == null) return;
await bind.sessionSetReverseMouseWheel(
sessionId: ffi.sessionId, value: value ? 'Y' : 'N');
}
return CkbMenuButton(
value: data == 'Y',
onChanged: enabled ? onChanged : null,
child: Text(translate('Reverse mouse wheel')),
ffi: ffi);
});
}
}
class _ChatMenu extends StatefulWidget {
final String id;
final FFI ffi;
_ChatMenu({
Key? key,
required this.id,
required this.ffi,
}) : super(key: key);
@override
State<_ChatMenu> createState() => _ChatMenuState();
}
class _ChatMenuState extends State<_ChatMenu> {
// Using in StatelessWidget got `Looking up a deactivated widget's ancestor is unsafe`.
final chatButtonKey = GlobalKey();
@override
Widget build(BuildContext context) {
return _IconSubmenuButton(
tooltip: 'Chat',
key: chatButtonKey,
svg: 'assets/chat.svg',
ffi: widget.ffi,
color: _ToolbarTheme.blueColor,
hoverColor: _ToolbarTheme.hoverBlueColor,
menuChildren: [textChat(), voiceCall()]);
}
textChat() {
return MenuButton(
child: Text(translate('Text chat')),
ffi: widget.ffi,
onPressed: () {
RenderBox? renderBox =
chatButtonKey.currentContext?.findRenderObject() as RenderBox?;
Offset? initPos;
if (renderBox != null) {
final pos = renderBox.localToGlobal(Offset.zero);
initPos = Offset(pos.dx, pos.dy + _ToolbarTheme.dividerHeight);
}
widget.ffi.chatModel.changeCurrentKey(
MessageKey(widget.ffi.id, ChatModel.clientModeID));
widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos);
});
}
voiceCall() {
return MenuButton(
child: Text(translate('Voice call')),
ffi: widget.ffi,
onPressed: () =>
bind.sessionRequestVoiceCall(sessionId: widget.ffi.sessionId),
);
}
}
class _VoiceCallMenu extends StatelessWidget {
final String id;
final FFI ffi;
_VoiceCallMenu({
Key? key,
required this.id,
required this.ffi,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Obx(
() {
final String tooltip;
final String icon;
switch (ffi.chatModel.voiceCallStatus.value) {
case VoiceCallStatus.waitingForResponse:
tooltip = "Waiting";
icon = "assets/call_wait.svg";
break;
case VoiceCallStatus.connected:
tooltip = "Disconnect";
icon = "assets/call_end.svg";
break;
default:
return Offstage();
}
return _IconMenuButton(
assetName: icon,
tooltip: tooltip,
onPressed: () =>
bind.sessionCloseVoiceCall(sessionId: ffi.sessionId),
color: _ToolbarTheme.redColor,
hoverColor: _ToolbarTheme.hoverRedColor);
},
);
}
}
class _RecordMenu extends StatelessWidget {
final FFI ffi;
const _RecordMenu({Key? key, required this.ffi}) : super(key: key);
@override
Widget build(BuildContext context) {
var ffiModel = Provider.of<FfiModel>(context);
var recordingModel = Provider.of<RecordingModel>(context);
final visible =
recordingModel.start || ffiModel.permissions['recording'] != false;
if (!visible) return Offstage();
final menuButton = _IconMenuButton(
assetName: 'assets/rec.svg',
tooltip: recordingModel.start
? 'Stop session recording'
: 'Start session recording',
onPressed: () => recordingModel.toggle(),
color: recordingModel.start
? _ToolbarTheme.redColor
: _ToolbarTheme.blueColor,
hoverColor: recordingModel.start
? _ToolbarTheme.hoverRedColor
: _ToolbarTheme.hoverBlueColor,
);
return ChangeNotifierProvider.value(
value: ffi.qualityMonitorModel,
child: Consumer<QualityMonitorModel>(
builder: (context, model, child) => Offstage(
// If already started, AV1->Hidden/Stop, Other->Start, same as actual
offstage: model.data.codecFormat == 'AV1',
child: menuButton,
)));
}
}
class _CloseMenu extends StatelessWidget {
final String id;
final FFI ffi;
const _CloseMenu({Key? key, required this.id, required this.ffi})
: super(key: key);
@override
Widget build(BuildContext context) {
return _IconMenuButton(
assetName: 'assets/close.svg',
tooltip: 'Close',
onPressed: () => clientClose(ffi.sessionId, ffi.dialogManager),
color: _ToolbarTheme.redColor,
hoverColor: _ToolbarTheme.hoverRedColor,
);
}
}
class _IconMenuButton extends StatefulWidget {
final String? assetName;
final Widget? icon;
final String tooltip;
final Color color;
final Color hoverColor;
final VoidCallback? onPressed;
final double? hMargin;
final double? vMargin;
final bool topLevel;
const _IconMenuButton({
Key? key,
this.assetName,
this.icon,
required this.tooltip,
required this.color,
required this.hoverColor,
required this.onPressed,
this.hMargin,
this.vMargin,
this.topLevel = true,
}) : super(key: key);
@override
State<_IconMenuButton> createState() => _IconMenuButtonState();
}
class _IconMenuButtonState extends State<_IconMenuButton> {
bool hover = false;
@override
Widget build(BuildContext context) {
assert(widget.assetName != null || widget.icon != null);
final icon = widget.icon ??
SvgPicture.asset(
widget.assetName!,
colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn),
width: _ToolbarTheme.buttonSize,
height: _ToolbarTheme.buttonSize,
);
var button = SizedBox(
width: _ToolbarTheme.buttonSize,
height: _ToolbarTheme.buttonSize,
child: MenuItemButton(
style: ButtonStyle(
backgroundColor: MaterialStatePropertyAll(Colors.transparent),
padding: MaterialStatePropertyAll(EdgeInsets.zero),
overlayColor: MaterialStatePropertyAll(Colors.transparent)),
onHover: (value) => setState(() {
hover = value;
}),
onPressed: widget.onPressed,
child: Tooltip(
message: translate(widget.tooltip),
child: Material(
type: MaterialType.transparency,
child: Ink(
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(_ToolbarTheme.iconRadius),
color: hover ? widget.hoverColor : widget.color,
),
child: icon)),
)),
).marginSymmetric(
horizontal: widget.hMargin ?? _ToolbarTheme.buttonHMargin,
vertical: widget.vMargin ?? _ToolbarTheme.buttonVMargin);
button = Tooltip(
message: widget.tooltip,
child: button,
);
if (widget.topLevel) {
return MenuBar(children: [button]);
} else {
return button;
}
}
}
class _IconSubmenuButton extends StatefulWidget {
final String tooltip;
final String? svg;
final Widget? icon;
final Color color;
final Color hoverColor;
final List<Widget> menuChildren;
final MenuStyle? menuStyle;
final FFI ffi;
_IconSubmenuButton(
{Key? key,
this.svg,
this.icon,
required this.tooltip,
required this.color,
required this.hoverColor,
required this.menuChildren,
required this.ffi,
this.menuStyle})
: super(key: key);
@override
State<_IconSubmenuButton> createState() => _IconSubmenuButtonState();
}
class _IconSubmenuButtonState extends State<_IconSubmenuButton> {
bool hover = false;
@override
Widget build(BuildContext context) {
assert(widget.svg != null || widget.icon != null);
final icon = widget.icon ??
SvgPicture.asset(
widget.svg!,
colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn),
width: _ToolbarTheme.buttonSize,
height: _ToolbarTheme.buttonSize,
);
final button = SizedBox(
width: _ToolbarTheme.buttonSize,
height: _ToolbarTheme.buttonSize,
child: SubmenuButton(
menuStyle: widget.menuStyle ?? _ToolbarTheme.defaultMenuStyle,
style: _ToolbarTheme.defaultMenuButtonStyle,
onHover: (value) => setState(() {
hover = value;
}),
child: Tooltip(
message: translate(widget.tooltip),
child: Material(
type: MaterialType.transparency,
child: Ink(
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(_ToolbarTheme.iconRadius),
color: hover ? widget.hoverColor : widget.color,
),
child: icon))),
menuChildren: widget.menuChildren
.map((e) => _buildPointerTrackWidget(e, widget.ffi))
.toList()));
return MenuBar(children: [
button.marginSymmetric(
horizontal: _ToolbarTheme.buttonHMargin,
vertical: _ToolbarTheme.buttonVMargin)
]);
}
}
class _SubmenuButton extends StatelessWidget {
final List<Widget> menuChildren;
final Widget? child;
final FFI ffi;
const _SubmenuButton({
Key? key,
required this.menuChildren,
required this.child,
required this.ffi,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return SubmenuButton(
key: key,
child: child,
menuChildren:
menuChildren.map((e) => _buildPointerTrackWidget(e, ffi)).toList(),
menuStyle: _ToolbarTheme.defaultMenuStyle,
);
}
}
class MenuButton extends StatelessWidget {
final VoidCallback? onPressed;
final Widget? trailingIcon;
final Widget? child;
final FFI ffi;
MenuButton(
{Key? key,
this.onPressed,
this.trailingIcon,
required this.child,
required this.ffi})
: super(key: key);
@override
Widget build(BuildContext context) {
return MenuItemButton(
key: key,
onPressed: onPressed != null
? () {
_menuDismissCallback(ffi);
onPressed?.call();
}
: null,
trailingIcon: trailingIcon,
child: child);
}
}
class CkbMenuButton extends StatelessWidget {
final bool? value;
final ValueChanged<bool?>? onChanged;
final Widget? child;
final FFI ffi;
const CkbMenuButton(
{Key? key,
required this.value,
required this.onChanged,
required this.child,
required this.ffi})
: super(key: key);
@override
Widget build(BuildContext context) {
return CheckboxMenuButton(
key: key,
value: value,
child: child,
onChanged: onChanged != null
? (bool? value) {
_menuDismissCallback(ffi);
onChanged?.call(value);
}
: null,
);
}
}
class RdoMenuButton<T> extends StatelessWidget {
final T value;
final T? groupValue;
final ValueChanged<T?>? onChanged;
final Widget? child;
final FFI ffi;
const RdoMenuButton({
Key? key,
required this.value,
required this.groupValue,
required this.child,
required this.ffi,
this.onChanged,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return RadioMenuButton(
value: value,
groupValue: groupValue,
child: child,
onChanged: onChanged != null
? (T? value) {
_menuDismissCallback(ffi);
onChanged?.call(value);
}
: null,
);
}
}
class _DraggableShowHide extends StatefulWidget {
final SessionID sessionId;
final RxDouble fractionX;
final RxBool dragging;
final RxBool show;
final Function(bool) setFullscreen;
final Function() setMinimize;
const _DraggableShowHide({
Key? key,
required this.sessionId,
required this.fractionX,
required this.dragging,
required this.show,
required this.setFullscreen,
required this.setMinimize,
}) : super(key: key);
@override
State<_DraggableShowHide> createState() => _DraggableShowHideState();
}
class _DraggableShowHideState extends State<_DraggableShowHide> {
Offset position = Offset.zero;
Size size = Size.zero;
double left = 0.0;
double right = 1.0;
@override
initState() {
super.initState();
final confLeft = double.tryParse(
bind.mainGetLocalOption(key: 'remote-menubar-drag-left'));
if (confLeft == null) {
bind.mainSetLocalOption(
key: 'remote-menubar-drag-left', value: left.toString());
} else {
left = confLeft;
}
final confRight = double.tryParse(
bind.mainGetLocalOption(key: 'remote-menubar-drag-right'));
if (confRight == null) {
bind.mainSetLocalOption(
key: 'remote-menubar-drag-right', value: right.toString());
} else {
right = confRight;
}
}
Widget _buildDraggable(BuildContext context) {
return Draggable(
axis: Axis.horizontal,
child: Icon(
Icons.drag_indicator,
size: 20,
color: MyTheme.color(context).drag_indicator,
),
feedback: widget,
onDragStarted: (() {
final RenderObject? renderObj = context.findRenderObject();
if (renderObj != null) {
final RenderBox renderBox = renderObj as RenderBox;
size = renderBox.size;
position = renderBox.localToGlobal(Offset.zero);
}
widget.dragging.value = true;
}),
onDragEnd: (details) {
final mediaSize = MediaQueryData.fromView(View.of(context)).size;
widget.fractionX.value +=
(details.offset.dx - position.dx) / (mediaSize.width - size.width);
if (widget.fractionX.value < left) {
widget.fractionX.value = left;
}
if (widget.fractionX.value > right) {
widget.fractionX.value = right;
}
bind.sessionPeerOption(
sessionId: widget.sessionId,
name: 'remote-menubar-drag-x',
value: widget.fractionX.value.toString(),
);
widget.dragging.value = false;
},
);
}
@override
Widget build(BuildContext context) {
final ButtonStyle buttonStyle = ButtonStyle(
minimumSize: MaterialStateProperty.all(const Size(0, 0)),
padding: MaterialStateProperty.all(EdgeInsets.zero),
);
final isFullscreen = stateGlobal.fullscreen;
const double iconSize = 20;
final child = Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildDraggable(context),
TextButton(
onPressed: () {
widget.setFullscreen(!isFullscreen);
setState(() {});
},
child: Tooltip(
message: translate(isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'),
child: Icon(
isFullscreen ? Icons.fullscreen_exit : Icons.fullscreen,
size: iconSize,
),
),
),
Offstage(
offstage: !isFullscreen,
child: TextButton(
onPressed: () => widget.setMinimize(),
child: Tooltip(
message: translate('Minimize'),
child: Icon(
Icons.remove,
size: iconSize,
),
),
),
),
TextButton(
onPressed: () => setState(() {
widget.show.value = !widget.show.value;
}),
child: Obx((() => Tooltip(
message: translate(
widget.show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
child: Icon(
widget.show.isTrue ? Icons.expand_less : Icons.expand_more,
size: iconSize,
),
))),
),
],
);
return TextButtonTheme(
data: TextButtonThemeData(style: buttonStyle),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context)
.menuBarTheme
.style
?.backgroundColor
?.resolve(MaterialState.values.toSet()),
2023-02-14 20:57:33 +08:00
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(5),
),
),
child: SizedBox(
height: 20,
child: child,
),
),
);
}
}
class InputModeMenu {
final String key;
final String menu;
InputModeMenu({required this.key, required this.menu});
}
_menuDismissCallback(FFI ffi) => ffi.inputModel.refreshMousePos();
Widget _buildPointerTrackWidget(Widget child, FFI ffi) {
return Listener(
onPointerHover: (PointerHoverEvent e) =>
ffi.inputModel.lastMousePos = e.position,
child: MouseRegion(
child: child,
),
);
}
class _MultiMonitorMenu extends StatelessWidget {
final String id;
final FFI ffi;
const _MultiMonitorMenu({
2023-03-24 01:54:49 +08:00
Key? key,
required this.id,
required this.ffi,
2023-03-24 01:54:49 +08:00
}) : super(key: key);
@override
Widget build(BuildContext context) {
final List<Widget> rowChildren = [];
final pi = ffi.ffiModel.pi;
for (int i = 0; i < pi.displays.length; i++) {
rowChildren.add(
Obx(() {
RxInt display = CurrentDisplayState.find(id);
return _IconMenuButton(
tooltip: "",
topLevel: false,
color: i == display.value
? _ToolbarTheme.blueColor
: Colors.grey[800]!,
hoverColor: i == display.value
? _ToolbarTheme.hoverBlueColor
: Colors.grey[850]!,
icon: Container(
alignment: AlignmentDirectional.center,
constraints:
const BoxConstraints(minHeight: _ToolbarTheme.height),
child: Stack(
alignment: Alignment.center,
children: [
SvgPicture.asset(
"assets/screen.svg",
colorFilter:
ColorFilter.mode(Colors.white, BlendMode.srcIn),
),
Obx(
() => Text(
(i + 1).toString(),
style: TextStyle(
color: i == display.value
? _ToolbarTheme.blueColor
: Colors.grey[800]!,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
onPressed: () {
if (display.value != i) {
bind.sessionSwitchDisplay(sessionId: ffi.sessionId, value: i);
}
},
);
}),
);
}
return Row(children: rowChildren);
}
}