diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 054ef91be..be3ba2497 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -99,7 +99,7 @@ class _FileManagerTabPageState extends State { body: DesktopTab( controller: tabController, onWindowCloseButton: handleWindowCloseButton, - tail: const AddButton().paddingOnly(left: 10), + tail: const AddButton(), labelGetter: DesktopTab.tablabelGetter, )), ); diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index ce8bbd284..edd995adc 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -108,7 +108,7 @@ class _PortForwardTabPageState extends State { tabController.clear(); return true; }, - tail: AddButton().paddingOnly(left: 10), + tail: AddButton(), labelGetter: DesktopTab.tablabelGetter, )), ); diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 8f6a96f47..9d7d0c221 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -143,7 +143,7 @@ class _ConnectionTabPageState extends State { body: DesktopTab( controller: tabController, onWindowCloseButton: handleWindowCloseButton, - tail: const AddButton().paddingOnly(left: 10), + tail: const AddButton(), pageViewBuilder: (pageView) => pageView, labelGetter: DesktopTab.tablabelGetter, tabBuilder: (key, icon, label, themeConf) => Obx(() { diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 26bef1f10..961a42477 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -157,7 +157,7 @@ class ConnectionManagerState extends State { controller: serverModel.tabController, selectedBorderColor: MyTheme.accent, maxLabelWidth: 100, - tail: buildScrollJumper(), + tail: null, //buildScrollJumper(), selectedTabBackgroundColor: Theme.of(context).hintColor.withOpacity(0), tabBuilder: (key, icon, label, themeConf) { diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index fb13efbef..0f13ce2d9 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -16,6 +16,7 @@ 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'; @@ -256,6 +257,8 @@ class DesktopTab extends StatelessWidget { late final DesktopTabType tabType; late final bool isMainWindow; + final RxList invisibleTabKeys = RxList.empty(); + DesktopTab({ Key? key, required this.controller, @@ -430,6 +433,7 @@ class DesktopTab extends StatelessWidget { }, child: _ListView( controller: controller, + invisibleTabKeys: invisibleTabKeys, tabBuilder: tabBuilder, tabMenuBuilder: tabMenuBuilder, labelGetter: labelGetter, @@ -448,12 +452,14 @@ class DesktopTab extends StatelessWidget { tabType: tabType, state: state, tabController: controller, + invisibleTabKeys: invisibleTabKeys, tail: tail, showMinimize: showMinimize, showMaximize: showMaximize, showClose: showClose, onClose: onWindowCloseButton, - ) + labelGetter: labelGetter, + ).paddingOnly(left: 10) ], ); } @@ -471,17 +477,22 @@ class WindowActionPanel extends StatefulWidget { final Widget? tail; final Future Function()? onClose; + final RxList invisibleTabKeys; + final LabelGetter? labelGetter; + const WindowActionPanel( {Key? key, required this.isMainWindow, required this.tabType, required this.state, required this.tabController, + required this.invisibleTabKeys, this.tail, this.showMinimize = true, this.showMaximize = true, this.showClose = true, - this.onClose}) + this.onClose, + this.labelGetter}) : super(key: key); @override @@ -658,11 +669,34 @@ class WindowActionPanelState extends State super.onWindowClose(); } + 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 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(() => Offstage( + offstage: + !(showTabDowndown() && existingInvisibleTab().isNotEmpty), + child: _TabDropDownButton( + controller: widget.tabController, + labelGetter: widget.labelGetter, + tabkeys: existingInvisibleTab()), + )), Offstage(offstage: widget.tail == null, child: widget.tail), Offstage( offstage: kUseCompatibleUiMode, @@ -814,6 +848,7 @@ Future closeConfirmDialog() async { class _ListView extends StatelessWidget { final DesktopTabController controller; + final RxList invisibleTabKeys; final TabBuilder? tabBuilder; final TabMenuBuilder? tabMenuBuilder; @@ -825,8 +860,9 @@ class _ListView extends StatelessWidget { Rx get state => controller.state; - const _ListView({ + _ListView({ required this.controller, + required this.invisibleTabKeys, this.tabBuilder, this.tabMenuBuilder, this.labelGetter, @@ -846,6 +882,19 @@ class _ListView extends StatelessWidget { 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( @@ -858,36 +907,41 @@ class _ListView extends StatelessWidget { : state.value.tabs.asMap().entries.map((e) { final index = e.key; final tab = e.value; - return _Tab( + final label = labelGetter == null + ? Rx(tab.label) + : labelGetter!(tab.label); + return VisibilityDetector( key: ValueKey(tab.key), - index: index, - tabInfoKey: tab.key, - label: labelGetter == null - ? Rx(tab.label) - : labelGetter!(tab.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, + 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, + ), ); }).toList())); } @@ -1115,6 +1169,7 @@ 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; @@ -1124,6 +1179,7 @@ class ActionIcon extends StatefulWidget { this.message, required this.icon, this.onTap, + this.onTapDown, this.isClose = false, this.iconSize = _kActionIconSize, this.boxSize = _kTabBarHeight - 1}) @@ -1153,6 +1209,7 @@ class _ActionIconState extends State { : MyTheme.tabbar(context).hoverColor, onHover: (value) => hover.value = value, onTap: widget.onTap, + onTapDown: widget.onTapDown, child: SizedBox( height: widget.boxSize, width: widget.boxSize, @@ -1193,6 +1250,103 @@ class AddButton extends StatelessWidget { } } +class _TabDropDownButton extends StatefulWidget { + final DesktopTabController controller; + final List 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 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( + 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( + () => Offstage( + offstage: !(tabInfo?.onTabCloseButton != null && + menuHover.value), + child: 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))), + ), + ) + ], + ), + ), + ); + }).toList(), + ); + }, + ); + } +} + class TabbarTheme extends ThemeExtension { final Color? selectedTabIconColor; final Color? unSelectedTabIconColor;