mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-07-16 21:05:07 +03:00
Feature: Add monitor-switch buttons to remote toolbars (#15314)
* Feature: add monitor-switch buttons to remote toolbars Add a one-click "switch to next monitor" control to both desktop toolbars: - Main toolbar: always shown when the remote has more than one monitor, styled to match the existing blue icon buttons (white screen, black number). - Minimized (draggable show/hide) toolbar: off by default, toggled via a new "Show monitor switch on minimized toolbar" checkbox in the Display menu and persisted as a local option. Signed-off-by: StealUrKill <35749471+StealUrKill@users.noreply.github.com> * Update remote_toolbar.dart * refact: unify monitor-switch button icons, share tooltip Addressing the review feedback on the monitor-switch toolbar buttons: - Add assets/display_switcher.svg and use it for both the main and minimized buttons. This replaces the hand-drawn glyph (Containers + magic numbers) on the main toolbar. The icon scales with DPI/theme and the two toolbars stay visually consistent. - Flip the minimized button's label to white for contrast, since the new icon has a solid screen. - Move the tooltip string into a shared _MonitorCycle.tooltip getter so both buttons use one source of truth. - Use const Offstage() for consistency with the surrounding returns. Signed-off-by: StealUrKill <35749471+StealUrKill@users.noreply.github.com> * Improve monitor-switch settings and toolbar behavior - Nest the minimized-toolbar option under the main one in settings only show when the main option is enabled. - Only show the minimized switch button on the collapsed toolbar handle, so it no longer duplicates the main switch while the toolbar is expanded. Signed-off-by: StealUrKill <35749471+StealUrKill@users.noreply.github.com> --------- Signed-off-by: StealUrKill <35749471+StealUrKill@users.noreply.github.com> Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
This commit is contained in:
7
flutter/assets/display_switcher.svg
Normal file
7
flutter/assets/display_switcher.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||||
|
<g fill="#000000" fill-rule="evenodd">
|
||||||
|
<rect x="4" y="6" width="24" height="16" rx="3"/>
|
||||||
|
<rect x="14.5" y="22" width="3" height="2"/>
|
||||||
|
<rect x="9.5" y="24" width="13" height="2.5" rx="1.25"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 303 B |
@@ -174,6 +174,8 @@ const String kOptionShowVirtualMouse = "show-virtual-mouse";
|
|||||||
const String kOptionVirtualMouseScale = "virtual-mouse-scale";
|
const String kOptionVirtualMouseScale = "virtual-mouse-scale";
|
||||||
const String kOptionShowVirtualJoystick = "show-virtual-joystick";
|
const String kOptionShowVirtualJoystick = "show-virtual-joystick";
|
||||||
const String kOptionAllowAskForNoteAtEndOfConnection = "allow-ask-for-note";
|
const String kOptionAllowAskForNoteAtEndOfConnection = "allow-ask-for-note";
|
||||||
|
const String kOptionAllowMonitorSwitchMainToolbar = "allow-monitor-switch-main-toolbar";
|
||||||
|
const String kOptionAllowMonitorSwitchMinToolbar = "allow-monitor-switch-min-toolbar";
|
||||||
const String kOptionEnableShowTerminalExtraKeys = "enable-show-terminal-extra-keys";
|
const String kOptionEnableShowTerminalExtraKeys = "enable-show-terminal-extra-keys";
|
||||||
|
|
||||||
// network options
|
// network options
|
||||||
|
|||||||
@@ -407,6 +407,7 @@ class _GeneralState extends State<_General> {
|
|||||||
final RxBool serviceStop =
|
final RxBool serviceStop =
|
||||||
isWeb ? RxBool(false) : Get.find<RxBool>(tag: 'stop-service');
|
isWeb ? RxBool(false) : Get.find<RxBool>(tag: 'stop-service');
|
||||||
RxBool serviceBtnEnabled = true.obs;
|
RxBool serviceBtnEnabled = true.obs;
|
||||||
|
final GlobalKey _minToolbarOptionKey = GlobalKey();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -605,6 +606,47 @@ class _GeneralState extends State<_General> {
|
|||||||
},
|
},
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
children.add(_OptionCheckBox(
|
||||||
|
context,
|
||||||
|
'Show monitor switch button on the main toolbar',
|
||||||
|
kOptionAllowMonitorSwitchMainToolbar,
|
||||||
|
isServer: false,
|
||||||
|
update: (enabled) async {
|
||||||
|
if (!enabled) {
|
||||||
|
await mainSetLocalBoolOption(
|
||||||
|
kOptionAllowMonitorSwitchMinToolbar, false);
|
||||||
|
}
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
reloadAllWindows();
|
||||||
|
if (enabled) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
final ctx = _minToolbarOptionKey.currentContext;
|
||||||
|
if (ctx != null) {
|
||||||
|
Scrollable.ensureVisible(
|
||||||
|
ctx,
|
||||||
|
alignment: 0.5,
|
||||||
|
duration: const Duration(milliseconds: 250),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
));
|
||||||
|
if (mainGetLocalBoolOptionSync(kOptionAllowMonitorSwitchMainToolbar)) {
|
||||||
|
children.add(KeyedSubtree(
|
||||||
|
key: _minToolbarOptionKey,
|
||||||
|
child: _OptionCheckBox(
|
||||||
|
context,
|
||||||
|
'Show on the minimized toolbar',
|
||||||
|
kOptionAllowMonitorSwitchMinToolbar,
|
||||||
|
isServer: false,
|
||||||
|
update: (_) {
|
||||||
|
reloadAllWindows();
|
||||||
|
},
|
||||||
|
).marginOnly(left: _kCheckBoxLeftMargin * 3),
|
||||||
|
));
|
||||||
|
}
|
||||||
return _Card(title: 'Other', children: children);
|
return _Card(title: 'Other', children: children);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -779,6 +779,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
|||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
child: _DraggableShowHide(
|
child: _DraggableShowHide(
|
||||||
id: widget.id,
|
id: widget.id,
|
||||||
|
ffi: widget.ffi,
|
||||||
sessionId: widget.ffi.sessionId,
|
sessionId: widget.ffi.sessionId,
|
||||||
dragging: _dragging,
|
dragging: _dragging,
|
||||||
fraction: _fraction,
|
fraction: _fraction,
|
||||||
@@ -805,6 +806,16 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
|||||||
BuildContext context, _ToolbarEdge edge, bool isHorizontal) {
|
BuildContext context, _ToolbarEdge edge, bool isHorizontal) {
|
||||||
final List<Widget> toolbarItems = [];
|
final List<Widget> toolbarItems = [];
|
||||||
toolbarItems.add(_PinMenu(state: widget.state));
|
toolbarItems.add(_PinMenu(state: widget.state));
|
||||||
|
toolbarItems.add(Obx(() {
|
||||||
|
if ((PrivacyModeState.find(widget.id).isEmpty ||
|
||||||
|
allowDisplaySwitchInPrivacyMode(pi)) &&
|
||||||
|
pi.displaysCount.value > 1 &&
|
||||||
|
mainGetLocalBoolOptionSync(kOptionAllowMonitorSwitchMainToolbar)) {
|
||||||
|
return _MainMonitorSwitchButton(id: widget.id, ffi: widget.ffi);
|
||||||
|
} else {
|
||||||
|
return const Offstage();
|
||||||
|
}
|
||||||
|
}));
|
||||||
if (!isWebDesktop) {
|
if (!isWebDesktop) {
|
||||||
toolbarItems.add(_MobileActionMenu(ffi: widget.ffi));
|
toolbarItems.add(_MobileActionMenu(ffi: widget.ffi));
|
||||||
}
|
}
|
||||||
@@ -965,6 +976,80 @@ class _MobileActionMenu extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _MonitorCycle {
|
||||||
|
final String id;
|
||||||
|
final FFI ffi;
|
||||||
|
const _MonitorCycle(this.id, this.ffi);
|
||||||
|
|
||||||
|
PeerInfo get _pi => ffi.ffiModel.pi;
|
||||||
|
int get total => _pi.displays.length;
|
||||||
|
int get _current => CurrentDisplayState.find(id).value;
|
||||||
|
bool get _inRange => _current >= 0 && _current < total;
|
||||||
|
|
||||||
|
String get label => _inRange ? '${_current + 1}' : '*';
|
||||||
|
String get tooltip => '${translate('Switch display')} ($label/$total)';
|
||||||
|
|
||||||
|
void next() {
|
||||||
|
final t = total;
|
||||||
|
if (t < 2) return;
|
||||||
|
final from = _inRange ? _current : -1;
|
||||||
|
openMonitorInTheSameTab((from + 1) % t, ffi, _pi, updateCursorPos: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MainMonitorSwitchButton extends StatelessWidget {
|
||||||
|
final String id;
|
||||||
|
final FFI ffi;
|
||||||
|
|
||||||
|
const _MainMonitorSwitchButton({
|
||||||
|
Key? key,
|
||||||
|
required this.id,
|
||||||
|
required this.ffi,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final cycle = _MonitorCycle(id, ffi);
|
||||||
|
return Obx(() {
|
||||||
|
if (cycle.total < 2) return const Offstage();
|
||||||
|
final label = cycle.label;
|
||||||
|
|
||||||
|
return _IconMenuButton(
|
||||||
|
tooltip: cycle.tooltip,
|
||||||
|
color: _ToolbarTheme.blueColor,
|
||||||
|
hoverColor: _ToolbarTheme.hoverBlueColor,
|
||||||
|
onPressed: cycle.next,
|
||||||
|
icon: SizedBox(
|
||||||
|
width: _ToolbarTheme.buttonSize,
|
||||||
|
height: _ToolbarTheme.buttonSize,
|
||||||
|
child: Stack(
|
||||||
|
alignment: const Alignment(0, -0.125),
|
||||||
|
children: [
|
||||||
|
SvgPicture.asset(
|
||||||
|
'assets/display_switcher.svg',
|
||||||
|
colorFilter:
|
||||||
|
const ColorFilter.mode(Colors.white, BlendMode.srcIn),
|
||||||
|
width: _ToolbarTheme.buttonSize,
|
||||||
|
height: _ToolbarTheme.buttonSize,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.black,
|
||||||
|
fontSize: 11,
|
||||||
|
height: 1,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _MonitorMenu extends StatelessWidget {
|
class _MonitorMenu extends StatelessWidget {
|
||||||
final String id;
|
final String id;
|
||||||
final FFI ffi;
|
final FFI ffi;
|
||||||
@@ -2971,6 +3056,7 @@ class RdoMenuButton<T> extends StatelessWidget {
|
|||||||
|
|
||||||
class _DraggableShowHide extends StatefulWidget {
|
class _DraggableShowHide extends StatefulWidget {
|
||||||
final String id;
|
final String id;
|
||||||
|
final FFI ffi;
|
||||||
final SessionID sessionId;
|
final SessionID sessionId;
|
||||||
final RxDouble fraction;
|
final RxDouble fraction;
|
||||||
final Rx<_ToolbarEdge> edge;
|
final Rx<_ToolbarEdge> edge;
|
||||||
@@ -2994,6 +3080,7 @@ class _DraggableShowHide extends StatefulWidget {
|
|||||||
const _DraggableShowHide({
|
const _DraggableShowHide({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.id,
|
required this.id,
|
||||||
|
required this.ffi,
|
||||||
required this.sessionId,
|
required this.sessionId,
|
||||||
required this.fraction,
|
required this.fraction,
|
||||||
required this.edge,
|
required this.edge,
|
||||||
@@ -3250,6 +3337,9 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
_buildDraggable(context),
|
_buildDraggable(context),
|
||||||
|
Obx(() => collapse.isTrue
|
||||||
|
? _MinimizedMonitorSwitchButton(id: widget.id, ffi: widget.ffi)
|
||||||
|
: const Offstage()),
|
||||||
Obx(() => buttonWrapper(
|
Obx(() => buttonWrapper(
|
||||||
() {
|
() {
|
||||||
widget.setFullscreen(!isFullscreen.value);
|
widget.setFullscreen(!isFullscreen.value);
|
||||||
@@ -3410,3 +3500,70 @@ class EdgeThicknessControl extends StatelessWidget {
|
|||||||
return slider;
|
return slider;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _MinimizedMonitorSwitchButton extends StatelessWidget {
|
||||||
|
final String id;
|
||||||
|
final FFI ffi;
|
||||||
|
|
||||||
|
const _MinimizedMonitorSwitchButton({
|
||||||
|
Key? key,
|
||||||
|
required this.id,
|
||||||
|
required this.ffi,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
const double iconSize = 20;
|
||||||
|
final cycle = _MonitorCycle(id, ffi);
|
||||||
|
|
||||||
|
return Obx(() {
|
||||||
|
final label = cycle.label;
|
||||||
|
if (!mainGetLocalBoolOptionSync(kOptionAllowMonitorSwitchMinToolbar)) {
|
||||||
|
return const Offstage();
|
||||||
|
}
|
||||||
|
if (cycle.total < 2) return const Offstage();
|
||||||
|
if (PrivacyModeState.find(id).isNotEmpty &&
|
||||||
|
!allowDisplaySwitchInPrivacyMode(ffi.ffiModel.pi)) {
|
||||||
|
return const Offstage();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Tooltip(
|
||||||
|
message: cycle.tooltip,
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: cycle.next,
|
||||||
|
style: ButtonStyle(
|
||||||
|
minimumSize: MaterialStateProperty.all(const Size(0, 0)),
|
||||||
|
padding: MaterialStateProperty.all(EdgeInsets.zero),
|
||||||
|
backgroundColor: MaterialStateProperty.resolveWith((states) {
|
||||||
|
if (states.contains(MaterialState.hovered)) {
|
||||||
|
return _ToolbarTheme.blueColor.withOpacity(0.15);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
alignment: const Alignment(0, -0.125),
|
||||||
|
children: [
|
||||||
|
SvgPicture.asset(
|
||||||
|
'assets/display_switcher.svg',
|
||||||
|
colorFilter:
|
||||||
|
ColorFilter.mode(_ToolbarTheme.blueColor, BlendMode.srcIn),
|
||||||
|
width: iconSize,
|
||||||
|
height: iconSize,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 9,
|
||||||
|
height: 1,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user