diff --git a/lib/view/page/editor.dart b/lib/view/page/editor.dart index bac20568..afcb1bac 100644 --- a/lib/view/page/editor.dart +++ b/lib/view/page/editor.dart @@ -14,6 +14,7 @@ import 'package:server_box/data/store/setting.dart'; import 'package:server_box/view/widget/two_line_text.dart'; import 'package:re_editor/re_editor.dart'; +import 'package:server_box/view/widget/code_find_panel_view.dart'; enum EditorPageRetType { path, text } @@ -67,6 +68,7 @@ class _EditorPageState extends State { final _focusNode = FocusNode(); late CodeLineEditingController _controller; + late CodeFindController _findController; late Map _codeTheme; String? _langCode; @@ -74,14 +76,17 @@ class _EditorPageState extends State { @override void dispose() { - super.dispose(); _controller.dispose(); + _findController.dispose(); _focusNode.dispose(); + super.dispose(); } @override void initState() { super.initState(); + _controller = CodeLineEditingController(); + _findController = CodeFindController(_controller); _init(); } @@ -120,6 +125,11 @@ class _EditorPageState extends State { down: l10n.editor, ), actions: [ + IconButton( + icon: const Icon(Icons.search), + tooltip: libL10n.search, + onPressed: () => _findController.findMode(), + ), PopupMenuButton( icon: const Icon(Icons.language), tooltip: libL10n.language, @@ -150,6 +160,7 @@ class _EditorPageState extends State { color: _EditorReTheme.getBackground(_codeTheme), child: CodeEditor( controller: _controller, + findController: _findController, wordWrap: Stores.setting.editorSoftWrap.fetch(), focusNode: _focusNode, indicatorBuilder: (context, editingController, chunkController, notifier) { @@ -162,7 +173,8 @@ class _EditorPageState extends State { ], ); }, - // findBuilder: (context, controller, readOnly) => CodeFindPanelView(controller: controller, readOnly: readOnly), + findBuilder: (context, controller, readOnly) => + CodeFindPanelView(controller: controller, readOnly: readOnly), // toolbarController: const ContextMenuControllerImpl(), ), ); @@ -175,7 +187,6 @@ extension on _EditorPageState { if (Stores.setting.editorHighlight.fetch()) { _langCode = widget.args?.langCode ?? Highlights.getCode(widget.args?.path); } - _controller = CodeLineEditingController(); if (_langCode == null) { _setupCtrl(); } else { diff --git a/lib/view/widget/code_find_panel_view.dart b/lib/view/widget/code_find_panel_view.dart new file mode 100644 index 00000000..c39298c8 --- /dev/null +++ b/lib/view/widget/code_find_panel_view.dart @@ -0,0 +1,239 @@ +import 'dart:math'; + +import 'package:fl_lib/fl_lib.dart'; +import 'package:flutter/material.dart'; +import 'package:re_editor/re_editor.dart'; + +const EdgeInsetsGeometry _kDefaultFindMargin = EdgeInsets.only(right: 10); +const double _kDefaultFindPanelWidth = 360; +const double _kDefaultFindPanelHeight = 36; +const double _kDefaultReplacePanelHeight = _kDefaultFindPanelHeight * 2; +const double _kDefaultFindIconSize = 16; +const double _kDefaultFindIconWidth = 30; +const double _kDefaultFindIconHeight = 30; +const double _kDefaultFindInputFontSize = 13; +const double _kDefaultFindResultFontSize = 12; +const EdgeInsetsGeometry _kDefaultFindPadding = EdgeInsets.only(left: 5, right: 5, top: 2.5, bottom: 2.5); +const EdgeInsetsGeometry _kDefaultFindInputContentPadding = EdgeInsets.only(left: 5, right: 5); + +class CodeFindPanelView extends StatelessWidget implements PreferredSizeWidget { + final CodeFindController controller; + final EdgeInsetsGeometry margin; + final bool readOnly; + final Color? iconColor; + final Color? iconSelectedColor; + final double iconSize; + final double inputFontSize; + final double resultFontSize; + final Color? inputTextColor; + final Color? resultFontColor; + final EdgeInsetsGeometry padding; + final InputDecoration decoration; + + const CodeFindPanelView({ + super.key, + required this.controller, + this.margin = _kDefaultFindMargin, + required this.readOnly, + this.iconSelectedColor, + this.iconColor, + this.iconSize = _kDefaultFindIconSize, + this.inputFontSize = _kDefaultFindInputFontSize, + this.resultFontSize = _kDefaultFindResultFontSize, + this.inputTextColor, + this.resultFontColor, + this.padding = _kDefaultFindPadding, + this.decoration = const InputDecoration( + filled: true, + contentPadding: _kDefaultFindInputContentPadding, + border: OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(0)), gapPadding: 0), + ), + }); + + @override + Size get preferredSize { + final value = controller.value; + final height = value == null + ? 0.0 + : (value.replaceMode ? _kDefaultReplacePanelHeight : _kDefaultFindPanelHeight) + margin.vertical; + return Size(double.infinity, height); + } + + @override + Widget build(BuildContext context) { + if (controller.value == null) return UIs.placeholder; + + return Container( + margin: margin, + alignment: Alignment.topRight, + height: preferredSize.height, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: SizedBox( + width: _kDefaultFindPanelWidth, + child: Column( + children: [ + _buildFindInputView(context), + if (controller.value!.replaceMode) _buildReplaceInputView(context), + ], + ), + ), + ), + ); + } + + Widget _buildFindInputView(BuildContext context) { + final CodeFindValue value = controller.value!; + final String result; + if (value.result == null) { + result = libL10n.empty; + } else { + final index = value.result?.index; + final count = value.result?.matches.length; + result = '$index/$count'; + } + return Row( + children: [ + SizedBox( + width: _kDefaultFindPanelWidth / 1.75, + height: _kDefaultFindPanelHeight, + child: Stack( + alignment: Alignment.center, + children: [ + _buildTextField( + context: context, + controller: controller.findInputController, + focusNode: controller.findInputFocusNode, + iconsWidth: _kDefaultFindIconWidth * 1.5, + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + _buildCheckText( + context: context, + text: 'Aa', + checked: value.option.caseSensitive, + onPressed: controller.toggleCaseSensitive, + ), + _buildCheckText( + context: context, + text: '.*', + checked: value.option.regex, + onPressed: controller.toggleRegex, + ) + ], + ) + ], + )), + Text(result, style: TextStyle(color: resultFontColor, fontSize: resultFontSize)), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + _buildIconButton( + onPressed: value.result == null ? null : controller.previousMatch, + icon: Icons.arrow_upward, + tooltip: libL10n.previous, + ), + _buildIconButton( + onPressed: value.result == null ? null : controller.nextMatch, + icon: Icons.arrow_downward, + tooltip: libL10n.next, + ), + _buildIconButton( + onPressed: controller.close, + icon: Icons.close, + tooltip: libL10n.close, + ) + ], + ), + ) + ], + ); + } + + Widget _buildReplaceInputView(BuildContext context) { + final CodeFindValue value = controller.value!; + return Row( + children: [ + SizedBox( + width: _kDefaultFindPanelWidth / 1.75, + height: _kDefaultFindPanelHeight, + child: _buildTextField( + context: context, + controller: controller.replaceInputController, + focusNode: controller.replaceInputFocusNode, + ), + ), + _buildIconButton( + onPressed: value.result == null ? null : controller.replaceMatch, + icon: Icons.done, + tooltip: libL10n.replace, + ), + _buildIconButton( + onPressed: value.result == null ? null : controller.replaceAllMatches, + icon: Icons.done_all, + tooltip: libL10n.replaceAll, + ) + ], + ); + } + + Widget _buildTextField({ + required BuildContext context, + required TextEditingController controller, + required FocusNode focusNode, + double iconsWidth = 0, + }) { + return Padding( + padding: padding, + child: TextField( + maxLines: 1, + focusNode: focusNode, + style: TextStyle(color: inputTextColor, fontSize: inputFontSize), + decoration: decoration.copyWith( + contentPadding: + (decoration.contentPadding ?? EdgeInsets.zero).add(EdgeInsets.only(right: iconsWidth))), + controller: controller, + ), + ); + } + + Widget _buildCheckText({ + required BuildContext context, + required String text, + required bool checked, + required VoidCallback onPressed, + }) { + final Color selectedColor = iconSelectedColor ?? Theme.of(context).primaryColor; + return GestureDetector( + onTap: onPressed, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: SizedBox( + width: _kDefaultFindIconWidth * 0.75, + child: Text( + text, + style: TextStyle( + color: checked ? selectedColor : iconColor, + fontSize: inputFontSize, + ), + ), + ), + ), + ); + } + + Widget _buildIconButton({required IconData icon, VoidCallback? onPressed, String? tooltip}) { + return IconButton( + onPressed: onPressed, + icon: Icon( + icon, + size: iconSize, + ), + constraints: const BoxConstraints(maxWidth: _kDefaultFindIconWidth, maxHeight: _kDefaultFindIconHeight), + tooltip: tooltip, + splashRadius: max(_kDefaultFindIconWidth, _kDefaultFindIconHeight) / 2, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 0add42af..72330eb2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -534,8 +534,8 @@ packages: dependency: "direct main" description: path: "." - ref: "v1.0.283" - resolved-ref: "6e25f9f00bbf5c2e2900009f9f2e28454050c4ec" + ref: "v1.0.285" + resolved-ref: "5bcde3c934290093bc468d24c6f308da8fb8dd4e" url: "https://github.com/lppcg/fl_lib" source: git version: "0.0.1" diff --git a/pubspec.yaml b/pubspec.yaml index f0b99893..4def2158 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -63,7 +63,7 @@ dependencies: fl_lib: git: url: https://github.com/lppcg/fl_lib - ref: v1.0.283 + ref: v1.0.285 dependency_overrides: # webdav_client_plus: