dropdown menu for tabs that cannot be displayed (#7918)

* The visibility threshold is 75%
* Used in remote, file transfer, port forward and cm

Signed-off-by: 21pages <pages21@163.com>
This commit is contained in:
21pages 2024-05-06 17:10:02 +08:00 committed by GitHub
parent 937cea5a01
commit f6223a6f71
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 190 additions and 36 deletions

View File

@ -99,7 +99,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
body: DesktopTab(
controller: tabController,
onWindowCloseButton: handleWindowCloseButton,
tail: const AddButton().paddingOnly(left: 10),
tail: const AddButton(),
labelGetter: DesktopTab.tablabelGetter,
)),
);

View File

@ -108,7 +108,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
tabController.clear();
return true;
},
tail: AddButton().paddingOnly(left: 10),
tail: AddButton(),
labelGetter: DesktopTab.tablabelGetter,
)),
);

View File

@ -143,7 +143,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
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(() {

View File

@ -157,7 +157,7 @@ class ConnectionManagerState extends State<ConnectionManager> {
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) {

View File

@ -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<String> 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<bool> Function()? onClose;
final RxList<String> 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<WindowActionPanel>
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<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(() => 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<bool> closeConfirmDialog() async {
class _ListView extends StatelessWidget {
final DesktopTabController controller;
final RxList<String> invisibleTabKeys;
final TabBuilder? tabBuilder;
final TabMenuBuilder? tabMenuBuilder;
@ -825,8 +860,9 @@ class _ListView extends StatelessWidget {
Rx<DesktopTabState> 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<String>(tab.label)
: labelGetter!(tab.label);
return VisibilityDetector(
key: ValueKey(tab.key),
index: index,
tabInfoKey: tab.key,
label: labelGetter == null
? Rx<String>(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<ActionIcon> {
: 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<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(
() => 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<TabbarTheme> {
final Color? selectedTabIconColor;
final Color? unSelectedTabIconColor;