From 22200b8812cfb9507f2b04acb3d592e34fbcc56c Mon Sep 17 00:00:00 2001 From: Jimmy Li <81349106+aenckli@users.noreply.github.com> Date: Mon, 12 Sep 2022 00:39:15 +0800 Subject: [PATCH] 1. changed translation locale country code to lower case (#946) --- example/assets/sample_data_nomedia.json | 521 ++++++++++++++++++++++++ example/lib/main.dart | 11 +- example/lib/pages/home_page.dart | 3 +- example/lib/pages/read_only_page.dart | 3 +- lib/src/translations/toolbar.i18n.dart | 2 +- lib/src/widgets/delegate.dart | 21 + lib/src/widgets/editor.dart | 2 +- lib/src/widgets/raw_editor.dart | 80 +++- lib/src/widgets/text_selection.dart | 55 ++- 9 files changed, 684 insertions(+), 14 deletions(-) create mode 100644 example/assets/sample_data_nomedia.json diff --git a/example/assets/sample_data_nomedia.json b/example/assets/sample_data_nomedia.json new file mode 100644 index 00000000..e541d153 --- /dev/null +++ b/example/assets/sample_data_nomedia.json @@ -0,0 +1,521 @@ +[ + { + "insert": "Flutter Quill" + }, + { + "attributes": { + "header": 1 + }, + "insert": "\n" + }, + { + "insert": "\nRich text editor for Flutter" + }, + { + "attributes": { + "header": 2 + }, + "insert": "\n" + }, + { + "insert": "Quill component for Flutter" + }, + { + "attributes": { + "header": 3 + }, + "insert": "\n" + }, + { + "insert": "This " + }, + { + "attributes": { + "italic": true, + "background": "transparent" + }, + "insert": "library" + }, + { + "insert": " supports " + }, + { + "attributes": { + "bold": true, + "background": "#ebd6ff" + }, + "insert": "mobile" + }, + { + "insert": " platform " + }, + { + "attributes": { + "underline": true, + "bold": true, + "color": "#e60000" + }, + "insert": "only" + }, + { + "attributes": { + "color": "rgba(0, 0, 0, 0.847)" + }, + "insert": " and " + }, + { + "attributes": { + "strike": true, + "color": "black" + }, + "insert": "web" + }, + { + "insert": " is not supported.\nYou are welcome to use " + }, + { + "attributes": { + "link": "https://bulletjournal.us/home/index.html" + }, + "insert": "Bullet Journal" + }, + { + "insert": ":\nTrack personal and group journals (ToDo, Note, Ledger) from multiple views with timely reminders" + }, + { + "attributes": { + "list": "ordered" + }, + "insert": "\n" + }, + { + "insert": "Share your tasks and notes with teammates, and see changes as they happen in real-time, across all devices" + }, + { + "attributes": { + "list": "ordered" + }, + "insert": "\n" + }, + { + "insert": "Check out what you and your teammates are working on each day" + }, + { + "attributes": { + "list": "ordered" + }, + "insert": "\n" + }, + { + "insert": "\nSplitting bills with friends can never be easier." + }, + { + "attributes": { + "list": "bullet" + }, + "insert": "\n" + }, + { + "insert": "Start creating a group and invite your friends to join." + }, + { + "attributes": { + "list": "bullet" + }, + "insert": "\n" + }, + { + "insert": "Create a BuJo of Ledger type to see expense or balance summary." + }, + { + "attributes": { + "list": "bullet" + }, + "insert": "\n" + }, + { + "insert": "\nAttach one or multiple labels to tasks, notes or transactions. Later you can track them just using the label(s)." + }, + { + "attributes": { + "blockquote": true + }, + "insert": "\n" + }, + { + "insert": "\nvar BuJo = 'Bullet' + 'Journal'" + }, + { + "attributes": { + "code-block": true + }, + "insert": "\n" + }, + { + "insert": "\nStart tracking in your browser" + }, + { + "attributes": { + "indent": 1 + }, + "insert": "\n" + }, + { + "insert": "Stop the timer on your phone" + }, + { + "attributes": { + "indent": 1 + }, + "insert": "\n" + }, + { + "insert": "All your time entries are synced" + }, + { + "attributes": { + "indent": 2 + }, + "insert": "\n" + }, + { + "insert": "between the phone apps" + }, + { + "attributes": { + "indent": 2 + }, + "insert": "\n" + }, + { + "insert": "and the website." + }, + { + "attributes": { + "indent": 3 + }, + "insert": "\n" + }, + { + "insert": "\n" + }, + { + "insert": "\nCenter Align" + }, + { + "attributes": { + "align": "center" + }, + "insert": "\n" + }, + { + "insert": "Right Align" + }, + { + "attributes": { + "align": "right" + }, + "insert": "\n" + }, + { + "insert": "Justify Align" + }, + { + "attributes": { + "align": "justify" + }, + "insert": "\n" + }, + { + "insert": "Have trouble finding things? " + }, + { + "attributes": { + "list": "ordered" + }, + "insert": "\n" + }, + { + "insert": "Just type in the search bar" + }, + { + "attributes": { + "indent": 1, + "list": "ordered" + }, + "insert": "\n" + }, + { + "insert": "and easily find contents" + }, + { + "attributes": { + "indent": 2, + "list": "ordered" + }, + "insert": "\n" + }, + { + "insert": "across projects or folders." + }, + { + "attributes": { + "indent": 2, + "list": "ordered" + }, + "insert": "\n" + }, + { + "insert": "It matches text in your note or task." + }, + { + "attributes": { + "indent": 1, + "list": "ordered" + }, + "insert": "\n" + }, + { + "insert": "Enable reminders so that you will get notified by" + }, + { + "attributes": { + "list": "ordered" + }, + "insert": "\n" + }, + { + "insert": "email" + }, + { + "attributes": { + "indent": 1, + "list": "ordered" + }, + "insert": "\n" + }, + { + "insert": "message on your phone" + }, + { + "attributes": { + "indent": 1, + "list": "ordered" + }, + "insert": "\n" + }, + { + "insert": "popup on the web site" + }, + { + "attributes": { + "indent": 1, + "list": "ordered" + }, + "insert": "\n" + }, + { + "insert": "Create a BuJo serving as project or folder" + }, + { + "attributes": { + "list": "bullet" + }, + "insert": "\n" + }, + { + "insert": "Organize your" + }, + { + "attributes": { + "indent": 1, + "list": "bullet" + }, + "insert": "\n" + }, + { + "insert": "tasks" + }, + { + "attributes": { + "indent": 2, + "list": "bullet" + }, + "insert": "\n" + }, + { + "insert": "notes" + }, + { + "attributes": { + "indent": 2, + "list": "bullet" + }, + "insert": "\n" + }, + { + "insert": "transactions" + }, + { + "attributes": { + "indent": 2, + "list": "bullet" + }, + "insert": "\n" + }, + { + "insert": "under BuJo " + }, + { + "attributes": { + "indent": 3, + "list": "bullet" + }, + "insert": "\n" + }, + { + "insert": "See them in Calendar" + }, + { + "attributes": { + "list": "bullet" + }, + "insert": "\n" + }, + { + "insert": "or hierarchical view" + }, + { + "attributes": { + "indent": 1, + "list": "bullet" + }, + "insert": "\n" + }, + { + "insert": "this is a check list" + }, + { + "attributes": { + "list": "checked" + }, + "insert": "\n" + }, + { + "insert": "this is a uncheck list" + }, + { + "attributes": { + "list": "unchecked" + }, + "insert": "\n" + }, + { + "insert": "Font " + }, + { + "attributes": { + "font": "sans-serif" + }, + "insert": "Sans Serif" + }, + { + "insert": " " + }, + { + "attributes": { + "font": "serif" + }, + "insert": "Serif" + }, + { + "insert": " " + }, + { + "attributes": { + "font": "monospace" + }, + "insert": "Monospace" + }, + { + "insert": " Size " + }, + { + "attributes": { + "size": "small" + }, + "insert": "Small" + }, + { + "insert": " " + }, + { + "attributes": { + "size": "large" + }, + "insert": "Large" + }, + { + "insert": " " + }, + { + "attributes": { + "size": "huge" + }, + "insert": "Huge" + }, + { + "attributes": { + "size": "15.0" + }, + "insert": "font size 15" + }, + { + "insert": " " + }, + { + "attributes": { + "size": "35" + }, + "insert": "font size 35" + }, + { + "insert": " " + }, + { + "attributes": { + "size": "20" + }, + "insert": "font size 20" + }, + { + "attributes": { + "token": "built_in" + }, + "insert": " diff" + }, + { + "attributes": { + "token": "operator" + }, + "insert": "-match" + }, + { + "attributes": { + "token": "literal" + }, + "insert": "-patch" + }, + { + "insert": { + "image": "https://user-images.githubusercontent.com/122956/72955931-ccc07900-3d52-11ea-89b1-d468a6e2aa2b.png" + }, + "attributes": { + "width": "230", + "style": "display: block; margin: auto;" + } + }, + { + "insert": "\n" + } +] \ No newline at end of file diff --git a/example/lib/main.dart b/example/lib/main.dart index 5b4feb2b..ce398bce 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; - +import 'package:flutter_localizations/flutter_localizations.dart'; import 'pages/home_page.dart'; void main() { @@ -30,6 +30,15 @@ class MyApp extends StatelessWidget { // closer together (more dense) than on mobile platforms. visualDensity: VisualDensity.adaptivePlatformDensity, ), + localizationsDelegates: [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: [ + const Locale('en', "US"), + const Locale('zh', "HK"), + ], home: HomePage(), ); } diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index 9910e4dd..5e5f348e 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -7,6 +7,7 @@ import 'package:filesystem_picker/filesystem_picker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_quill/extensions.dart'; import 'package:flutter_quill/flutter_quill.dart' hide Text; import 'package:flutter_quill_extensions/flutter_quill_extensions.dart'; import 'package:path/path.dart'; @@ -33,7 +34,7 @@ class _HomePageState extends State { Future _loadFromAssets() async { try { - final result = await rootBundle.loadString('assets/sample_data.json'); + final result = await rootBundle.loadString(isDesktop() ? 'assets/sample_data_nomedia.json' : 'assets/sample_data.json'); final doc = Document.fromJson(jsonDecode(result)); setState(() { _controller = QuillController( diff --git a/example/lib/pages/read_only_page.dart b/example/lib/pages/read_only_page.dart index 469e9f86..831552a3 100644 --- a/example/lib/pages/read_only_page.dart +++ b/example/lib/pages/read_only_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_quill/extensions.dart'; import 'package:flutter_quill/flutter_quill.dart' hide Text; import 'package:flutter_quill_extensions/flutter_quill_extensions.dart'; @@ -19,7 +20,7 @@ class _ReadOnlyPageState extends State { @override Widget build(BuildContext context) { return DemoScaffold( - documentFilename: 'sample_data.json', + documentFilename: isDesktop() ? 'assets/sample_data_nomedia.json' : 'sample_data_nomedia.json', builder: _buildContent, showToolbar: _edit == true, floatingActionButton: FloatingActionButton.extended( diff --git a/lib/src/translations/toolbar.i18n.dart b/lib/src/translations/toolbar.i18n.dart index 0b5e5527..b6e1f92c 100644 --- a/lib/src/translations/toolbar.i18n.dart +++ b/lib/src/translations/toolbar.i18n.dart @@ -734,7 +734,7 @@ extension Localization on String { 'Camera': 'Camera', 'Video': 'Video', }, - 'zh_HK': { + 'zh_hk': { 'Paste a link': '貼上連結', 'Ok': '確定', 'Select Color': '選擇顏色', diff --git a/lib/src/widgets/delegate.dart b/lib/src/widgets/delegate.dart index f2328849..615e53c2 100644 --- a/lib/src/widgets/delegate.dart +++ b/lib/src/widgets/delegate.dart @@ -2,6 +2,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; +import 'package:flutter_quill/src/utils/platform.dart'; import '../../flutter_quill.dart'; import 'text_selection.dart'; @@ -111,6 +112,7 @@ class EditorTextSelectionGestureDetectorBuilder { // For backwards-compatibility, we treat a null kind the same as touch. final kind = details.kind; shouldShowSelectionToolbar = kind == null || + kind == PointerDeviceKind.mouse || // this is added to enable word selection by mouse double tap kind == PointerDeviceKind.touch || kind == PointerDeviceKind.stylus; } @@ -178,6 +180,14 @@ class EditorTextSelectionGestureDetectorBuilder { } } + /// onSingleTapUp for mouse right click + @protected + void onSecondarySingleTapUp(TapUpDetails details) { // added to show toolbar by right click + if (shouldShowSelectionToolbar) { + editor!.showToolbar(); + } + } + /// Handler for [EditorTextSelectionGestureDetector.onSingleTapCancel]. /// /// By default, it services as place holder to enable subclass override. @@ -311,6 +321,13 @@ class EditorTextSelectionGestureDetectorBuilder { @protected void onDragSelectionEnd(DragEndDetails details) { renderEditor!.handleDragEnd(details); + if (isDesktop()) { // added to show selection copy/paste toolbar after drag to select + if (delegate.selectionEnabled) { + if (shouldShowSelectionToolbar) { + editor!.showToolbar(); + } + } + } } /// Returns a [EditorTextSelectionGestureDetector] configured with @@ -330,6 +347,10 @@ class EditorTextSelectionGestureDetectorBuilder { onSingleLongTapMoveUpdate: onSingleLongTapMoveUpdate, onSingleLongTapEnd: onSingleLongTapEnd, onDoubleTapDown: onDoubleTapDown, + onSecondaryTapDown: null, + onSecondarySingleTapUp: onSecondarySingleTapUp, + onSecondarySingleTapCancel: null, + onSecondaryDoubleTapDown: null, onDragSelectionStart: onDragSelectionStart, onDragSelectionUpdate: onDragSelectionUpdate, onDragSelectionEnd: onDragSelectionEnd, diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index bb673c4b..7ec83545 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -640,7 +640,7 @@ class _QuillEditorSelectionGestureDetectorBuilder try { if (delegate.selectionEnabled && !_isPositionSelected(details)) { final _platform = Theme.of(_state.context).platform; - if (isAppleOS(_platform)) { + if (isAppleOS(_platform) || isDesktop()) { // added isDesktop() to enable extend selection in Windows platform switch (details.kind) { case PointerDeviceKind.mouse: case PointerDeviceKind.stylus: diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index 9412b042..282ec420 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -362,14 +362,21 @@ class RawEditorState extends EditorState return QuillStyles( data: _styles!, - child: Actions( - actions: _actions, - child: Focus( - focusNode: widget.focusNode, - child: QuillKeyboardListener( - child: Container( - constraints: constraints, - child: child, + child: Shortcuts( + shortcuts: { // shortcuts added for Windows platform + LogicalKeySet(LogicalKeyboardKey.escape): HideSelectionToolbarIntent(), + LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyZ): const UndoTextIntent(SelectionChangedCause.keyboard), + LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyY): const RedoTextIntent(SelectionChangedCause.keyboard), + }, + child: Actions( + actions: _actions, + child: Focus( + focusNode: widget.focusNode, + child: QuillKeyboardListener( + child: Container( + constraints: constraints, + child: child, + ), ), ), ), @@ -1172,6 +1179,10 @@ class RawEditorState extends EditorState CopySelectionTextIntent: _makeOverridable(_CopySelectionAction(this)), PasteTextIntent: _makeOverridable(CallbackAction( onInvoke: (intent) => pasteText(intent.cause))), + + HideSelectionToolbarIntent: _makeOverridable(_HideSelectionToolbarAction(this)), + UndoTextIntent: _makeOverridable(_UndoKeyboardAction(this)), + RedoTextIntent: _makeOverridable(_RedoKeyboardAction(this)), }; @override @@ -1868,3 +1879,56 @@ class _CopySelectionAction extends ContextAction { state.textEditingValue.selection.isValid && !state.textEditingValue.selection.isCollapsed; } + +// intent class for "escape" key to dismiss selection toolbar in Windows platform +class HideSelectionToolbarIntent extends Intent { + const HideSelectionToolbarIntent(); +} + +class _HideSelectionToolbarAction extends ContextAction { + _HideSelectionToolbarAction(this.state); + + final RawEditorState state; + + @override + void invoke(HideSelectionToolbarIntent intent, [BuildContext? context]) { + state.hideToolbar(); + } + + @override + bool get isActionEnabled => + state.textEditingValue.selection.isValid; +} + +class _UndoKeyboardAction extends ContextAction { + _UndoKeyboardAction(this.state); + + final RawEditorState state; + + @override + void invoke(UndoTextIntent intent, [BuildContext? context]) { + if (state.controller.hasUndo) { + state.controller.undo(); + } + } + + @override + bool get isActionEnabled => true; +} + +class _RedoKeyboardAction extends ContextAction { + _RedoKeyboardAction(this.state); + + final RawEditorState state; + + @override + void invoke(RedoTextIntent intent, [BuildContext? context]) { + if (state.controller.hasRedo) { + state.controller.redo(); + } + } + + @override + bool get isActionEnabled => true; +} + diff --git a/lib/src/widgets/text_selection.dart b/lib/src/widgets/text_selection.dart index 9478dcab..ad022890 100644 --- a/lib/src/widgets/text_selection.dart +++ b/lib/src/widgets/text_selection.dart @@ -695,6 +695,10 @@ class EditorTextSelectionGestureDetector extends StatefulWidget { this.onForcePressEnd, this.onSingleTapUp, this.onSingleTapCancel, + this.onSecondaryTapDown, + this.onSecondarySingleTapUp, + this.onSecondarySingleTapCancel, + this.onSecondaryDoubleTapDown, this.onSingleLongTapStart, this.onSingleLongTapMoveUpdate, this.onSingleLongTapEnd, @@ -730,6 +734,15 @@ class EditorTextSelectionGestureDetector extends StatefulWidget { /// another gesture from the touch is recognized. final GestureTapCancelCallback? onSingleTapCancel; + /// onTapDown for mouse right click + final GestureTapDownCallback? onSecondaryTapDown; + /// onTapUp for mouse right click + final GestureTapUpCallback? onSecondarySingleTapUp; + /// onTapCancel for mouse right click + final GestureTapCancelCallback? onSecondarySingleTapCancel; + /// onDoubleTap for mouse right click + final GestureTapDownCallback? onSecondaryDoubleTapDown; + /// Called for a single long tap that's sustained for longer than /// [kLongPressTimeout] but not necessarily lifted. Not called for a /// double-tap-hold, which calls [onDoubleTapDown] instead. @@ -781,6 +794,9 @@ class _EditorTextSelectionGestureDetectorState // subsequent tap up / tap hold of the same tap. bool _isDoubleTap = false; + // _isDoubleTap for mouse right click + bool _isSecondaryDoubleTap = false; + @override void dispose() { _doubleTapTimer?.cancel(); @@ -829,6 +845,40 @@ class _EditorTextSelectionGestureDetectorState } } + // added secondary tap function for mouse right click to show toolbar + void _handleSecondaryTapDown(TapDownDetails details) { + if (widget.onSecondaryTapDown != null) { + widget.onSecondaryTapDown!(details); + } + if (_doubleTapTimer != null && + _isWithinDoubleTapTolerance(details.globalPosition)) { + if (widget.onSecondaryDoubleTapDown != null) { + widget.onSecondaryDoubleTapDown!(details); + } + + _doubleTapTimer!.cancel(); + _doubleTapTimeout(); + _isDoubleTap = true; + } + } + + void _handleSecondaryTapUp(TapUpDetails details) { + if (!_isSecondaryDoubleTap) { + if (widget.onSecondarySingleTapUp != null) { + widget.onSecondarySingleTapUp!(details); + } + _lastTapOffset = details.globalPosition; + _doubleTapTimer = Timer(kDoubleTapTimeout, _doubleTapTimeout); + } + _isSecondaryDoubleTap = false; + } + + void _handleSecondaryTapCancel() { + if (widget.onSecondarySingleTapCancel != null) { + widget.onSecondarySingleTapCancel!(); + } + } + DragStartDetails? _lastDragStartDetails; DragUpdateDetails? _lastDragUpdateDetails; Timer? _dragUpdateThrottleTimer; @@ -940,7 +990,10 @@ class _EditorTextSelectionGestureDetectorState instance ..onTapDown = _handleTapDown ..onTapUp = _handleTapUp - ..onTapCancel = _handleTapCancel; + ..onTapCancel = _handleTapCancel + ..onSecondaryTapDown = _handleSecondaryTapDown + ..onSecondaryTapUp = _handleSecondaryTapUp + ..onSecondaryTapCancel = _handleSecondaryTapCancel; }, );