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:
parent
937cea5a01
commit
f6223a6f71
@ -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,
|
||||
)),
|
||||
);
|
||||
|
@ -108,7 +108,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||
tabController.clear();
|
||||
return true;
|
||||
},
|
||||
tail: AddButton().paddingOnly(left: 10),
|
||||
tail: AddButton(),
|
||||
labelGetter: DesktopTab.tablabelGetter,
|
||||
)),
|
||||
);
|
||||
|
@ -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(() {
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user