* This happens when after changing DesktopTab to StatefulWidget, 1.2.7 and 1.3.0 have this problem. * When `addConnection` in server_model.dart is called, the old closed client is removed, the client parameter of buildConnectionCard is new, but client id inside Consumer is old. * The only state in cm page is timer, its value is kept in test. * There may be a better way to solve the ui update. Signed-off-by: 21pages <sunboeasy@gmail.com>
		
			
				
	
	
		
			1542 lines
		
	
	
		
			50 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			1542 lines
		
	
	
		
			50 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'dart:async';
 | |
| import 'dart:math';
 | |
| import 'dart:ui' as ui;
 | |
| 
 | |
| import 'package:bot_toast/bot_toast.dart';
 | |
| import 'package:desktop_multi_window/desktop_multi_window.dart';
 | |
| import 'package:flutter/gestures.dart';
 | |
| import 'package:flutter/material.dart' hide TabBarTheme;
 | |
| import 'package:flutter_hbb/common.dart';
 | |
| import 'package:flutter_hbb/consts.dart';
 | |
| import 'package:flutter_hbb/desktop/pages/remote_page.dart';
 | |
| import 'package:flutter_hbb/main.dart';
 | |
| import 'package:flutter_hbb/models/platform_model.dart';
 | |
| import 'package:flutter_hbb/models/state_model.dart';
 | |
| import 'package:get/get.dart';
 | |
| 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:visibility_detector/visibility_detector.dart';
 | |
| 
 | |
| import '../../utils/multi_window_manager.dart';
 | |
| 
 | |
| const double _kTabBarHeight = kDesktopRemoteTabBarHeight;
 | |
| const double _kIconSize = 18;
 | |
| const double _kDividerIndent = 10;
 | |
| const double _kActionIconSize = 12;
 | |
| 
 | |
| class TabInfo {
 | |
|   final String key; // Notice: cm use client_id.toString() as key
 | |
|   final String label;
 | |
|   final IconData? selectedIcon;
 | |
|   final IconData? unselectedIcon;
 | |
|   final bool closable;
 | |
|   final VoidCallback? onTabCloseButton;
 | |
|   final VoidCallback? onTap;
 | |
|   final Widget page;
 | |
| 
 | |
|   TabInfo(
 | |
|       {required this.key,
 | |
|       required this.label,
 | |
|       this.selectedIcon,
 | |
|       this.unselectedIcon,
 | |
|       this.closable = true,
 | |
|       this.onTabCloseButton,
 | |
|       this.onTap,
 | |
|       required this.page});
 | |
| }
 | |
| 
 | |
| enum DesktopTabType {
 | |
|   main,
 | |
|   cm,
 | |
|   remoteScreen,
 | |
|   fileTransfer,
 | |
|   portForward,
 | |
|   install,
 | |
| }
 | |
| 
 | |
| class DesktopTabState {
 | |
|   final List<TabInfo> tabs = [];
 | |
|   final ScrollPosController scrollController =
 | |
|       ScrollPosController(itemCount: 0);
 | |
|   final PageController pageController = PageController();
 | |
|   int selected = 0;
 | |
| 
 | |
|   TabInfo get selectedTabInfo => tabs[selected];
 | |
| 
 | |
|   DesktopTabState() {
 | |
|     scrollController.itemCount = tabs.length;
 | |
|   }
 | |
| }
 | |
| 
 | |
| CancelFunc showRightMenu(ToastBuilder builder,
 | |
|     {BuildContext? context, Offset? target}) {
 | |
|   return BotToast.showAttachedWidget(
 | |
|     target: target,
 | |
|     targetContext: context,
 | |
|     verticalOffset: 0.0,
 | |
|     horizontalOffset: 0.0,
 | |
|     duration: Duration(seconds: 300),
 | |
|     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;
 | |
| 
 | |
|   /// index, key
 | |
|   Function(int, String)? onRemoved;
 | |
|   Function(String)? onSelected;
 | |
| 
 | |
|   DesktopTabController(
 | |
|       {required this.tabType, this.onRemoved, this.onSelected});
 | |
| 
 | |
|   int get length => state.value.tabs.length;
 | |
| 
 | |
|   void add(TabInfo tab) {
 | |
|     if (!isDesktop) return;
 | |
|     final index = state.value.tabs.indexWhere((e) => e.key == tab.key);
 | |
|     int toIndex;
 | |
|     if (index >= 0) {
 | |
|       toIndex = index;
 | |
|     } else {
 | |
|       state.update((val) {
 | |
|         val!.tabs.add(tab);
 | |
|       });
 | |
|       state.value.scrollController.itemCount = state.value.tabs.length;
 | |
|       toIndex = state.value.tabs.length - 1;
 | |
|       assert(toIndex >= 0);
 | |
|     }
 | |
|     try {
 | |
|       // tabPage has not been initialized, call `onSelected` at the end of initState
 | |
|       jumpTo(toIndex, callOnSelected: false);
 | |
|     } catch (e) {
 | |
|       // call before binding controller will throw
 | |
|       debugPrint("Failed to jumpTo: $e");
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   void remove(int index) {
 | |
|     if (!isDesktop) return;
 | |
|     final len = state.value.tabs.length;
 | |
|     if (index < 0 || index > len - 1) return;
 | |
|     final key = state.value.tabs[index].key;
 | |
|     final currentSelected = state.value.selected;
 | |
|     int toIndex = 0;
 | |
|     if (index == len - 1) {
 | |
|       toIndex = max(0, currentSelected - 1);
 | |
|     } else if (index < len - 1 && index < currentSelected) {
 | |
|       toIndex = max(0, currentSelected - 1);
 | |
|     }
 | |
|     state.value.tabs.removeAt(index);
 | |
|     state.value.scrollController.itemCount = state.value.tabs.length;
 | |
|     jumpTo(toIndex);
 | |
|     onRemoved?.call(index, key);
 | |
|   }
 | |
| 
 | |
|   /// For addTab, tabPage has not been initialized, set [callOnSelected] to false,
 | |
|   /// and call [onSelected] at the end of initState
 | |
|   bool jumpTo(int index, {bool callOnSelected = true}) {
 | |
|     if (!isDesktop || index < 0) {
 | |
|       return false;
 | |
|     }
 | |
|     state.update((val) {
 | |
|       if (val != null) {
 | |
|         val.selected = index;
 | |
|         Future.delayed(Duration(milliseconds: 100), (() {
 | |
|           if (val.pageController.hasClients) {
 | |
|             val.pageController.jumpToPage(index);
 | |
|           }
 | |
|           val.scrollController.itemCount = val.tabs.length;
 | |
|           if (val.scrollController.hasClients &&
 | |
|               val.scrollController.itemCount > index) {
 | |
|             val.scrollController
 | |
|                 .scrollToItem(index, center: false, animate: true);
 | |
|           }
 | |
|         }));
 | |
|       }
 | |
|     });
 | |
|     if ((isDesktop && (bind.isIncomingOnly() || bind.isOutgoingOnly())) ||
 | |
|         callOnSelected) {
 | |
|       if (state.value.tabs.length > index) {
 | |
|         final key = state.value.tabs[index].key;
 | |
|         onSelected?.call(key);
 | |
|       }
 | |
|     }
 | |
|     return true;
 | |
|   }
 | |
| 
 | |
|   bool jumpToByKey(String key, {bool callOnSelected = true}) =>
 | |
|       jumpTo(state.value.tabs.indexWhere((tab) => tab.key == key),
 | |
|           callOnSelected: callOnSelected);
 | |
| 
 | |
|   bool jumpToByKeyAndDisplay(String key, int display) {
 | |
|     for (int i = 0; i < state.value.tabs.length; i++) {
 | |
|       final tab = state.value.tabs[i];
 | |
|       if (tab.key == key) {
 | |
|         final ffi = (tab.page as RemotePage).ffi;
 | |
|         if (ffi.ffiModel.pi.currentDisplay == display) {
 | |
|           return jumpTo(i, callOnSelected: true);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   void closeBy(String? key) {
 | |
|     if (!isDesktop) return;
 | |
|     assert(onRemoved != null);
 | |
|     if (key == null) {
 | |
|       if (state.value.selected < state.value.tabs.length) {
 | |
|         remove(state.value.selected);
 | |
|       }
 | |
|     } else {
 | |
|       final index = state.value.tabs.indexWhere((tab) => tab.key == key);
 | |
|       remove(index);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   void clear() {
 | |
|     state.value.tabs.clear();
 | |
|     state.refresh();
 | |
|   }
 | |
| 
 | |
|   Widget? widget(String key) {
 | |
|     return state.value.tabs.firstWhereOrNull((tab) => tab.key == key)?.page;
 | |
|   }
 | |
| }
 | |
| 
 | |
| class TabThemeConf {
 | |
|   double iconSize;
 | |
| 
 | |
|   TabThemeConf({required this.iconSize});
 | |
| }
 | |
| 
 | |
| 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
 | |
| int _lastClickTime = 0;
 | |
| 
 | |
| class DesktopTab extends StatefulWidget {
 | |
|   final bool showLogo;
 | |
|   final bool showTitle;
 | |
|   final bool showMinimize;
 | |
|   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;
 | |
|   final LabelGetter? labelGetter;
 | |
|   final double? maxLabelWidth;
 | |
|   final Color? selectedTabBackgroundColor;
 | |
|   final Color? unSelectedTabBackgroundColor;
 | |
|   final Color? selectedBorderColor;
 | |
|   final RxBool? blockTab;
 | |
| 
 | |
|   final DesktopTabController controller;
 | |
| 
 | |
|   final _scrollDebounce = Debouncer(delay: Duration(milliseconds: 50));
 | |
| 
 | |
|   final RxList<String> invisibleTabKeys = RxList.empty();
 | |
| 
 | |
|   DesktopTab({
 | |
|     Key? key,
 | |
|     required this.controller,
 | |
|     this.showLogo = true,
 | |
|     this.showTitle = false,
 | |
|     this.showMinimize = true,
 | |
|     this.showMaximize = true,
 | |
|     this.showClose = true,
 | |
|     this.pageViewBuilder,
 | |
|     this.tabMenuBuilder,
 | |
|     this.tail,
 | |
|     this.onWindowCloseButton,
 | |
|     this.tabBuilder,
 | |
|     this.labelGetter,
 | |
|     this.maxLabelWidth,
 | |
|     this.selectedTabBackgroundColor,
 | |
|     this.unSelectedTabBackgroundColor,
 | |
|     this.selectedBorderColor,
 | |
|     this.blockTab,
 | |
|   }) : super(key: key);
 | |
| 
 | |
|   static RxString tablabelGetter(String peerId) {
 | |
|     final alias = bind.mainGetPeerOptionSync(id: peerId, key: 'alias');
 | |
|     return RxString(getDesktopTabLabel(peerId, alias));
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   State<DesktopTab> createState() {
 | |
|     return _DesktopTabState();
 | |
|   }
 | |
| }
 | |
| 
 | |
| // ignore: must_be_immutable
 | |
| class _DesktopTabState extends State<DesktopTab>
 | |
|     with MultiWindowListener, WindowListener {
 | |
|   final _saveFrameDebounce = Debouncer(delay: Duration(seconds: 1));
 | |
|   Timer? _macOSCheckRestoreTimer;
 | |
|   int _macOSCheckRestoreCounter = 0;
 | |
| 
 | |
|   bool get showLogo => widget.showLogo;
 | |
|   bool get showTitle => widget.showTitle;
 | |
|   bool get showMinimize => widget.showMinimize;
 | |
|   bool get showMaximize => widget.showMaximize;
 | |
|   bool get showClose => widget.showClose;
 | |
|   Widget Function(Widget pageView)? get pageViewBuilder =>
 | |
|       widget.pageViewBuilder;
 | |
|   TabMenuBuilder? get tabMenuBuilder => widget.tabMenuBuilder;
 | |
|   Widget? get tail => widget.tail;
 | |
|   Future<bool> Function()? get onWindowCloseButton =>
 | |
|       widget.onWindowCloseButton;
 | |
|   TabBuilder? get tabBuilder => widget.tabBuilder;
 | |
|   LabelGetter? get labelGetter => widget.labelGetter;
 | |
|   double? get maxLabelWidth => widget.maxLabelWidth;
 | |
|   Color? get selectedTabBackgroundColor => widget.selectedTabBackgroundColor;
 | |
|   Color? get unSelectedTabBackgroundColor =>
 | |
|       widget.unSelectedTabBackgroundColor;
 | |
|   Color? get selectedBorderColor => widget.selectedBorderColor;
 | |
|   RxBool? get blockTab => widget.blockTab;
 | |
|   DesktopTabController get controller => widget.controller;
 | |
|   RxList<String> get invisibleTabKeys => widget.invisibleTabKeys;
 | |
|   Debouncer get _scrollDebounce => widget._scrollDebounce;
 | |
| 
 | |
|   Rx<DesktopTabState> get state => controller.state;
 | |
| 
 | |
|   DesktopTabType get tabType => controller.tabType;
 | |
|   bool get isMainWindow =>
 | |
|       tabType == DesktopTabType.main ||
 | |
|       tabType == DesktopTabType.cm ||
 | |
|       tabType == DesktopTabType.install;
 | |
| 
 | |
|   _DesktopTabState() : super();
 | |
| 
 | |
|   static RxString tablabelGetter(String peerId) {
 | |
|     final alias = bind.mainGetPeerOptionSync(id: peerId, key: 'alias');
 | |
|     return RxString(getDesktopTabLabel(peerId, alias));
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void initState() {
 | |
|     super.initState();
 | |
|     DesktopMultiWindow.addListener(this);
 | |
|     windowManager.addListener(this);
 | |
| 
 | |
|     Future.delayed(Duration(milliseconds: 500), () {
 | |
|       if (isMainWindow) {
 | |
|         windowManager.isMaximized().then((maximized) {
 | |
|           if (stateGlobal.isMaximized.value != maximized) {
 | |
|             WidgetsBinding.instance.addPostFrameCallback(
 | |
|                 (_) => setState(() => stateGlobal.setMaximized(maximized)));
 | |
|           }
 | |
|         });
 | |
|       } else {
 | |
|         final wc = WindowController.fromWindowId(kWindowId!);
 | |
|         wc.isMaximized().then((maximized) {
 | |
|           debugPrint("isMaximized $maximized");
 | |
|           if (stateGlobal.isMaximized.value != maximized) {
 | |
|             WidgetsBinding.instance.addPostFrameCallback(
 | |
|                 (_) => setState(() => stateGlobal.setMaximized(maximized)));
 | |
|           }
 | |
|         });
 | |
|       }
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void dispose() {
 | |
|     DesktopMultiWindow.removeListener(this);
 | |
|     windowManager.removeListener(this);
 | |
|     _macOSCheckRestoreTimer?.cancel();
 | |
|     super.dispose();
 | |
|   }
 | |
| 
 | |
|   void _setMaximized(bool maximize) {
 | |
|     stateGlobal.setMaximized(maximize);
 | |
|     _saveFrameDebounce.call(_saveFrame);
 | |
|     setState(() {});
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void onWindowFocus() {
 | |
|     stateGlobal.isFocused.value = true;
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void onWindowBlur() {
 | |
|     stateGlobal.isFocused.value = false;
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void onWindowMinimize() {
 | |
|     stateGlobal.setMinimized(true);
 | |
|     stateGlobal.setMaximized(false);
 | |
|     super.onWindowMinimize();
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void onWindowMaximize() {
 | |
|     stateGlobal.setMinimized(false);
 | |
|     _setMaximized(true);
 | |
|     super.onWindowMaximize();
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void onWindowUnmaximize() {
 | |
|     stateGlobal.setMinimized(false);
 | |
|     _setMaximized(false);
 | |
|     super.onWindowUnmaximize();
 | |
|   }
 | |
| 
 | |
|   _saveFrame() async {
 | |
|     if (tabType == DesktopTabType.main) {
 | |
|       await saveWindowPosition(WindowType.Main);
 | |
|     } else if (kWindowType != null && kWindowId != null) {
 | |
|       await saveWindowPosition(kWindowType!, windowId: kWindowId);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void onWindowMoved() {
 | |
|     _saveFrameDebounce.call(_saveFrame);
 | |
|     super.onWindowMoved();
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void onWindowResized() {
 | |
|     _saveFrameDebounce.call(_saveFrame);
 | |
|     super.onWindowMoved();
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void onWindowClose() async {
 | |
|     mainWindowClose() async => await windowManager.hide();
 | |
|     notMainWindowClose(WindowController windowController) async {
 | |
|       if (controller.length != 0) {
 | |
|         debugPrint("close not empty multiwindow from taskbar");
 | |
|         if (isWindows) {
 | |
|           await windowController.show();
 | |
|           await windowController.focus();
 | |
|           final res = await onWindowCloseButton?.call() ?? true;
 | |
|           if (!res) return;
 | |
|         }
 | |
|         controller.clear();
 | |
|       }
 | |
|       await windowController.hide();
 | |
|       await rustDeskWinManager
 | |
|           .call(WindowType.Main, kWindowEventHide, {"id": kWindowId!});
 | |
|     }
 | |
| 
 | |
|     macOSWindowClose(
 | |
|       Future<bool> Function() checkFullscreen,
 | |
|       Future<void> Function() closeFunc,
 | |
|     ) async {
 | |
|       _macOSCheckRestoreCounter = 0;
 | |
|       _macOSCheckRestoreTimer =
 | |
|           Timer.periodic(Duration(milliseconds: 30), (timer) async {
 | |
|         _macOSCheckRestoreCounter++;
 | |
|         if (!await checkFullscreen() || _macOSCheckRestoreCounter >= 30) {
 | |
|           _macOSCheckRestoreTimer?.cancel();
 | |
|           _macOSCheckRestoreTimer = null;
 | |
|           Timer(Duration(milliseconds: 700), () async => await closeFunc());
 | |
|         }
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     // hide window on close
 | |
|     if (isMainWindow) {
 | |
|       if (rustDeskWinManager.getActiveWindows().contains(kMainWindowId)) {
 | |
|         await rustDeskWinManager.unregisterActiveWindow(kMainWindowId);
 | |
|       }
 | |
|       // macOS specific workaround, the window is not hiding when in fullscreen.
 | |
|       if (isMacOS && await windowManager.isFullScreen()) {
 | |
|         await windowManager.setFullScreen(false);
 | |
|         await macOSWindowClose(
 | |
|           () async => await windowManager.isFullScreen(),
 | |
|           mainWindowClose,
 | |
|         );
 | |
|       } else {
 | |
|         await mainWindowClose();
 | |
|       }
 | |
|     } else {
 | |
|       // it's safe to hide the subwindow
 | |
|       final controller = WindowController.fromWindowId(kWindowId!);
 | |
|       if (isMacOS) {
 | |
|         // onWindowClose() maybe called multiple times because of loopCloseWindow() in remote_tab_page.dart.
 | |
|         // use ??=  to make sure the value is set on first call.
 | |
| 
 | |
|         if (await onWindowCloseButton?.call() ?? true) {
 | |
|           if (await controller.isFullScreen()) {
 | |
|             await controller.setFullscreen(false);
 | |
|             stateGlobal.setFullscreen(false, procWnd: false);
 | |
|             await macOSWindowClose(
 | |
|               () async => await controller.isFullScreen(),
 | |
|               () async => await notMainWindowClose(controller),
 | |
|             );
 | |
|           } else {
 | |
|             await notMainWindowClose(controller);
 | |
|           }
 | |
|         }
 | |
|       } else {
 | |
|         await notMainWindowClose(controller);
 | |
|       }
 | |
|     }
 | |
|     super.onWindowClose();
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return Column(children: [
 | |
|       Obx(() {
 | |
|         if (stateGlobal.showTabBar.isTrue &&
 | |
|             !(kUseCompatibleUiMode && isHideSingleItem())) {
 | |
|           final showBottomDivider = _showTabBarBottomDivider(tabType);
 | |
|           return SizedBox(
 | |
|             height: _kTabBarHeight,
 | |
|             child: Column(
 | |
|               children: [
 | |
|                 SizedBox(
 | |
|                   height:
 | |
|                       showBottomDivider ? _kTabBarHeight - 1 : _kTabBarHeight,
 | |
|                   child: _buildBar(),
 | |
|                 ),
 | |
|                 if (showBottomDivider)
 | |
|                   const Divider(
 | |
|                     height: 1,
 | |
|                   ),
 | |
|               ],
 | |
|             ),
 | |
|           );
 | |
|         } else {
 | |
|           return Offstage();
 | |
|         }
 | |
|       }),
 | |
|       Expanded(
 | |
|           child: pageViewBuilder != null
 | |
|               ? pageViewBuilder!(_buildPageView())
 | |
|               : _buildPageView())
 | |
|     ]);
 | |
|   }
 | |
| 
 | |
|   Widget _buildBlock({required Widget child}) {
 | |
|     if (blockTab != null) {
 | |
|       return buildRemoteBlock(
 | |
|           child: child,
 | |
|           block: blockTab!,
 | |
|           use: canBeBlocked,
 | |
|           mask: tabType == DesktopTabType.main);
 | |
|     } else {
 | |
|       return child;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   List<Widget> _tabWidgets = [];
 | |
|   Widget _buildPageView() {
 | |
|     final child = _buildBlock(
 | |
|         child: Obx(() => PageView(
 | |
|             controller: state.value.pageController,
 | |
|             physics: NeverScrollableScrollPhysics(),
 | |
|             children: () {
 | |
|               if (DesktopTabType.cm == tabType) {
 | |
|                 // Fix when adding a new tab still showing closed tabs with the same peer id, which would happen after the DesktopTab was stateful.
 | |
|                 return state.value.tabs.map((tab) {
 | |
|                   return tab.page;
 | |
|                 }).toList();
 | |
|               }
 | |
| 
 | |
|               /// to-do refactor, separate connection state and UI state for remote session.
 | |
|               /// [workaround] PageView children need an immutable list, after it has been passed into PageView
 | |
|               final tabLen = state.value.tabs.length;
 | |
|               if (tabLen == _tabWidgets.length) {
 | |
|                 return _tabWidgets;
 | |
|               } else if (_tabWidgets.isNotEmpty &&
 | |
|                   tabLen == _tabWidgets.length + 1) {
 | |
|                 /// On add. Use the previous list(pointer) to prevent item's state init twice.
 | |
|                 /// *[_tabWidgets.isNotEmpty] means TabsWindow(remote_tab_page or file_manager_tab_page) opened before, but was hidden. In this case, we have to reload, otherwise the child can't be built.
 | |
|                 _tabWidgets.add(state.value.tabs.last.page);
 | |
|                 return _tabWidgets;
 | |
|               } else {
 | |
|                 /// On remove or change. Use new list(pointer) to reload list children so that items loading order is normal.
 | |
|                 /// the Widgets in list must enable [AutomaticKeepAliveClientMixin]
 | |
|                 final newList = state.value.tabs.map((v) => v.page).toList();
 | |
|                 _tabWidgets = newList;
 | |
|                 return newList;
 | |
|               }
 | |
|             }())));
 | |
|     if (tabType == DesktopTabType.remoteScreen) {
 | |
|       return Container(color: kColorCanvas, child: child);
 | |
|     } else {
 | |
|       return child;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /// Check whether to show ListView
 | |
|   ///
 | |
|   /// Conditions:
 | |
|   /// - hide single item when only has one item (home) on [DesktopTabPage].
 | |
|   bool isHideSingleItem() {
 | |
|     return state.value.tabs.length == 1 &&
 | |
|         (controller.tabType == DesktopTabType.main ||
 | |
|             controller.tabType == DesktopTabType.install);
 | |
|   }
 | |
| 
 | |
|   Widget _buildBar() {
 | |
|     return Row(
 | |
|       mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | |
|       children: [
 | |
|         Expanded(
 | |
|             child: GestureDetector(
 | |
|                 // custom double tap handler
 | |
|                 onTap: !(bind.isIncomingOnly() && isInHomePage()) &&
 | |
|                         showMaximize
 | |
|                     ? () {
 | |
|                         final current = DateTime.now().millisecondsSinceEpoch;
 | |
|                         final elapsed = current - _lastClickTime;
 | |
|                         _lastClickTime = current;
 | |
|                         if (elapsed < bind.getDoubleClickTime()) {
 | |
|                           // onDoubleTap
 | |
|                           toggleMaximize(isMainWindow)
 | |
|                               .then((value) => stateGlobal.setMaximized(value));
 | |
|                         }
 | |
|                       }
 | |
|                     : null,
 | |
|                 onPanStart: (_) => startDragging(isMainWindow),
 | |
|                 onPanCancel: () {
 | |
|                   // We want to disable dragging of the tab area in the tab bar.
 | |
|                   // Disable dragging is needed because macOS handles dragging by default.
 | |
|                   if (isMacOS) {
 | |
|                     setMovable(isMainWindow, false);
 | |
|                   }
 | |
|                 },
 | |
|                 onPanEnd: (_) {
 | |
|                   if (isMacOS) {
 | |
|                     setMovable(isMainWindow, false);
 | |
|                   }
 | |
|                 },
 | |
|                 child: Row(
 | |
|                   children: [
 | |
|                     Offstage(
 | |
|                         offstage: !isMacOS,
 | |
|                         child: const SizedBox(
 | |
|                           width: 78,
 | |
|                         )),
 | |
|                     Offstage(
 | |
|                       offstage: kUseCompatibleUiMode || isMacOS,
 | |
|                       child: Row(children: [
 | |
|                         Offstage(
 | |
|                           offstage: !showLogo,
 | |
|                           child: loadIcon(16),
 | |
|                         ),
 | |
|                         Offstage(
 | |
|                             offstage: !showTitle,
 | |
|                             child: const Text(
 | |
|                               "RustDesk",
 | |
|                               style: TextStyle(fontSize: 13),
 | |
|                             ).marginOnly(left: 2))
 | |
|                       ]).marginOnly(
 | |
|                         left: 5,
 | |
|                         right: 10,
 | |
|                       ),
 | |
|                     ),
 | |
|                     Expanded(
 | |
|                         child: Listener(
 | |
|                             // handle mouse wheel
 | |
|                             onPointerSignal: (e) {
 | |
|                               if (e is PointerScrollEvent) {
 | |
|                                 final sc =
 | |
|                                     controller.state.value.scrollController;
 | |
|                                 if (!sc.canScroll) return;
 | |
|                                 _scrollDebounce.call(() {
 | |
|                                   sc.animateTo(sc.offset + e.scrollDelta.dy,
 | |
|                                       duration: Duration(milliseconds: 200),
 | |
|                                       curve: Curves.ease);
 | |
|                                 });
 | |
|                               }
 | |
|                             },
 | |
|                             child: _ListView(
 | |
|                               controller: controller,
 | |
|                               invisibleTabKeys: invisibleTabKeys,
 | |
|                               tabBuilder: tabBuilder,
 | |
|                               tabMenuBuilder: tabMenuBuilder,
 | |
|                               labelGetter: labelGetter,
 | |
|                               maxLabelWidth: maxLabelWidth,
 | |
|                               selectedTabBackgroundColor:
 | |
|                                   selectedTabBackgroundColor,
 | |
|                               unSelectedTabBackgroundColor:
 | |
|                                   unSelectedTabBackgroundColor,
 | |
|                               selectedBorderColor: selectedBorderColor,
 | |
|                             ))),
 | |
|                   ],
 | |
|                 ))),
 | |
|         // hide simulated action buttons when we in compatible ui mode, because of reusing system title bar.
 | |
|         WindowActionPanel(
 | |
|           isMainWindow: isMainWindow,
 | |
|           state: state,
 | |
|           tabController: controller,
 | |
|           invisibleTabKeys: invisibleTabKeys,
 | |
|           tail: tail,
 | |
|           showMinimize: showMinimize,
 | |
|           showMaximize: showMaximize,
 | |
|           showClose: showClose,
 | |
|           onClose: onWindowCloseButton,
 | |
|           labelGetter: labelGetter,
 | |
|         ).paddingOnly(left: 10)
 | |
|       ],
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class WindowActionPanel extends StatefulWidget {
 | |
|   final bool isMainWindow;
 | |
|   final Rx<DesktopTabState> state;
 | |
|   final DesktopTabController tabController;
 | |
| 
 | |
|   final bool showMinimize;
 | |
|   final bool showMaximize;
 | |
|   final bool showClose;
 | |
|   final Widget? tail;
 | |
|   final Future<bool> Function()? onClose;
 | |
| 
 | |
|   final RxList<String> invisibleTabKeys;
 | |
|   final LabelGetter? labelGetter;
 | |
| 
 | |
|   const WindowActionPanel(
 | |
|       {Key? key,
 | |
|       required this.isMainWindow,
 | |
|       required this.state,
 | |
|       required this.tabController,
 | |
|       required this.invisibleTabKeys,
 | |
|       this.tail,
 | |
|       this.showMinimize = true,
 | |
|       this.showMaximize = true,
 | |
|       this.showClose = true,
 | |
|       this.onClose,
 | |
|       this.labelGetter})
 | |
|       : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   State<StatefulWidget> createState() {
 | |
|     return WindowActionPanelState();
 | |
|   }
 | |
| }
 | |
| 
 | |
| class WindowActionPanelState extends State<WindowActionPanel> {
 | |
|   bool showTabDowndown() {
 | |
|     return widget.tabController.state.value.tabs.length > 1 &&
 | |
|         (widget.tabController.tabType == DesktopTabType.remoteScreen ||
 | |
|             widget.tabController.tabType == DesktopTabType.fileTransfer ||
 | |
|             widget.tabController.tabType == DesktopTabType.portForward ||
 | |
|             widget.tabController.tabType == DesktopTabType.cm);
 | |
|   }
 | |
| 
 | |
|   List<String> existingInvisibleTab() {
 | |
|     return widget.invisibleTabKeys
 | |
|         .where((key) =>
 | |
|             widget.tabController.state.value.tabs.any((tab) => tab.key == key))
 | |
|         .toList();
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return Row(
 | |
|       mainAxisAlignment: MainAxisAlignment.end,
 | |
|       children: [
 | |
|         Obx(() {
 | |
|           if (showTabDowndown() && existingInvisibleTab().isNotEmpty) {
 | |
|             return _TabDropDownButton(
 | |
|                 controller: widget.tabController,
 | |
|                 labelGetter: widget.labelGetter,
 | |
|                 tabkeys: existingInvisibleTab());
 | |
|           } else {
 | |
|             return Offstage();
 | |
|           }
 | |
|         }),
 | |
|         if (widget.tail != null) widget.tail!,
 | |
|         if (!kUseCompatibleUiMode)
 | |
|           Row(
 | |
|             children: [
 | |
|               if (widget.showMinimize && !isMacOS)
 | |
|                 ActionIcon(
 | |
|                   message: 'Minimize',
 | |
|                   icon: IconFont.min,
 | |
|                   onTap: () {
 | |
|                     if (widget.isMainWindow) {
 | |
|                       windowManager.minimize();
 | |
|                     } else {
 | |
|                       WindowController.fromWindowId(kWindowId!).minimize();
 | |
|                     }
 | |
|                   },
 | |
|                   isClose: false,
 | |
|                 ),
 | |
|               if (widget.showMaximize && !isMacOS)
 | |
|                 Obx(() => ActionIcon(
 | |
|                       message: stateGlobal.isMaximized.isTrue
 | |
|                           ? 'Restore'
 | |
|                           : 'Maximize',
 | |
|                       icon: stateGlobal.isMaximized.isTrue
 | |
|                           ? IconFont.restore
 | |
|                           : IconFont.max,
 | |
|                       onTap: bind.isIncomingOnly() && isInHomePage()
 | |
|                           ? null
 | |
|                           : _toggleMaximize,
 | |
|                       isClose: false,
 | |
|                     )),
 | |
|               if (widget.showClose && !isMacOS)
 | |
|                 ActionIcon(
 | |
|                   message: 'Close',
 | |
|                   icon: IconFont.close,
 | |
|                   onTap: () async {
 | |
|                     final res = await widget.onClose?.call() ?? true;
 | |
|                     if (res) {
 | |
|                       // hide for all window
 | |
|                       // note: the main window can be restored by tray icon
 | |
|                       Future.delayed(Duration.zero, () async {
 | |
|                         if (widget.isMainWindow) {
 | |
|                           await windowManager.close();
 | |
|                         } else {
 | |
|                           await WindowController.fromWindowId(kWindowId!)
 | |
|                               .close();
 | |
|                         }
 | |
|                       });
 | |
|                     }
 | |
|                   },
 | |
|                   isClose: true,
 | |
|                 )
 | |
|             ],
 | |
|           ),
 | |
|       ],
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   void _toggleMaximize() {
 | |
|     toggleMaximize(widget.isMainWindow).then((maximize) {
 | |
|       // update state for sub window, wc.unmaximize/maximize() will not invoke onWindowMaximize/Unmaximize
 | |
|       stateGlobal.setMaximized(maximize);
 | |
|     });
 | |
|   }
 | |
| }
 | |
| 
 | |
| void startDragging(bool isMainWindow) {
 | |
|   if (isMainWindow) {
 | |
|     windowManager.startDragging();
 | |
|   } else {
 | |
|     WindowController.fromWindowId(kWindowId!).startDragging();
 | |
|   }
 | |
| }
 | |
| 
 | |
| void setMovable(bool isMainWindow, bool movable) {
 | |
|   if (isMainWindow) {
 | |
|     windowManager.setMovable(movable);
 | |
|   } else {
 | |
|     WindowController.fromWindowId(kWindowId!).setMovable(movable);
 | |
|   }
 | |
| }
 | |
| 
 | |
| /// return true -> window will be maximize
 | |
| /// return false -> window will be unmaximize
 | |
| Future<bool> toggleMaximize(bool isMainWindow) async {
 | |
|   if (isMainWindow) {
 | |
|     if (await windowManager.isMaximized()) {
 | |
|       windowManager.unmaximize();
 | |
|       return false;
 | |
|     } else {
 | |
|       windowManager.maximize();
 | |
|       return true;
 | |
|     }
 | |
|   } else {
 | |
|     final wc = WindowController.fromWindowId(kWindowId!);
 | |
|     if (await wc.isMaximized()) {
 | |
|       wc.unmaximize();
 | |
|       return false;
 | |
|     } else {
 | |
|       wc.maximize();
 | |
|       return true;
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| Future<bool> closeConfirmDialog() async {
 | |
|   var confirm = true;
 | |
|   final res = await gFFI.dialogManager.show<bool>((setState, close, context) {
 | |
|     submit() {
 | |
|       String value = bool2option(kOptionEnableConfirmClosingTabs, confirm);
 | |
|       bind.mainSetLocalOption(
 | |
|           key: kOptionEnableConfirmClosingTabs, value: value);
 | |
|       close(true);
 | |
|     }
 | |
| 
 | |
|     return CustomAlertDialog(
 | |
|       title: Row(children: [
 | |
|         const Icon(Icons.warning_amber_sharp,
 | |
|             color: Colors.redAccent, size: 28),
 | |
|         const SizedBox(width: 10),
 | |
|         Text(translate("Warning")),
 | |
|       ]),
 | |
|       content: Column(
 | |
|           mainAxisAlignment: MainAxisAlignment.start,
 | |
|           crossAxisAlignment: CrossAxisAlignment.start,
 | |
|           children: [
 | |
|             Text(translate("Disconnect all devices?")),
 | |
|             CheckboxListTile(
 | |
|               contentPadding: const EdgeInsets.all(0),
 | |
|               dense: true,
 | |
|               controlAffinity: ListTileControlAffinity.leading,
 | |
|               title: Text(
 | |
|                 translate("Confirm before closing multiple tabs"),
 | |
|               ),
 | |
|               value: confirm,
 | |
|               onChanged: (v) {
 | |
|                 if (v == null) return;
 | |
|                 setState(() => confirm = v);
 | |
|               },
 | |
|             )
 | |
|           ]),
 | |
|       // confirm checkbox
 | |
|       actions: [
 | |
|         dialogButton("Cancel", onPressed: close, isOutline: true),
 | |
|         dialogButton("OK", onPressed: submit),
 | |
|       ],
 | |
|       onSubmit: submit,
 | |
|       onCancel: close,
 | |
|     );
 | |
|   });
 | |
|   return res == true;
 | |
| }
 | |
| 
 | |
| class _ListView extends StatelessWidget {
 | |
|   final DesktopTabController controller;
 | |
|   final RxList<String> invisibleTabKeys;
 | |
| 
 | |
|   final TabBuilder? tabBuilder;
 | |
|   final TabMenuBuilder? tabMenuBuilder;
 | |
|   final LabelGetter? labelGetter;
 | |
|   final double? maxLabelWidth;
 | |
|   final Color? selectedTabBackgroundColor;
 | |
|   final Color? selectedBorderColor;
 | |
|   final Color? unSelectedTabBackgroundColor;
 | |
| 
 | |
|   Rx<DesktopTabState> get state => controller.state;
 | |
| 
 | |
|   _ListView({
 | |
|     required this.controller,
 | |
|     required this.invisibleTabKeys,
 | |
|     this.tabBuilder,
 | |
|     this.tabMenuBuilder,
 | |
|     this.labelGetter,
 | |
|     this.maxLabelWidth,
 | |
|     this.selectedTabBackgroundColor,
 | |
|     this.unSelectedTabBackgroundColor,
 | |
|     this.selectedBorderColor,
 | |
|   });
 | |
| 
 | |
|   /// Check whether to show ListView
 | |
|   ///
 | |
|   /// Conditions:
 | |
|   /// - hide single item when only has one item (home) on [DesktopTabPage].
 | |
|   bool isHideSingleItem() {
 | |
|     return state.value.tabs.length == 1 &&
 | |
|             controller.tabType == DesktopTabType.main ||
 | |
|         controller.tabType == DesktopTabType.install;
 | |
|   }
 | |
| 
 | |
|   onVisibilityChanged(VisibilityInfo info) {
 | |
|     final key = (info.key as ValueKey).value;
 | |
|     if (info.visibleFraction < 0.75) {
 | |
|       if (!invisibleTabKeys.contains(key)) {
 | |
|         invisibleTabKeys.add(key);
 | |
|       }
 | |
|       invisibleTabKeys.removeWhere((key) =>
 | |
|           controller.state.value.tabs.where((e) => e.key == key).isEmpty);
 | |
|     } else {
 | |
|       invisibleTabKeys.remove(key);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return Obx(() => ListView(
 | |
|         controller: state.value.scrollController,
 | |
|         scrollDirection: Axis.horizontal,
 | |
|         shrinkWrap: true,
 | |
|         physics: const BouncingScrollPhysics(),
 | |
|         children: isHideSingleItem()
 | |
|             ? List.empty()
 | |
|             : state.value.tabs.asMap().entries.map((e) {
 | |
|                 final index = e.key;
 | |
|                 final tab = e.value;
 | |
|                 final label = labelGetter == null
 | |
|                     ? Rx<String>(tab.label)
 | |
|                     : labelGetter!(tab.label);
 | |
|                 final child = VisibilityDetector(
 | |
|                   key: ValueKey(tab.key),
 | |
|                   onVisibilityChanged: onVisibilityChanged,
 | |
|                   child: _Tab(
 | |
|                     key: ValueKey(tab.key),
 | |
|                     index: index,
 | |
|                     tabInfoKey: tab.key,
 | |
|                     label: label,
 | |
|                     tabType: controller.tabType,
 | |
|                     selectedIcon: tab.selectedIcon,
 | |
|                     unselectedIcon: tab.unselectedIcon,
 | |
|                     closable: tab.closable,
 | |
|                     selected: state.value.selected,
 | |
|                     onClose: () {
 | |
|                       if (tab.onTabCloseButton != null) {
 | |
|                         tab.onTabCloseButton!();
 | |
|                       } else {
 | |
|                         controller.remove(index);
 | |
|                       }
 | |
|                     },
 | |
|                     onTap: () {
 | |
|                       controller.jumpTo(index);
 | |
|                       tab.onTap?.call();
 | |
|                     },
 | |
|                     tabBuilder: tabBuilder,
 | |
|                     tabMenuBuilder: tabMenuBuilder,
 | |
|                     maxLabelWidth: maxLabelWidth,
 | |
|                     selectedTabBackgroundColor: selectedTabBackgroundColor ??
 | |
|                         MyTheme.tabbar(context).selectedTabBackgroundColor,
 | |
|                     unSelectedTabBackgroundColor: unSelectedTabBackgroundColor,
 | |
|                     selectedBorderColor: selectedBorderColor,
 | |
|                   ),
 | |
|                 );
 | |
|                 return GestureDetector(
 | |
|                   onPanStart: (e) {},
 | |
|                   child: child,
 | |
|                 );
 | |
|               }).toList()));
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _Tab extends StatefulWidget {
 | |
|   final int index;
 | |
|   final String tabInfoKey;
 | |
|   final Rx<String> label;
 | |
|   final DesktopTabType tabType;
 | |
|   final IconData? selectedIcon;
 | |
|   final IconData? unselectedIcon;
 | |
|   final bool closable;
 | |
|   final int selected;
 | |
|   final Function() onClose;
 | |
|   final Function() onTap;
 | |
|   final TabBuilder? tabBuilder;
 | |
|   final TabMenuBuilder? tabMenuBuilder;
 | |
|   final double? maxLabelWidth;
 | |
|   final Color? selectedTabBackgroundColor;
 | |
|   final Color? unSelectedTabBackgroundColor;
 | |
|   final Color? selectedBorderColor;
 | |
| 
 | |
|   const _Tab({
 | |
|     Key? key,
 | |
|     required this.index,
 | |
|     required this.tabInfoKey,
 | |
|     required this.label,
 | |
|     required this.tabType,
 | |
|     this.selectedIcon,
 | |
|     this.unselectedIcon,
 | |
|     this.tabBuilder,
 | |
|     this.tabMenuBuilder,
 | |
|     required this.closable,
 | |
|     required this.selected,
 | |
|     required this.onClose,
 | |
|     required this.onTap,
 | |
|     this.maxLabelWidth,
 | |
|     this.selectedTabBackgroundColor,
 | |
|     this.unSelectedTabBackgroundColor,
 | |
|     this.selectedBorderColor,
 | |
|   }) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   State<_Tab> createState() => _TabState();
 | |
| }
 | |
| 
 | |
| class _TabState extends State<_Tab> with RestorationMixin {
 | |
|   final RestorableBool restoreHover = RestorableBool(false);
 | |
| 
 | |
|   Widget _buildTabContent() {
 | |
|     bool showIcon =
 | |
|         widget.selectedIcon != null && widget.unselectedIcon != null;
 | |
|     bool isSelected = widget.index == widget.selected;
 | |
| 
 | |
|     final icon = Offstage(
 | |
|         offstage: !showIcon,
 | |
|         child: Icon(
 | |
|           isSelected ? widget.selectedIcon : widget.unselectedIcon,
 | |
|           size: _kIconSize,
 | |
|           color: isSelected
 | |
|               ? MyTheme.tabbar(context).selectedTabIconColor
 | |
|               : MyTheme.tabbar(context).unSelectedTabIconColor,
 | |
|         ).paddingOnly(right: 5));
 | |
|     final labelWidget = Obx(() {
 | |
|       return ConstrainedBox(
 | |
|           constraints: BoxConstraints(maxWidth: widget.maxLabelWidth ?? 200),
 | |
|           child: Tooltip(
 | |
|             message: widget.tabType == DesktopTabType.main
 | |
|                 ? ''
 | |
|                 : translate(widget.label.value),
 | |
|             child: Text(
 | |
|               translate(widget.label.value),
 | |
|               textAlign: TextAlign.center,
 | |
|               style: TextStyle(
 | |
|                   color: isSelected
 | |
|                       ? MyTheme.tabbar(context).selectedTextColor
 | |
|                       : MyTheme.tabbar(context).unSelectedTextColor),
 | |
|               overflow: TextOverflow.ellipsis,
 | |
|             ),
 | |
|           ));
 | |
|     });
 | |
| 
 | |
|     Widget getWidgetWithBuilder() {
 | |
|       if (widget.tabBuilder == null) {
 | |
|         return Row(
 | |
|           mainAxisAlignment: MainAxisAlignment.center,
 | |
|           children: [
 | |
|             icon,
 | |
|             labelWidget,
 | |
|           ],
 | |
|         );
 | |
|       } 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
 | |
|   Widget build(BuildContext context) {
 | |
|     bool isSelected = widget.index == widget.selected;
 | |
|     bool showDivider =
 | |
|         widget.index != widget.selected - 1 && widget.index != widget.selected;
 | |
|     RxBool hover = restoreHover.value.obs;
 | |
|     return Ink(
 | |
|       child: InkWell(
 | |
|         onHover: (value) {
 | |
|           hover.value = value;
 | |
|           restoreHover.value = value;
 | |
|         },
 | |
|         onTap: () => widget.onTap(),
 | |
|         child: Container(
 | |
|             decoration: isSelected && widget.selectedBorderColor != null
 | |
|                 ? BoxDecoration(
 | |
|                     border: Border(
 | |
|                       bottom: BorderSide(
 | |
|                         color: widget.selectedBorderColor!,
 | |
|                         width: 1,
 | |
|                       ),
 | |
|                     ),
 | |
|                   )
 | |
|                 : null,
 | |
|             child: Container(
 | |
|               color: isSelected
 | |
|                   ? widget.selectedTabBackgroundColor
 | |
|                   : widget.unSelectedTabBackgroundColor,
 | |
|               child: Row(
 | |
|                 children: [
 | |
|                   SizedBox(
 | |
|                       // _kTabBarHeight also displays normally
 | |
|                       height: _showTabBarBottomDivider(widget.tabType)
 | |
|                           ? _kTabBarHeight - 1
 | |
|                           : _kTabBarHeight,
 | |
|                       child: Row(
 | |
|                           crossAxisAlignment: CrossAxisAlignment.center,
 | |
|                           children: [
 | |
|                             _buildTabContent(),
 | |
|                             Obx((() => _CloseButton(
 | |
|                                   visible: hover.value && widget.closable,
 | |
|                                   tabSelected: isSelected,
 | |
|                                   onClose: () => widget.onClose(),
 | |
|                                 )))
 | |
|                           ])).paddingOnly(left: 10, right: 5),
 | |
|                   Offstage(
 | |
|                     offstage: !showDivider,
 | |
|                     child: VerticalDivider(
 | |
|                       width: 1,
 | |
|                       indent: _kDividerIndent,
 | |
|                       endIndent: _kDividerIndent,
 | |
|                       color: MyTheme.tabbar(context).dividerColor,
 | |
|                     ),
 | |
|                   )
 | |
|                 ],
 | |
|               ),
 | |
|             )),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   String? get restorationId => "_Tab${widget.label.value}";
 | |
| 
 | |
|   @override
 | |
|   void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
 | |
|     registerForRestoration(restoreHover, 'restoreHover');
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _CloseButton extends StatelessWidget {
 | |
|   final bool visible;
 | |
|   final bool tabSelected;
 | |
|   final Function onClose;
 | |
| 
 | |
|   const _CloseButton({
 | |
|     Key? key,
 | |
|     required this.visible,
 | |
|     required this.tabSelected,
 | |
|     required this.onClose,
 | |
|   }) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return SizedBox(
 | |
|             width: _kIconSize,
 | |
|             child: () {
 | |
|               if (visible) {
 | |
|                 return InkWell(
 | |
|                   hoverColor: MyTheme.tabbar(context).closeHoverColor,
 | |
|                   customBorder: const CircleBorder(),
 | |
|                   onTap: () => onClose(),
 | |
|                   child: Icon(
 | |
|                     Icons.close,
 | |
|                     size: _kIconSize,
 | |
|                     color: tabSelected
 | |
|                         ? MyTheme.tabbar(context).selectedIconColor
 | |
|                         : MyTheme.tabbar(context).unSelectedIconColor,
 | |
|                   ),
 | |
|                 );
 | |
|               } else {
 | |
|                 return Offstage();
 | |
|               }
 | |
|             }())
 | |
|         .paddingOnly(left: 10);
 | |
|   }
 | |
| }
 | |
| 
 | |
| class ActionIcon extends StatefulWidget {
 | |
|   final String? message;
 | |
|   final IconData icon;
 | |
|   final GestureTapCallback? onTap;
 | |
|   final GestureTapDownCallback? onTapDown;
 | |
|   final bool isClose;
 | |
|   final double iconSize;
 | |
|   final double boxSize;
 | |
| 
 | |
|   const ActionIcon(
 | |
|       {Key? key,
 | |
|       this.message,
 | |
|       required this.icon,
 | |
|       this.onTap,
 | |
|       this.onTapDown,
 | |
|       this.isClose = false,
 | |
|       this.iconSize = _kActionIconSize,
 | |
|       this.boxSize = _kTabBarHeight - 1})
 | |
|       : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   State<ActionIcon> createState() => _ActionIconState();
 | |
| }
 | |
| 
 | |
| class _ActionIconState extends State<ActionIcon> {
 | |
|   final hover = false.obs;
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return Tooltip(
 | |
|       message: widget.message != null ? translate(widget.message!) : "",
 | |
|       waitDuration: const Duration(seconds: 1),
 | |
|       child: InkWell(
 | |
|         hoverColor: widget.isClose
 | |
|             ? const Color.fromARGB(255, 196, 43, 28)
 | |
|             : MyTheme.tabbar(context).hoverColor,
 | |
|         onHover: (value) => hover.value = value,
 | |
|         onTap: widget.onTap,
 | |
|         onTapDown: widget.onTapDown,
 | |
|         child: SizedBox(
 | |
|           height: widget.boxSize,
 | |
|           width: widget.boxSize,
 | |
|           child: widget.onTap == null
 | |
|               ? Icon(
 | |
|                   widget.icon,
 | |
|                   color: Colors.grey,
 | |
|                   size: widget.iconSize,
 | |
|                 )
 | |
|               : Obx(
 | |
|                   () => Icon(
 | |
|                     widget.icon,
 | |
|                     color: hover.value && widget.isClose
 | |
|                         ? Colors.white
 | |
|                         : MyTheme.tabbar(context).unSelectedIconColor,
 | |
|                     size: widget.iconSize,
 | |
|                   ),
 | |
|                 ),
 | |
|         ),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class AddButton extends StatelessWidget {
 | |
|   const AddButton({
 | |
|     Key? key,
 | |
|   }) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return ActionIcon(
 | |
|         message: 'New Connection',
 | |
|         icon: IconFont.add,
 | |
|         onTap: () => rustDeskWinManager.call(
 | |
|             WindowType.Main, kWindowMainWindowOnTop, ""),
 | |
|         isClose: false);
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _TabDropDownButton extends StatefulWidget {
 | |
|   final DesktopTabController controller;
 | |
|   final List<String> tabkeys;
 | |
|   final LabelGetter? labelGetter;
 | |
| 
 | |
|   const _TabDropDownButton(
 | |
|       {required this.controller, required this.tabkeys, this.labelGetter});
 | |
| 
 | |
|   @override
 | |
|   State<_TabDropDownButton> createState() => _TabDropDownButtonState();
 | |
| }
 | |
| 
 | |
| class _TabDropDownButtonState extends State<_TabDropDownButton> {
 | |
|   var position = RelativeRect.fromLTRB(0, 0, 0, 0);
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     List<String> sortedKeys = widget.controller.state.value.tabs
 | |
|         .where((e) => widget.tabkeys.contains(e.key))
 | |
|         .map((e) => e.key)
 | |
|         .toList();
 | |
|     return ActionIcon(
 | |
|       onTapDown: (details) {
 | |
|         final x = details.globalPosition.dx;
 | |
|         final y = details.globalPosition.dy;
 | |
|         position = RelativeRect.fromLTRB(x, y, x, y);
 | |
|       },
 | |
|       icon: Icons.arrow_drop_down,
 | |
|       onTap: () {
 | |
|         showMenu(
 | |
|           context: context,
 | |
|           position: position,
 | |
|           items: sortedKeys.map((e) {
 | |
|             var label = e;
 | |
|             final tabInfo = widget.controller.state.value.tabs
 | |
|                 .firstWhereOrNull((element) => element.key == e);
 | |
|             if (tabInfo != null) {
 | |
|               label = tabInfo.label;
 | |
|             }
 | |
|             if (widget.labelGetter != null) {
 | |
|               label = widget.labelGetter!(e).value;
 | |
|             }
 | |
|             var index = widget.controller.state.value.tabs
 | |
|                 .indexWhere((t) => t.key == e);
 | |
|             label = '${index + 1}. $label';
 | |
|             final menuHover = false.obs;
 | |
|             final btnHover = false.obs;
 | |
|             return PopupMenuItem<String>(
 | |
|               value: e,
 | |
|               height: 32,
 | |
|               onTap: () {
 | |
|                 widget.controller.jumpToByKey(e);
 | |
|                 if (Navigator.of(context).canPop()) {
 | |
|                   Navigator.of(context).pop();
 | |
|                 }
 | |
|               },
 | |
|               child: MouseRegion(
 | |
|                 onHover: (event) => setState(() => menuHover.value = true),
 | |
|                 onExit: (event) => setState(() => menuHover.value = false),
 | |
|                 child: Row(
 | |
|                   children: [
 | |
|                     Expanded(
 | |
|                       child: InkWell(child: Text(label)),
 | |
|                     ),
 | |
|                     Obx(
 | |
|                       () {
 | |
|                         if (tabInfo?.onTabCloseButton != null &&
 | |
|                             menuHover.value) {
 | |
|                           return InkWell(
 | |
|                               onTap: () {
 | |
|                                 tabInfo?.onTabCloseButton?.call();
 | |
|                                 if (Navigator.of(context).canPop()) {
 | |
|                                   Navigator.of(context).pop();
 | |
|                                 }
 | |
|                               },
 | |
|                               child: MouseRegion(
 | |
|                                   cursor: SystemMouseCursors.click,
 | |
|                                   onHover: (event) =>
 | |
|                                       setState(() => btnHover.value = true),
 | |
|                                   onExit: (event) =>
 | |
|                                       setState(() => btnHover.value = false),
 | |
|                                   child: Icon(Icons.close,
 | |
|                                       color:
 | |
|                                           btnHover.value ? Colors.red : null)));
 | |
|                         } else {
 | |
|                           return Offstage();
 | |
|                         }
 | |
|                       },
 | |
|                     ),
 | |
|                   ],
 | |
|                 ),
 | |
|               ),
 | |
|             );
 | |
|           }).toList(),
 | |
|         );
 | |
|       },
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| bool _showTabBarBottomDivider(DesktopTabType tabType) {
 | |
|   return tabType == DesktopTabType.main || tabType == DesktopTabType.install;
 | |
| }
 | |
| 
 | |
| class TabbarTheme extends ThemeExtension<TabbarTheme> {
 | |
|   final Color? selectedTabIconColor;
 | |
|   final Color? unSelectedTabIconColor;
 | |
|   final Color? selectedTextColor;
 | |
|   final Color? unSelectedTextColor;
 | |
|   final Color? selectedIconColor;
 | |
|   final Color? unSelectedIconColor;
 | |
|   final Color? dividerColor;
 | |
|   final Color? hoverColor;
 | |
|   final Color? closeHoverColor;
 | |
|   final Color? selectedTabBackgroundColor;
 | |
| 
 | |
|   const TabbarTheme(
 | |
|       {required this.selectedTabIconColor,
 | |
|       required this.unSelectedTabIconColor,
 | |
|       required this.selectedTextColor,
 | |
|       required this.unSelectedTextColor,
 | |
|       required this.selectedIconColor,
 | |
|       required this.unSelectedIconColor,
 | |
|       required this.dividerColor,
 | |
|       required this.hoverColor,
 | |
|       required this.closeHoverColor,
 | |
|       required this.selectedTabBackgroundColor});
 | |
| 
 | |
|   static const light = TabbarTheme(
 | |
|       selectedTabIconColor: MyTheme.accent,
 | |
|       unSelectedTabIconColor: Color.fromARGB(255, 162, 203, 241),
 | |
|       selectedTextColor: Colors.black,
 | |
|       unSelectedTextColor: Color.fromARGB(255, 112, 112, 112),
 | |
|       selectedIconColor: Color.fromARGB(255, 26, 26, 26),
 | |
|       unSelectedIconColor: Color.fromARGB(255, 96, 96, 96),
 | |
|       dividerColor: Color.fromARGB(255, 238, 238, 238),
 | |
|       hoverColor: Colors.white54,
 | |
|       closeHoverColor: Colors.white,
 | |
|       selectedTabBackgroundColor: Colors.white54);
 | |
| 
 | |
|   static const dark = TabbarTheme(
 | |
|       selectedTabIconColor: MyTheme.accent,
 | |
|       unSelectedTabIconColor: Color.fromARGB(255, 30, 65, 98),
 | |
|       selectedTextColor: Colors.white,
 | |
|       unSelectedTextColor: Color.fromARGB(255, 192, 192, 192),
 | |
|       selectedIconColor: Color.fromARGB(255, 192, 192, 192),
 | |
|       unSelectedIconColor: Color.fromARGB(255, 255, 255, 255),
 | |
|       dividerColor: Color.fromARGB(255, 64, 64, 64),
 | |
|       hoverColor: Colors.black26,
 | |
|       closeHoverColor: Colors.black,
 | |
|       selectedTabBackgroundColor: Colors.black26);
 | |
| 
 | |
|   @override
 | |
|   ThemeExtension<TabbarTheme> copyWith({
 | |
|     Color? selectedTabIconColor,
 | |
|     Color? unSelectedTabIconColor,
 | |
|     Color? selectedTextColor,
 | |
|     Color? unSelectedTextColor,
 | |
|     Color? selectedIconColor,
 | |
|     Color? unSelectedIconColor,
 | |
|     Color? dividerColor,
 | |
|     Color? hoverColor,
 | |
|     Color? closeHoverColor,
 | |
|     Color? selectedTabBackgroundColor,
 | |
|   }) {
 | |
|     return TabbarTheme(
 | |
|       selectedTabIconColor: selectedTabIconColor ?? this.selectedTabIconColor,
 | |
|       unSelectedTabIconColor:
 | |
|           unSelectedTabIconColor ?? this.unSelectedTabIconColor,
 | |
|       selectedTextColor: selectedTextColor ?? this.selectedTextColor,
 | |
|       unSelectedTextColor: unSelectedTextColor ?? this.unSelectedTextColor,
 | |
|       selectedIconColor: selectedIconColor ?? this.selectedIconColor,
 | |
|       unSelectedIconColor: unSelectedIconColor ?? this.unSelectedIconColor,
 | |
|       dividerColor: dividerColor ?? this.dividerColor,
 | |
|       hoverColor: hoverColor ?? this.hoverColor,
 | |
|       closeHoverColor: closeHoverColor ?? this.closeHoverColor,
 | |
|       selectedTabBackgroundColor:
 | |
|           selectedTabBackgroundColor ?? this.selectedTabBackgroundColor,
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   ThemeExtension<TabbarTheme> lerp(
 | |
|       ThemeExtension<TabbarTheme>? other, double t) {
 | |
|     if (other is! TabbarTheme) {
 | |
|       return this;
 | |
|     }
 | |
|     return TabbarTheme(
 | |
|       selectedTabIconColor:
 | |
|           Color.lerp(selectedTabIconColor, other.selectedTabIconColor, t),
 | |
|       unSelectedTabIconColor:
 | |
|           Color.lerp(unSelectedTabIconColor, other.unSelectedTabIconColor, t),
 | |
|       selectedTextColor:
 | |
|           Color.lerp(selectedTextColor, other.selectedTextColor, t),
 | |
|       unSelectedTextColor:
 | |
|           Color.lerp(unSelectedTextColor, other.unSelectedTextColor, t),
 | |
|       selectedIconColor:
 | |
|           Color.lerp(selectedIconColor, other.selectedIconColor, t),
 | |
|       unSelectedIconColor:
 | |
|           Color.lerp(unSelectedIconColor, other.unSelectedIconColor, t),
 | |
|       dividerColor: Color.lerp(dividerColor, other.dividerColor, t),
 | |
|       hoverColor: Color.lerp(hoverColor, other.hoverColor, t),
 | |
|       closeHoverColor: Color.lerp(closeHoverColor, other.closeHoverColor, t),
 | |
|       selectedTabBackgroundColor: Color.lerp(
 | |
|           selectedTabBackgroundColor, other.selectedTabBackgroundColor, t),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   static color(BuildContext context) {
 | |
|     return Theme.of(context).extension<ColorThemeExtension>()!;
 | |
|   }
 | |
| }
 |