From 6c098bc9928abc6feea613714ad7753cd034dbdd Mon Sep 17 00:00:00 2001 From: AraxTheCoder <40892690+AraxTheCoder@users.noreply.github.com> Date: Wed, 4 Oct 2023 10:58:03 +0200 Subject: [PATCH] feat: delete and duplicate selection (#937) * feat: added duplication and deletion of selection * fix: added tooltips to selection options * refactor: shifting of duplicated selection * fix: typo in comment Co-authored-by: Adil Hanney * fix: match duplication button in EditorPageManager Co-authored-by: Adil Hanney * fix: match delete button in EditorPageManager Co-authored-by: Adil Hanney * refactor: use conditional operator * refactor: use guard clauses and prevent unnecessary setState * refactor: rename SelectModal to SelectionBar * refactor: rename selectionOptions to selectionBar * fix: error in guard clause * fix: replace currentPageIndex with selectResult.pageIndex * fix: changed back to strokes.first.pageIndex becuase select.unselect() sets pageIndex to -1 * feat: only show selection options when selection is done * feat: unselect selection if selectionResullt is empty * ref: convert SelectionBar to StatelessWidget * chore: revert _missing_translations * ref: formatting * fix: assign new id to duplicated image * chore: revert timestamp to make merging easier --------- Co-authored-by: Adil Hanney --- lib/components/toolbar/selection_bar.dart | 50 +++++++++++++++ lib/components/toolbar/toolbar.dart | 19 ++++++ lib/data/tools/select.dart | 18 ++++++ lib/i18n/strings.g.dart | 12 ++++ lib/i18n/strings.i18n.yaml | 3 + lib/pages/editor/editor.dart | 77 +++++++++++++++++++++++ 6 files changed, 179 insertions(+) create mode 100644 lib/components/toolbar/selection_bar.dart diff --git a/lib/components/toolbar/selection_bar.dart b/lib/components/toolbar/selection_bar.dart new file mode 100644 index 000000000..c8c78240a --- /dev/null +++ b/lib/components/toolbar/selection_bar.dart @@ -0,0 +1,50 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:saber/components/theming/adaptive_icon.dart'; +import 'package:saber/i18n/strings.g.dart'; + +class SelectionBar extends StatelessWidget { + final VoidCallback duplicateSelection; + final VoidCallback deleteSelection; + + const SelectionBar({ + super.key, + required this.duplicateSelection, + required this.deleteSelection, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + onPressed: duplicateSelection, + style: TextButton.styleFrom( + foregroundColor: Theme.of(context).colorScheme.secondary, + backgroundColor: Colors.transparent, + shape: const CircleBorder(), + ), + tooltip: t.editor.selectionBar.duplicate, + icon: const AdaptiveIcon( + icon: Icons.content_copy, + cupertinoIcon: CupertinoIcons.doc_on_clipboard, + ), + ), + IconButton( + onPressed: deleteSelection, + style: TextButton.styleFrom( + foregroundColor: Theme.of(context).colorScheme.secondary, + backgroundColor: Colors.transparent, + shape: const CircleBorder(), + ), + tooltip: t.editor.selectionBar.delete, + icon: const AdaptiveIcon( + icon: Icons.delete, + cupertinoIcon: CupertinoIcons.delete, + ), + ), + ], + ); + } +} diff --git a/lib/components/toolbar/toolbar.dart b/lib/components/toolbar/toolbar.dart index bafd3cfbb..33d4f2cf0 100644 --- a/lib/components/toolbar/toolbar.dart +++ b/lib/components/toolbar/toolbar.dart @@ -10,6 +10,7 @@ import 'package:saber/components/theming/dynamic_material_app.dart'; import 'package:saber/components/toolbar/color_bar.dart'; import 'package:saber/components/toolbar/export_bar.dart'; import 'package:saber/components/toolbar/pen_modal.dart'; +import 'package:saber/components/toolbar/selection_bar.dart'; import 'package:saber/components/toolbar/toolbar_button.dart'; import 'package:saber/data/editor/page.dart'; import 'package:saber/data/extensions/color_extensions.dart'; @@ -47,6 +48,9 @@ class Toolbar extends StatefulWidget { required this.paste, + required this.duplicateSelection, + required this.deleteSelection, + required this.exportAsSbn, required this.exportAsPdf, required this.exportAsPng, @@ -73,6 +77,9 @@ class Toolbar extends StatefulWidget { final VoidCallback paste; + final VoidCallback duplicateSelection; + final VoidCallback deleteSelection; + final Future Function()? exportAsSbn; final Future Function()? exportAsPdf; final Future Function()? exportAsPng; @@ -165,6 +172,13 @@ class _ToolbarState extends State { _ => null, }; + if (widget.currentTool == Select.currentSelect) { + // Enable selection bar only when selection is done + toolOptionsType.value = Select.currentSelect.doneSelecting + ? ToolOptions.select + : ToolOptions.hide; + } + final children = [ ValueListenableBuilder( valueListenable: showExportOptions, @@ -201,6 +215,10 @@ class _ToolbarState extends State { getTool: () => Highlighter.currentHighlighter, setTool: widget.setTool, ), + ToolOptions.select => SelectionBar( + duplicateSelection: widget.duplicateSelection, + deleteSelection: widget.deleteSelection, + ), }, ); }, @@ -483,4 +501,5 @@ enum ToolOptions { hide, pen, highlighter, + select, } diff --git a/lib/data/tools/select.dart b/lib/data/tools/select.dart index 85e69b4f4..f7ae9ea87 100644 --- a/lib/data/tools/select.dart +++ b/lib/data/tools/select.dart @@ -130,4 +130,22 @@ class SelectResult { required this.images, required this.path, }); + + bool get isEmpty { + return strokes.isEmpty && images.isEmpty; + } + + SelectResult copyWith({ + int? pageIndex, + List? strokes, + List? images, + Path? path, + }) { + return SelectResult( + pageIndex: pageIndex ?? this.pageIndex, + strokes: strokes ?? this.strokes, + images: images ?? this.images, + path: path ?? this.path, + ); + } } diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index ee0b855c2..ab694f1ec 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -295,6 +295,7 @@ class _StringsEditorEn { late final _StringsEditorPenOptionsEn penOptions = _StringsEditorPenOptionsEn._(_root); late final _StringsEditorColorsEn colors = _StringsEditorColorsEn._(_root); late final _StringsEditorImageOptionsEn imageOptions = _StringsEditorImageOptionsEn._(_root); + late final _StringsEditorSelectionBarEn selectionBar = _StringsEditorSelectionBarEn._(_root); late final _StringsEditorMenuEn menu = _StringsEditorMenuEn._(_root); late final _StringsEditorNewerFileFormatEn newerFileFormat = _StringsEditorNewerFileFormatEn._(_root); late final _StringsEditorQuillEn quill = _StringsEditorQuillEn._(_root); @@ -799,6 +800,17 @@ class _StringsEditorImageOptionsEn { String get delete => 'Delete'; } +// Path: editor.selectionBar +class _StringsEditorSelectionBarEn { + _StringsEditorSelectionBarEn._(this._root); + + final _StringsEn _root; // ignore: unused_field + + // Translations + String get delete => 'Delete'; + String get duplicate => 'Duplicate'; +} + // Path: editor.menu class _StringsEditorMenuEn { _StringsEditorMenuEn._(this._root); diff --git a/lib/i18n/strings.i18n.yaml b/lib/i18n/strings.i18n.yaml index 011d85779..ae9d491ae 100644 --- a/lib/i18n/strings.i18n.yaml +++ b/lib/i18n/strings.i18n.yaml @@ -251,6 +251,9 @@ editor: setAsBackground: Set as background removeAsBackground: Remove as background delete: Delete + selectionBar: + delete: Delete + duplicate: Duplicate menu: clearPage: Clear page $page/$totalPages clearAllPages: Clear all pages diff --git a/lib/pages/editor/editor.dart b/lib/pages/editor/editor.dart index db54cfeb7..e0168b45e 100644 --- a/lib/pages/editor/editor.dart +++ b/lib/pages/editor/editor.dart @@ -619,6 +619,10 @@ class EditorState extends State { )); } else { select.onDragEnd(page.strokes, page.images); + + if (select.selectResult.isEmpty) { + Select.currentSelect.unselect(); + } } } else if (currentTool is LaserPointer) { Stroke newStroke = (currentTool as LaserPointer).onDragEnd( @@ -1245,6 +1249,79 @@ class EditorState extends State { }); }, currentTool: currentTool, + duplicateSelection: () { + final select = currentTool as Select; + if (!select.doneSelecting) return; + + setState(() { + final page = coreInfo.pages[select.selectResult.pageIndex]; + final strokes = select.selectResult.strokes; + final images = select.selectResult.images; + + const duplicationFeedbackOffset = Offset(25, -25); + + final duplicatedStrokes = strokes + .map((stroke) { + return stroke.copy() + ..shift(duplicationFeedbackOffset); + }) + .toList(); + + final duplicatedImages = images + .map((image) { + return image.copy() + ..id = coreInfo.nextImageId++ + ..dstRect.shift(duplicationFeedbackOffset); + }) + .toList(); + + page.strokes.addAll(duplicatedStrokes); + page.images.addAll(duplicatedImages); + + select.selectResult = select.selectResult.copyWith( + strokes: duplicatedStrokes, + images: duplicatedImages, + path: select.selectResult.path.shift(duplicationFeedbackOffset), + ); + + history.recordChange(EditorHistoryItem( + type: EditorHistoryItemType.draw, + pageIndex: select.selectResult.pageIndex, + strokes: duplicatedStrokes, + images: duplicatedImages, + )); + autosaveAfterDelay(); + }); + }, + deleteSelection: () { + final select = currentTool as Select; + if (!select.doneSelecting) { + return; + } + + setState(() { + final page = coreInfo.pages[select.selectResult.pageIndex]; + final strokes = select.selectResult.strokes; + final images = select.selectResult.images; + + for (Stroke stroke in strokes) { + page.strokes.remove(stroke); + } + for (EditorImage image in images) { + page.images.remove(image); + } + + select.unselect(); + + history.recordChange(EditorHistoryItem( + type: EditorHistoryItemType.erase, + pageIndex: strokes.first.pageIndex, + strokes: strokes, + images: images, + )); + autosaveAfterDelay(); + }); + }, setColor: (color) { setState(() { updateColorBar(color);