Skip to content

Commit

Permalink
feat: undo/redo
Browse files Browse the repository at this point in the history
  • Loading branch information
chooyan-eng committed Dec 8, 2024
1 parent d9ea268 commit b5d525f
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 44 deletions.
25 changes: 24 additions & 1 deletion example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ class _CropSampleState extends State<CropSample> {
var _isCircleUi = false;
Uint8List? _croppedData;
var _statusText = '';
var _undoEnabled = false;
var _redoEnabled = false;

@override
void initState() {
Expand Down Expand Up @@ -153,6 +155,10 @@ class _CropSampleState extends State<CropSample> {
viewportRect.bottom - 24,
);
},
onHistoryChanged: (history) => setState(() {
_undoEnabled = history.undoCount > 0;
_redoEnabled = history.redoCount > 0;
}),
),
IgnorePointer(
child: Padding(
Expand Down Expand Up @@ -237,6 +243,24 @@ class _CropSampleState extends State<CropSample> {
}),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: _undoEnabled
? () => _cropController.undo()
: null,
child: Text('UNDO'),
),
const SizedBox(width: 16),
ElevatedButton(
onPressed: _redoEnabled
? () => _cropController.redo()
: null,
child: Text('REDO'),
),
],
),
const SizedBox(height: 16),
Container(
width: double.infinity,
Expand All @@ -255,7 +279,6 @@ class _CropSampleState extends State<CropSample> {
),
),
),
const SizedBox(height: 40),
],
),
),
Expand Down
12 changes: 12 additions & 0 deletions lib/src/widget/controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ class CropController {
/// change [ViewportBasedRect] of crop rect
/// based on [ImageBasedRect] of original image.
set area(ImageBasedRect value) => _delegate.onChangeArea(value);

/// request undo
void undo() => _delegate.onUndo();

/// request redo
void redo() => _delegate.onRedo();
}

/// Delegate of actions from [CropController]
Expand All @@ -57,4 +63,10 @@ class CropControllerDelegate {

/// callback that [CropController.area] is changed.
late ValueChanged<ImageBasedRect> onChangeArea;

/// callback that [CropController.undo] is called.
late VoidCallback onUndo;

/// callback that [CropController.redo] is called.
late VoidCallback onRedo;
}
40 changes: 38 additions & 2 deletions lib/src/widget/crop.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import 'package:flutter/gestures.dart';
typedef ViewportBasedRect = Rect;
typedef ImageBasedRect = Rect;

typedef History = (int undoCount, int redoCount);
typedef History = ({int undoCount, int redoCount});
typedef HistoryChangedCallback = void Function(History history);

typedef WillUpdateScale = bool Function(double newScale);
Expand Down Expand Up @@ -311,7 +311,9 @@ class _CropEditorState extends State<_CropEditor> {
}
..onChangeArea = (newArea) {
_resizeWith(widget.aspectRatio, newArea);
};
}
..onUndo = _undo
..onRedo = _redo;

// prepare for history state
_historyState = HistoryState(onHistoryChanged: widget.onHistoryChanged);
Expand Down Expand Up @@ -469,6 +471,20 @@ class _CropEditorState extends State<_CropEditor> {
}
}

void _undo() {
final last = _historyState.requestUndo(_readyState);
if (last != null) {
_updateCropRect(last);
}
}

void _redo() {
final last = _historyState.requestRedo(_readyState);
if (last != null) {
_updateCropRect(last);
}
}

/// crop given image with given area.
Future<void> _crop(bool withCircleShape) async {
assert(_parsedImageDetail != null);
Expand Down Expand Up @@ -496,6 +512,7 @@ class _CropEditorState extends State<_CropEditor> {

/// handle scale events with pinching
void _handleScaleStart(ScaleStartDetails detail) {
_historyState.pushHistory(_readyState);
_baseScale = _readyState.scale;
}

Expand All @@ -510,9 +527,18 @@ class _CropEditorState extends State<_CropEditor> {
);
}

DateTime? _pointerSignalLastUpdated;

/// handle mouse pointer signal event
void _handlePointerSignal(PointerSignalEvent signal) {
if (signal is PointerScrollEvent) {
final now = DateTime.now();
if (_pointerSignalLastUpdated == null ||
now.difference(_pointerSignalLastUpdated!).inMilliseconds > 500) {
_pointerSignalLastUpdated = now;
_historyState.pushHistory(_readyState);
}

if (signal.scrollDelta.dy > 0) {
_applyScale(
_readyState.scale - widget.scrollZoomSensitivity,
Expand Down Expand Up @@ -596,6 +622,8 @@ class _CropEditorState extends State<_CropEditor> {
left: _readyState.cropRect.left,
top: _readyState.cropRect.top,
child: GestureDetector(
onPanStart: (details) =>
_historyState.pushHistory(_readyState),
onPanUpdate: (details) => _updateCropRect(
_readyState.moveRect(details.delta),
),
Expand All @@ -610,6 +638,8 @@ class _CropEditorState extends State<_CropEditor> {
left: _readyState.cropRect.left - (dotTotalSize / 2),
top: _readyState.cropRect.top - (dotTotalSize / 2),
child: GestureDetector(
onPanStart: (details) =>
_historyState.pushHistory(_readyState),
onPanUpdate: widget.fixCropRect
? null
: (details) => _updateCropRect(
Expand All @@ -624,6 +654,8 @@ class _CropEditorState extends State<_CropEditor> {
left: _readyState.cropRect.right - (dotTotalSize / 2),
top: _readyState.cropRect.top - (dotTotalSize / 2),
child: GestureDetector(
onPanStart: (details) =>
_historyState.pushHistory(_readyState),
onPanUpdate: widget.fixCropRect
? null
: (details) => _updateCropRect(
Expand All @@ -638,6 +670,8 @@ class _CropEditorState extends State<_CropEditor> {
left: _readyState.cropRect.left - (dotTotalSize / 2),
top: _readyState.cropRect.bottom - (dotTotalSize / 2),
child: GestureDetector(
onPanStart: (details) =>
_historyState.pushHistory(_readyState),
onPanUpdate: widget.fixCropRect
? null
: (details) => _updateCropRect(
Expand All @@ -652,6 +686,8 @@ class _CropEditorState extends State<_CropEditor> {
left: _readyState.cropRect.right - (dotTotalSize / 2),
top: _readyState.cropRect.bottom - (dotTotalSize / 2),
child: GestureDetector(
onPanStart: (details) =>
_historyState.pushHistory(_readyState),
onPanUpdate: widget.fixCropRect
? null
: (details) => _updateCropRect(
Expand Down
21 changes: 14 additions & 7 deletions lib/src/widget/history_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,33 +21,40 @@ class HistoryState {
void pushHistory(CropEditorViewState viewState) {
history.add(viewState);
redoHistory.clear();
onHistoryChanged?.call((history.length, redoHistory.length));
onHistoryChanged?.call(
(undoCount: history.length, redoCount: redoHistory.length),
);
}

/// request [CropEditorViewState] for undo
/// this method will pop last history and push to redo history
CropEditorViewState? requestUndo() {
CropEditorViewState? requestUndo(CropEditorViewState current) {
if (history.isEmpty) {
return null;
}

redoHistory.add(current);
final last = history.removeLast();
redoHistory.add(last);
onHistoryChanged?.call((history.length, redoHistory.length));

onHistoryChanged?.call(
(undoCount: history.length, redoCount: redoHistory.length),
);

return last;
}

/// request [CropEditorViewState] for redo
/// this method will pop last redo history
CropEditorViewState? requestRedo() {
CropEditorViewState? requestRedo(CropEditorViewState current) {
if (redoHistory.isEmpty) {
return null;
}

history.add(current);
final last = redoHistory.removeLast();
history.add(last);
onHistoryChanged?.call((history.length, redoHistory.length));
onHistoryChanged?.call(
(undoCount: history.length, redoCount: redoHistory.length),
);
return last;
}
}
24 changes: 12 additions & 12 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -87,18 +87,18 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
url: "https://pub.dev"
source: hosted
version: "10.0.4"
version: "10.0.5"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
url: "https://pub.dev"
source: hosted
version: "3.0.3"
version: "3.0.5"
leak_tracker_testing:
dependency: transitive
description:
Expand All @@ -119,18 +119,18 @@ packages:
dependency: transitive
description:
name: material_color_utilities
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
source: hosted
version: "0.8.0"
version: "0.11.1"
meta:
dependency: transitive
description:
name: meta
sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
url: "https://pub.dev"
source: hosted
version: "1.12.0"
version: "1.15.0"
path:
dependency: transitive
description:
Expand Down Expand Up @@ -196,10 +196,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
url: "https://pub.dev"
source: hosted
version: "0.7.0"
version: "0.7.2"
typed_data:
dependency: transitive
description:
Expand All @@ -220,10 +220,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
url: "https://pub.dev"
source: hosted
version: "14.2.1"
version: "14.2.5"
xml:
dependency: transitive
description:
Expand Down
Loading

0 comments on commit b5d525f

Please sign in to comment.