mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-04-12 04:51:28 +03:00
flutter_desktop: password menu
Signed-off-by: fufesou <shuanglongchen@yeah.net>
This commit is contained in:
@@ -19,18 +19,18 @@ import 'package:tray_manager/tray_manager.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
class _PopupMenuTheme {
|
||||
class _MenubarTheme {
|
||||
static const Color commonColor = MyTheme.accent;
|
||||
// kMinInteractiveDimension
|
||||
static const double height = 25.0;
|
||||
static const double dividerHeight = 3.0;
|
||||
static const double dividerHeight = 12.0;
|
||||
}
|
||||
|
||||
class DesktopHomePage extends StatefulWidget {
|
||||
const DesktopHomePage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _DesktopHomePageState();
|
||||
State<DesktopHomePage> createState() => _DesktopHomePageState();
|
||||
}
|
||||
|
||||
const borderColor = Color(0xFF2F65BA);
|
||||
@@ -93,7 +93,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
buildServerBoard(BuildContext context) {
|
||||
return Container(
|
||||
color: MyTheme.color(context).grayBg,
|
||||
child: const ConnectionPage(),
|
||||
child: ConnectionPage(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
Container(
|
||||
height: 25,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
@@ -142,11 +142,11 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
child: TextFormField(
|
||||
controller: model.serverId,
|
||||
readOnly: true,
|
||||
decoration: const InputDecoration(
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.only(bottom: 20),
|
||||
),
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
),
|
||||
),
|
||||
@@ -161,190 +161,8 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
);
|
||||
}
|
||||
|
||||
// Future<MenuEntryBase<String>> _genSwitchEntry(
|
||||
// String label, String key) async {
|
||||
|
||||
// final v = await bind.mainGetOption(key: key);
|
||||
// bool enable;
|
||||
// if (key == "stop-service") {
|
||||
// enable = v != "Y";
|
||||
// } else if (key.startsWith("allow-")) {
|
||||
// enable = v == "Y";
|
||||
// } else {
|
||||
// enable = v != "N";
|
||||
// }
|
||||
|
||||
// return PopupMenuItem(
|
||||
// child: Row(
|
||||
// children: [
|
||||
// Icon(Icons.check,
|
||||
// color: enable ? null : MyTheme.accent.withAlpha(00)),
|
||||
// Text(
|
||||
// label,
|
||||
// style: genTextStyle(enable),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// value: key,
|
||||
// );
|
||||
// }
|
||||
|
||||
_popupMenu(BuildContext context, RelativeRect position) async {
|
||||
TextStyle styleEnabled = const TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal);
|
||||
TextStyle styleDisabled = const TextStyle(
|
||||
color: Colors.redAccent,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal,
|
||||
decoration: TextDecoration.lineThrough);
|
||||
|
||||
enabledEntry(String label, String key) {
|
||||
Rx<TextStyle> textStyle = styleEnabled.obs;
|
||||
return MenuEntrySwitch<String>(
|
||||
text: translate(label),
|
||||
textStyle: textStyle,
|
||||
getter: () async {
|
||||
final opt = await bind.mainGetOption(key: key);
|
||||
bool enabled;
|
||||
if (key == 'stop-service') {
|
||||
enabled = opt != 'Y';
|
||||
} else if (key.startsWith("allow-")) {
|
||||
enabled = opt == 'Y';
|
||||
} else {
|
||||
enabled = opt != 'N';
|
||||
}
|
||||
textStyle.value = enabled ? styleEnabled : styleDisabled;
|
||||
return enabled;
|
||||
},
|
||||
setter: (bool v) async {
|
||||
String opt;
|
||||
if (key == 'stop-service') {
|
||||
opt = v ? 'Y' : '';
|
||||
} else if (key.startsWith("allow-")) {
|
||||
opt = v ? 'Y' : '';
|
||||
} else {
|
||||
opt = v ? '' : 'N';
|
||||
}
|
||||
await bind.mainSetOption(key: key, value: opt);
|
||||
if (key == 'allow-darktheme') {
|
||||
changeTheme(opt);
|
||||
}
|
||||
},
|
||||
dismissOnClicked: false,
|
||||
);
|
||||
}
|
||||
|
||||
final userName = await gFFI.userModel.getUserName();
|
||||
final enabledInput = await bind.mainGetOption(key: 'enable-audio');
|
||||
final defaultInput = await gFFI.getDefaultAudioInput();
|
||||
|
||||
final List<MenuEntryBase<String>> menu = <MenuEntryBase<String>>[
|
||||
enabledEntry('Enable Keyboard/Mouse', 'enable-keyboard'),
|
||||
enabledEntry('Enable Clipboard', 'enable-clipboard'),
|
||||
enabledEntry('Enable File Transfer', 'enable-file-transfer'),
|
||||
enabledEntry('Enable TCP Tunneling', 'enable-tunnel'),
|
||||
// TODO: audio sub menu?
|
||||
// genAudioInputPopupMenuItem(enabledInput != "N", defaultInput),
|
||||
MenuEntryDivider(),
|
||||
MenuEntryButton(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
translate('ID/Relay Server'),
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
changeServer();
|
||||
},
|
||||
dismissOnClicked: true,
|
||||
),
|
||||
MenuEntryButton(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
translate('IP Whitelisting'),
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
changeWhiteList();
|
||||
},
|
||||
dismissOnClicked: true,
|
||||
),
|
||||
MenuEntryButton(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
translate('Socks5 Proxy'),
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
changeSocks5Proxy();
|
||||
},
|
||||
dismissOnClicked: true,
|
||||
),
|
||||
MenuEntryDivider(),
|
||||
enabledEntry('Enable Service', 'stop-service'),
|
||||
enabledEntry('Always connected via relay', 'allow-always-relay'),
|
||||
// FIXME: is this option correct?
|
||||
enabledEntry('Start ID/relay service', 'stop-rendezvous-service'),
|
||||
MenuEntryDivider(),
|
||||
userName.isEmpty
|
||||
? MenuEntryButton(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
translate('Login'),
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
login();
|
||||
},
|
||||
dismissOnClicked: true,
|
||||
)
|
||||
: MenuEntryButton(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
translate('Logout'),
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
logOut();
|
||||
},
|
||||
dismissOnClicked: true,
|
||||
),
|
||||
MenuEntryButton(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
translate('Change ID'),
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
changeId();
|
||||
},
|
||||
dismissOnClicked: true,
|
||||
),
|
||||
MenuEntryDivider(),
|
||||
enabledEntry('Dark Theme', 'allow-darktheme'),
|
||||
MenuEntryButton(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
translate('About'),
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
about();
|
||||
},
|
||||
dismissOnClicked: true,
|
||||
),
|
||||
];
|
||||
|
||||
await mod_menu.showMenu(
|
||||
context: context,
|
||||
position: position,
|
||||
items: menu
|
||||
.map((e) => e.build(
|
||||
context,
|
||||
const MenuConfig(
|
||||
commonColor: _PopupMenuTheme.commonColor,
|
||||
height: _PopupMenuTheme.height,
|
||||
dividerHeight: _PopupMenuTheme.dividerHeight)))
|
||||
.expand((i) => i)
|
||||
.toList());
|
||||
}
|
||||
|
||||
Widget buildPopupMenu(BuildContext context) {
|
||||
RelativeRect position = const RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0);
|
||||
var position;
|
||||
RxBool hover = false.obs;
|
||||
return InkWell(
|
||||
onTapDown: (detail) {
|
||||
@@ -353,7 +171,83 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
position = RelativeRect.fromLTRB(x, y, x, y);
|
||||
},
|
||||
onTap: () async {
|
||||
await _popupMenu(context, position);
|
||||
final userName = await gFFI.userModel.getUserName();
|
||||
final enabledInput = await bind.mainGetOption(key: 'enable-audio');
|
||||
final defaultInput = await gFFI.getDefaultAudioInput();
|
||||
var menu = <PopupMenuEntry>[
|
||||
await genEnablePopupMenuItem(
|
||||
translate("Enable Keyboard/Mouse"),
|
||||
'enable-keyboard',
|
||||
),
|
||||
await genEnablePopupMenuItem(
|
||||
translate("Enable Clipboard"),
|
||||
'enable-clipboard',
|
||||
),
|
||||
await genEnablePopupMenuItem(
|
||||
translate("Enable File Transfer"),
|
||||
'enable-file-transfer',
|
||||
),
|
||||
await genEnablePopupMenuItem(
|
||||
translate("Enable TCP Tunneling"),
|
||||
'enable-tunnel',
|
||||
),
|
||||
genAudioInputPopupMenuItem(enabledInput != "N", defaultInput),
|
||||
PopupMenuDivider(),
|
||||
PopupMenuItem(
|
||||
child: Text(translate("ID/Relay Server")),
|
||||
value: 'custom-server',
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Text(translate("IP Whitelisting")),
|
||||
value: 'whitelist',
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Text(translate("Socks5 Proxy")),
|
||||
value: 'socks5-proxy',
|
||||
),
|
||||
PopupMenuDivider(),
|
||||
await genEnablePopupMenuItem(
|
||||
translate("Enable Service"),
|
||||
'stop-service',
|
||||
),
|
||||
// TODO: direct server
|
||||
await genEnablePopupMenuItem(
|
||||
translate("Always connected via relay"),
|
||||
'allow-always-relay',
|
||||
),
|
||||
await genEnablePopupMenuItem(
|
||||
translate("Start ID/relay service"),
|
||||
'stop-rendezvous-service',
|
||||
),
|
||||
PopupMenuDivider(),
|
||||
userName.isEmpty
|
||||
? PopupMenuItem(
|
||||
child: Text(translate("Login")),
|
||||
value: 'login',
|
||||
)
|
||||
: PopupMenuItem(
|
||||
child: Text("${translate("Logout")} $userName"),
|
||||
value: 'logout',
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Text(translate("Change ID")),
|
||||
value: 'change-id',
|
||||
),
|
||||
PopupMenuDivider(),
|
||||
await genEnablePopupMenuItem(
|
||||
translate("Dark Theme"),
|
||||
'allow-darktheme',
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Text(translate("About")),
|
||||
value: 'about',
|
||||
),
|
||||
];
|
||||
final v =
|
||||
await showMenu(context: context, position: position, items: menu);
|
||||
if (v != null) {
|
||||
onSelectMenu(v);
|
||||
}
|
||||
},
|
||||
child: Obx(
|
||||
() => CircleAvatar(
|
||||
@@ -435,18 +329,19 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
onTap: () => bind.mainUpdateTemporaryPassword(),
|
||||
onHover: (value) => refreshHover.value = value,
|
||||
),
|
||||
FutureBuilder<Widget>(
|
||||
future: buildPasswordPopupMenu(context),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
print("${snapshot.error}");
|
||||
}
|
||||
if (snapshot.hasData) {
|
||||
return snapshot.data!;
|
||||
} else {
|
||||
return Offstage();
|
||||
}
|
||||
})
|
||||
const _PasswordPopupMenu(),
|
||||
// FutureBuilder<Widget>(
|
||||
// future: buildPasswordPopupMenu(context),
|
||||
// builder: (context, snapshot) {
|
||||
// if (snapshot.hasError) {
|
||||
// print("${snapshot.error}");
|
||||
// }
|
||||
// if (snapshot.hasData) {
|
||||
// return snapshot.data!;
|
||||
// } else {
|
||||
// return Offstage();
|
||||
// }
|
||||
// })
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -479,7 +374,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () => gFFI.serverModel.verificationMethod = value,
|
||||
onTap: () => gFFI.serverModel.setVerificationMethod(value),
|
||||
);
|
||||
final temporary_enabled =
|
||||
gFFI.serverModel.verificationMethod != kUsePermanentPassword;
|
||||
@@ -516,8 +411,11 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
onTap: () {
|
||||
if (gFFI.serverModel.temporaryPasswordLength !=
|
||||
e) {
|
||||
gFFI.serverModel.temporaryPasswordLength = e;
|
||||
bind.mainUpdateTemporaryPassword();
|
||||
() async {
|
||||
await gFFI.serverModel
|
||||
.setTemporaryPasswordLength(e);
|
||||
await bind.mainUpdateTemporaryPassword();
|
||||
}();
|
||||
}
|
||||
},
|
||||
))
|
||||
@@ -1148,3 +1046,120 @@ void setPasswordDialog() async {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
class _PasswordPopupMenu extends StatefulWidget {
|
||||
const _PasswordPopupMenu({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_PasswordPopupMenu> createState() => _PasswordPopupMenuState();
|
||||
}
|
||||
|
||||
class _PasswordPopupMenuState extends State<_PasswordPopupMenu> {
|
||||
final RxBool _tempEnabled = true.obs;
|
||||
final RxBool _permEnabled = true.obs;
|
||||
|
||||
List<MenuEntryBase<String>> _buildMenus() {
|
||||
return <MenuEntryBase<String>>[
|
||||
MenuEntryRadios<String>(
|
||||
text: translate('Password type'),
|
||||
optionsGetter: () => [
|
||||
MenuEntryRadioOption(
|
||||
text: translate('Use temporary password'),
|
||||
value: kUseTemporaryPassword),
|
||||
MenuEntryRadioOption(
|
||||
text: translate('Use permanent password'),
|
||||
value: kUsePermanentPassword),
|
||||
MenuEntryRadioOption(
|
||||
text: translate('Use both passwords'),
|
||||
value: kUseBothPasswords),
|
||||
],
|
||||
curOptionGetter: () async {
|
||||
return gFFI.serverModel.verificationMethod;
|
||||
},
|
||||
optionSetter: (String oldValue, String newValue) async {
|
||||
await bind.mainSetOption(
|
||||
key: "verification-method", value: newValue);
|
||||
await gFFI.serverModel.updatePasswordModel();
|
||||
setState(() {
|
||||
_tempEnabled.value =
|
||||
gFFI.serverModel.verificationMethod != kUsePermanentPassword;
|
||||
_permEnabled.value =
|
||||
gFFI.serverModel.verificationMethod != kUseTemporaryPassword;
|
||||
});
|
||||
}),
|
||||
MenuEntryDivider(),
|
||||
MenuEntryButton<String>(
|
||||
enabled: _permEnabled,
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
translate('Set permanent password'),
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
setPasswordDialog();
|
||||
},
|
||||
dismissOnClicked: true,
|
||||
),
|
||||
MenuEntrySubMenu(
|
||||
enabled: _tempEnabled,
|
||||
text: translate('Set temporary password length'),
|
||||
entries: [
|
||||
MenuEntryRadios<String>(
|
||||
enabled: _tempEnabled,
|
||||
text: translate(''),
|
||||
optionsGetter: () => [
|
||||
MenuEntryRadioOption(
|
||||
text: translate('6'),
|
||||
value: '6',
|
||||
enabled: _tempEnabled,
|
||||
),
|
||||
MenuEntryRadioOption(
|
||||
text: translate('8'),
|
||||
value: '8',
|
||||
enabled: _tempEnabled,
|
||||
),
|
||||
MenuEntryRadioOption(
|
||||
text: translate('10'),
|
||||
value: '10',
|
||||
enabled: _tempEnabled,
|
||||
),
|
||||
],
|
||||
curOptionGetter: () async {
|
||||
return gFFI.serverModel.temporaryPasswordLength;
|
||||
},
|
||||
optionSetter: (String oldValue, String newValue) async {
|
||||
if (oldValue != newValue) {
|
||||
await gFFI.serverModel.setTemporaryPasswordLength(newValue);
|
||||
await gFFI.serverModel.updatePasswordModel();
|
||||
}
|
||||
}),
|
||||
])
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final editHover = false.obs;
|
||||
return mod_menu.PopupMenuButton(
|
||||
padding: EdgeInsets.zero,
|
||||
onHover: (v) => editHover.value = v,
|
||||
tooltip: translate(''),
|
||||
position: mod_menu.PopupMenuPosition.overSide,
|
||||
itemBuilder: (BuildContext context) => _buildMenus()
|
||||
.map((entry) => entry.build(
|
||||
context,
|
||||
const MenuConfig(
|
||||
commonColor: _MenubarTheme.commonColor,
|
||||
height: _MenubarTheme.height,
|
||||
dividerHeight: _MenubarTheme.dividerHeight,
|
||||
)))
|
||||
.expand((i) => i)
|
||||
.toList(),
|
||||
child: Obx(() => Icon(Icons.edit,
|
||||
size: 22,
|
||||
color: editHover.value
|
||||
? MyTheme.color(context).text
|
||||
: const Color(0xFFDDDDDD))
|
||||
.marginOnly(bottom: 2)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,8 +313,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
translate("Use permanent password"),
|
||||
translate("Use both passwords"),
|
||||
];
|
||||
bool tmp_enabled = model.verificationMethod != kUsePermanentPassword;
|
||||
bool perm_enabled = model.verificationMethod != kUseTemporaryPassword;
|
||||
bool tmpEnabled = model.verificationMethod != kUsePermanentPassword;
|
||||
bool permEnabled = model.verificationMethod != kUseTemporaryPassword;
|
||||
String currentValue = values[keys.indexOf(model.verificationMethod)];
|
||||
List<Widget> radios = values
|
||||
.map((value) => _Radio<String>(
|
||||
@@ -323,16 +323,24 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
groupValue: currentValue,
|
||||
label: value,
|
||||
onChanged: ((value) {
|
||||
model.verificationMethod = keys[values.indexOf(value)];
|
||||
() async {
|
||||
await model
|
||||
.setVerificationMethod(keys[values.indexOf(value)]);
|
||||
await model.updatePasswordModel();
|
||||
}();
|
||||
}),
|
||||
enabled: !locked,
|
||||
))
|
||||
.toList();
|
||||
|
||||
var onChanged = tmp_enabled && !locked
|
||||
var onChanged = tmpEnabled && !locked
|
||||
? (value) {
|
||||
if (value != null)
|
||||
model.temporaryPasswordLength = value.toString();
|
||||
if (value != null) {
|
||||
() async {
|
||||
await model.setTemporaryPasswordLength(value.toString());
|
||||
await model.updatePasswordModel();
|
||||
}();
|
||||
}
|
||||
}
|
||||
: null;
|
||||
List<Widget> lengthRadios = ['6', '8', '10']
|
||||
@@ -364,10 +372,10 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
...lengthRadios,
|
||||
],
|
||||
),
|
||||
enabled: tmp_enabled && !locked),
|
||||
enabled: tmpEnabled && !locked),
|
||||
radios[1],
|
||||
_SubButton('Set permanent password', setPasswordDialog,
|
||||
perm_enabled && !locked),
|
||||
permEnabled && !locked),
|
||||
radios[2],
|
||||
]);
|
||||
})));
|
||||
|
||||
@@ -42,6 +42,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
String _value = '';
|
||||
final _cursorOverImage = false.obs;
|
||||
late RxBool _showRemoteCursor;
|
||||
late RxBool _remoteCursorMoved;
|
||||
late RxBool _keyboardEnabled;
|
||||
|
||||
final FocusNode _mobileFocusNode = FocusNode();
|
||||
@@ -61,8 +62,10 @@ class _RemotePageState extends State<RemotePage>
|
||||
CurrentDisplayState.init(id);
|
||||
KeyboardEnabledState.init(id);
|
||||
ShowRemoteCursorState.init(id);
|
||||
RemoteCursorMovedState.init(id);
|
||||
_showRemoteCursor = ShowRemoteCursorState.find(id);
|
||||
_keyboardEnabled = KeyboardEnabledState.find(id);
|
||||
_remoteCursorMoved = RemoteCursorMovedState.find(id);
|
||||
}
|
||||
|
||||
void _removeStates(String id) {
|
||||
@@ -71,6 +74,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
CurrentDisplayState.delete(id);
|
||||
ShowRemoteCursorState.delete(id);
|
||||
KeyboardEnabledState.delete(id);
|
||||
RemoteCursorMovedState.delete(id);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -396,6 +400,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
id: widget.id,
|
||||
cursorOverImage: _cursorOverImage,
|
||||
keyboardEnabled: _keyboardEnabled,
|
||||
remoteCursorMoved: _remoteCursorMoved,
|
||||
listenerBuilder: _buildImageListener,
|
||||
);
|
||||
}))
|
||||
@@ -460,6 +465,7 @@ class ImagePaint extends StatelessWidget {
|
||||
final String id;
|
||||
final Rx<bool> cursorOverImage;
|
||||
final Rx<bool> keyboardEnabled;
|
||||
final Rx<bool> remoteCursorMoved;
|
||||
final Widget Function(Widget)? listenerBuilder;
|
||||
final ScrollController _horizontal = ScrollController();
|
||||
final ScrollController _vertical = ScrollController();
|
||||
@@ -469,6 +475,7 @@ class ImagePaint extends StatelessWidget {
|
||||
required this.id,
|
||||
required this.cursorOverImage,
|
||||
required this.keyboardEnabled,
|
||||
required this.remoteCursorMoved,
|
||||
this.listenerBuilder})
|
||||
: super(key: key);
|
||||
|
||||
@@ -476,6 +483,7 @@ class ImagePaint extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final m = Provider.of<ImageModel>(context);
|
||||
var c = Provider.of<CanvasModel>(context);
|
||||
final cursor = Provider.of<CursorModel>(context);
|
||||
final s = c.scale;
|
||||
if (c.scrollStyle == ScrollStyle.scrollbar) {
|
||||
final imageWidget = SizedBox(
|
||||
@@ -501,12 +509,16 @@ class ImagePaint extends StatelessWidget {
|
||||
return false;
|
||||
},
|
||||
child: Obx(() => MouseRegion(
|
||||
// cursor: (keyboardEnabled.isTrue && cursorOverImage.isTrue)
|
||||
// ? SystemMouseCursors.none
|
||||
// : MouseCursor.defer,
|
||||
/// cursor: MouseCursor.defer,
|
||||
cursor: FlutterCustomCursor(
|
||||
path: "assets/pencil.png", x: 1.0, y: 8.0),
|
||||
cursor: (cursorOverImage.isTrue && keyboardEnabled.isTrue)
|
||||
? (remoteCursorMoved.isTrue
|
||||
? SystemMouseCursors.none
|
||||
: FlutterCustomMemoryImageCursor(
|
||||
pixbuf: cursor.rgba!,
|
||||
hotx: cursor.hotx,
|
||||
hoty: cursor.hoty,
|
||||
imageWidth: (cursor.image!.width * s).toInt(),
|
||||
imageHeight: (cursor.image!.height * s).toInt()))
|
||||
: MouseCursor.defer,
|
||||
onHover: (evt) {
|
||||
pos.value = evt.position;
|
||||
},
|
||||
|
||||
@@ -1031,6 +1031,7 @@ class PopupMenuButton<T> extends StatefulWidget {
|
||||
Key? key,
|
||||
required this.itemBuilder,
|
||||
this.initialValue,
|
||||
this.onHover,
|
||||
this.onSelected,
|
||||
this.onCanceled,
|
||||
this.tooltip,
|
||||
@@ -1061,6 +1062,9 @@ class PopupMenuButton<T> extends StatefulWidget {
|
||||
/// The value of the menu item, if any, that should be highlighted when the menu opens.
|
||||
final T? initialValue;
|
||||
|
||||
/// Called when the user hovers this button.
|
||||
final ValueChanged<bool>? onHover;
|
||||
|
||||
/// Called when the user selects a value from the popup menu created by this button.
|
||||
///
|
||||
/// If the popup menu is dismissed without selecting a value, [onCanceled] is
|
||||
@@ -1273,18 +1277,20 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
|
||||
|
||||
assert(debugCheckHasMaterialLocalizations(context));
|
||||
|
||||
if (widget.child != null)
|
||||
if (widget.child != null) {
|
||||
return Tooltip(
|
||||
message:
|
||||
widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip,
|
||||
child: InkWell(
|
||||
onTap: widget.enabled ? showButtonMenu : null,
|
||||
onHover: widget.onHover,
|
||||
canRequestFocus: _canRequestFocus,
|
||||
radius: widget.splashRadius,
|
||||
enableFeedback: enableFeedback,
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return IconButton(
|
||||
icon: widget.icon ?? Icon(Icons.adaptive.more),
|
||||
|
||||
@@ -12,7 +12,7 @@ class PopupMenuChildrenItem<T> extends mod_menu.PopupMenuEntry<T> {
|
||||
key,
|
||||
this.height = kMinInteractiveDimension,
|
||||
this.padding,
|
||||
this.enable = true,
|
||||
this.enabled,
|
||||
this.textStyle,
|
||||
this.onTap,
|
||||
this.position = mod_menu.PopupMenuPosition.overSide,
|
||||
@@ -25,7 +25,7 @@ class PopupMenuChildrenItem<T> extends mod_menu.PopupMenuEntry<T> {
|
||||
final Offset offset;
|
||||
final TextStyle? textStyle;
|
||||
final EdgeInsets? padding;
|
||||
final bool enable;
|
||||
final RxBool? enabled;
|
||||
final void Function()? onTap;
|
||||
final List<mod_menu.PopupMenuEntry<T>> Function(BuildContext) itemBuilder;
|
||||
final Widget child;
|
||||
@@ -56,25 +56,27 @@ class MyPopupMenuItemState<T, W extends PopupMenuChildrenItem<T>>
|
||||
TextStyle style = widget.textStyle ??
|
||||
popupMenuTheme.textStyle ??
|
||||
theme.textTheme.subtitle1!;
|
||||
|
||||
return mod_menu.PopupMenuButton<T>(
|
||||
enabled: widget.enable,
|
||||
position: widget.position,
|
||||
offset: widget.offset,
|
||||
onSelected: handleTap,
|
||||
itemBuilder: widget.itemBuilder,
|
||||
padding: EdgeInsets.zero,
|
||||
child: AnimatedDefaultTextStyle(
|
||||
style: style,
|
||||
duration: kThemeChangeDuration,
|
||||
child: Container(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
constraints: BoxConstraints(minHeight: widget.height),
|
||||
padding: widget.padding ?? const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: widget.child,
|
||||
return Obx(() {
|
||||
return mod_menu.PopupMenuButton<T>(
|
||||
enabled: widget.enabled != null ? widget.enabled!.value : true,
|
||||
position: widget.position,
|
||||
offset: widget.offset,
|
||||
onSelected: handleTap,
|
||||
itemBuilder: widget.itemBuilder,
|
||||
padding: EdgeInsets.zero,
|
||||
child: AnimatedDefaultTextStyle(
|
||||
style: style,
|
||||
duration: kThemeChangeDuration,
|
||||
child: Container(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
constraints: BoxConstraints(minHeight: widget.height),
|
||||
padding:
|
||||
widget.padding ?? const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,8 +100,12 @@ class MenuConfig {
|
||||
|
||||
abstract class MenuEntryBase<T> {
|
||||
bool dismissOnClicked;
|
||||
RxBool? enabled;
|
||||
|
||||
MenuEntryBase({this.dismissOnClicked = false});
|
||||
MenuEntryBase({
|
||||
this.dismissOnClicked = false,
|
||||
this.enabled,
|
||||
});
|
||||
List<mod_menu.PopupMenuEntry<T>> build(BuildContext context, MenuConfig conf);
|
||||
}
|
||||
|
||||
@@ -119,9 +125,14 @@ class MenuEntryRadioOption {
|
||||
String text;
|
||||
String value;
|
||||
bool dismissOnClicked;
|
||||
RxBool? enabled;
|
||||
|
||||
MenuEntryRadioOption(
|
||||
{required this.text, required this.value, this.dismissOnClicked = false});
|
||||
MenuEntryRadioOption({
|
||||
required this.text,
|
||||
required this.value,
|
||||
this.dismissOnClicked = false,
|
||||
this.enabled,
|
||||
});
|
||||
}
|
||||
|
||||
typedef RadioOptionsGetter = List<MenuEntryRadioOption> Function();
|
||||
@@ -138,13 +149,14 @@ class MenuEntryRadios<T> extends MenuEntryBase<T> {
|
||||
final RadioOptionSetter optionSetter;
|
||||
final RxString _curOption = "".obs;
|
||||
|
||||
MenuEntryRadios(
|
||||
{required this.text,
|
||||
required this.optionsGetter,
|
||||
required this.curOptionGetter,
|
||||
required this.optionSetter,
|
||||
dismissOnClicked = false})
|
||||
: super(dismissOnClicked: dismissOnClicked) {
|
||||
MenuEntryRadios({
|
||||
required this.text,
|
||||
required this.optionsGetter,
|
||||
required this.curOptionGetter,
|
||||
required this.optionSetter,
|
||||
dismissOnClicked = false,
|
||||
RxBool? enabled,
|
||||
}) : super(dismissOnClicked: dismissOnClicked, enabled: enabled) {
|
||||
() async {
|
||||
_curOption.value = await curOptionGetter();
|
||||
}();
|
||||
@@ -220,13 +232,17 @@ class MenuEntrySubRadios<T> extends MenuEntryBase<T> {
|
||||
final RadioOptionSetter optionSetter;
|
||||
final RxString _curOption = "".obs;
|
||||
|
||||
MenuEntrySubRadios(
|
||||
{required this.text,
|
||||
required this.optionsGetter,
|
||||
required this.curOptionGetter,
|
||||
required this.optionSetter,
|
||||
dismissOnClicked = false})
|
||||
: super(dismissOnClicked: dismissOnClicked) {
|
||||
MenuEntrySubRadios({
|
||||
required this.text,
|
||||
required this.optionsGetter,
|
||||
required this.curOptionGetter,
|
||||
required this.optionSetter,
|
||||
dismissOnClicked = false,
|
||||
RxBool? enabled,
|
||||
}) : super(
|
||||
dismissOnClicked: dismissOnClicked,
|
||||
enabled: enabled,
|
||||
) {
|
||||
() async {
|
||||
_curOption.value = await curOptionGetter();
|
||||
}();
|
||||
@@ -293,6 +309,7 @@ class MenuEntrySubRadios<T> extends MenuEntryBase<T> {
|
||||
BuildContext context, MenuConfig conf) {
|
||||
return [
|
||||
PopupMenuChildrenItem(
|
||||
enabled: super.enabled,
|
||||
padding: EdgeInsets.zero,
|
||||
height: conf.height,
|
||||
itemBuilder: (BuildContext context) =>
|
||||
@@ -327,9 +344,12 @@ abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
|
||||
final String text;
|
||||
final Rx<TextStyle>? textStyle;
|
||||
|
||||
MenuEntrySwitchBase(
|
||||
{required this.text, required dismissOnClicked, this.textStyle})
|
||||
: super(dismissOnClicked: dismissOnClicked);
|
||||
MenuEntrySwitchBase({
|
||||
required this.text,
|
||||
required dismissOnClicked,
|
||||
this.textStyle,
|
||||
RxBool? enabled,
|
||||
}) : super(dismissOnClicked: dismissOnClicked, enabled: enabled);
|
||||
|
||||
RxBool get curOption;
|
||||
Future<void> setOption(bool option);
|
||||
@@ -395,16 +415,19 @@ class MenuEntrySwitch<T> extends MenuEntrySwitchBase<T> {
|
||||
final SwitchSetter setter;
|
||||
final RxBool _curOption = false.obs;
|
||||
|
||||
MenuEntrySwitch(
|
||||
{required String text,
|
||||
required this.getter,
|
||||
required this.setter,
|
||||
Rx<TextStyle>? textStyle,
|
||||
dismissOnClicked = false})
|
||||
: super(
|
||||
text: text,
|
||||
textStyle: textStyle,
|
||||
dismissOnClicked: dismissOnClicked) {
|
||||
MenuEntrySwitch({
|
||||
required String text,
|
||||
required this.getter,
|
||||
required this.setter,
|
||||
Rx<TextStyle>? textStyle,
|
||||
dismissOnClicked = false,
|
||||
RxBool? enabled,
|
||||
}) : super(
|
||||
text: text,
|
||||
textStyle: textStyle,
|
||||
dismissOnClicked: dismissOnClicked,
|
||||
enabled: enabled,
|
||||
) {
|
||||
() async {
|
||||
_curOption.value = await getter();
|
||||
}();
|
||||
@@ -429,13 +452,14 @@ class MenuEntrySwitch2<T> extends MenuEntrySwitchBase<T> {
|
||||
final Switch2Getter getter;
|
||||
final SwitchSetter setter;
|
||||
|
||||
MenuEntrySwitch2(
|
||||
{required String text,
|
||||
required this.getter,
|
||||
required this.setter,
|
||||
Rx<TextStyle>? textStyle,
|
||||
dismissOnClicked = false})
|
||||
: super(
|
||||
MenuEntrySwitch2({
|
||||
required String text,
|
||||
required this.getter,
|
||||
required this.setter,
|
||||
Rx<TextStyle>? textStyle,
|
||||
dismissOnClicked = false,
|
||||
RxBool? enabled,
|
||||
}) : super(
|
||||
text: text,
|
||||
textStyle: textStyle,
|
||||
dismissOnClicked: dismissOnClicked);
|
||||
@@ -452,13 +476,18 @@ class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
|
||||
final String text;
|
||||
final List<MenuEntryBase<T>> entries;
|
||||
|
||||
MenuEntrySubMenu({required this.text, required this.entries});
|
||||
MenuEntrySubMenu({
|
||||
required this.text,
|
||||
required this.entries,
|
||||
RxBool? enabled,
|
||||
}) : super(enabled: enabled);
|
||||
|
||||
@override
|
||||
List<mod_menu.PopupMenuEntry<T>> build(
|
||||
BuildContext context, MenuConfig conf) {
|
||||
return [
|
||||
PopupMenuChildrenItem(
|
||||
enabled: super.enabled,
|
||||
height: conf.height,
|
||||
padding: EdgeInsets.zero,
|
||||
position: mod_menu.PopupMenuPosition.overSide,
|
||||
@@ -468,20 +497,24 @@ class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
|
||||
.toList(),
|
||||
child: Row(children: [
|
||||
const SizedBox(width: MenuConfig.midPadding),
|
||||
Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: MyTheme.color(context).text,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal),
|
||||
),
|
||||
Obx(() => Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: (super.enabled != null ? super.enabled!.value : true)
|
||||
? Colors.black
|
||||
: Colors.grey,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal),
|
||||
)),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Icon(
|
||||
Icons.keyboard_arrow_right,
|
||||
color: conf.commonColor,
|
||||
),
|
||||
child: Obx(() => Icon(
|
||||
Icons.keyboard_arrow_right,
|
||||
color: (super.enabled != null ? super.enabled!.value : true)
|
||||
? conf.commonColor
|
||||
: Colors.grey,
|
||||
)),
|
||||
))
|
||||
]),
|
||||
)
|
||||
@@ -493,36 +526,57 @@ class MenuEntryButton<T> extends MenuEntryBase<T> {
|
||||
final Widget Function(TextStyle? style) childBuilder;
|
||||
Function() proc;
|
||||
|
||||
MenuEntryButton(
|
||||
{required this.childBuilder,
|
||||
required this.proc,
|
||||
dismissOnClicked = false})
|
||||
: super(dismissOnClicked: dismissOnClicked);
|
||||
MenuEntryButton({
|
||||
required this.childBuilder,
|
||||
required this.proc,
|
||||
dismissOnClicked = false,
|
||||
RxBool? enabled,
|
||||
}) : super(
|
||||
dismissOnClicked: dismissOnClicked,
|
||||
enabled: enabled,
|
||||
);
|
||||
|
||||
Widget _buildChild(BuildContext context, MenuConfig conf) {
|
||||
return Obx(() {
|
||||
bool enabled = true;
|
||||
if (super.enabled != null) {
|
||||
enabled = super.enabled!.value;
|
||||
}
|
||||
const enabledStyle = TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal);
|
||||
const disabledStyle = TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal);
|
||||
return TextButton(
|
||||
onPressed: enabled
|
||||
? () {
|
||||
if (super.dismissOnClicked && Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
proc();
|
||||
}
|
||||
: null,
|
||||
child: Container(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
constraints: BoxConstraints(minHeight: conf.height),
|
||||
child: childBuilder(enabled ? enabledStyle : disabledStyle),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
List<mod_menu.PopupMenuEntry<T>> build(
|
||||
BuildContext context, MenuConfig conf) {
|
||||
return [
|
||||
mod_menu.PopupMenuItem(
|
||||
enabled: super.enabled != null ? super.enabled!.value : true,
|
||||
padding: EdgeInsets.zero,
|
||||
height: conf.height,
|
||||
child: TextButton(
|
||||
child: Container(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
constraints: BoxConstraints(minHeight: conf.height),
|
||||
child: childBuilder(
|
||||
TextStyle(
|
||||
color: MyTheme.color(context).text,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal),
|
||||
)),
|
||||
onPressed: () {
|
||||
if (super.dismissOnClicked && Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
proc();
|
||||
},
|
||||
),
|
||||
child: _buildChild(context, conf),
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user