Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 14.11.0
- **FEAT**(main-editor): `moveLayerListPosition` takes `skipUpdateHistory` to reorder layers within the current history step and redraws them in their new order.
- **FIX**(main-editor): `moveLayerListPosition` ignores a `newIndex` past the last layer instead of throwing a `RangeError`.

## 14.10.2
- **FIX**(main-editor): `onOpenSubEditor`, `onStartCloseSubEditor` and `onEndCloseSubEditor` report the tune editor as `SubEditor.tune` instead of `SubEditor.unknown`.

Expand Down
38 changes: 28 additions & 10 deletions lib/features/main_editor/main_editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2602,21 +2602,39 @@ class ProImageEditorState extends State<ProImageEditor>
/// Moves a layer in the list to a new position.
///
/// - `oldIndex` is the current index of the layer.
/// - `newIndex` is the desired index to move the layer to.
void moveLayerListPosition({required int oldIndex, required int newIndex}) {
/// - `newIndex` is the index the layer ends up at, counted after it is
/// removed. An index outside the layer list is ignored.
///
/// Pass [skipUpdateHistory] to reorder the current history step instead of
/// recording a new one, e.g. when a drag already recorded its step when it
/// started, as with [setLayerTimeline]. The layers are redrawn in their new
/// order either way.
void moveLayerListPosition({
required int oldIndex,
required int newIndex,
bool skipUpdateHistory = false,
}) {
if (oldIndex == newIndex || oldIndex < 0 || newIndex < 0) return;
if (oldIndex >= activeLayers.length || newIndex >= activeLayers.length) {
return;
}

final layers = _layerCopyManager.copyLayerList(activeLayers);

if (oldIndex < layers.length && newIndex <= layers.length) {
final item = layers.removeAt(oldIndex);
final layers = skipUpdateHistory
? activeLayers
: _layerCopyManager.copyLayerList(activeLayers);

// Insert directly at newIndex, no adjustment needed
layers.insert(newIndex, item);
// Insert directly at newIndex, no adjustment needed
layers.insert(newIndex, layers.removeAt(oldIndex));

addHistory(layers: layers);
setState(() {});
if (skipUpdateHistory) {
// The step's background screenshot still shows the old order, so let
// the final capture take a new one.
stateManager.activeScreenshot?.broken = true;
_controllers.uiLayerCtrl.add(null);
return;
}
addHistory(layers: layers);
setState(() {});
}

/// Moves the given layer one step forward in the stack.
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: pro_image_editor
description: "A Flutter image editor: Seamlessly enhance your images with user-friendly editing features."
version: 14.10.2
version: 14.11.0
homepage: https://github.com/hm21/pro_image_editor/
repository: https://github.com/hm21/pro_image_editor/
documentation: https://github.com/hm21/pro_image_editor/
Expand Down
159 changes: 159 additions & 0 deletions test/features/main_editor/move_layer_list_position_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:material_ui/material_ui.dart';
import 'package:pro_image_editor/core/models/multi_threading/thread_capture_model.dart';
import 'package:pro_image_editor/pro_image_editor.dart';
import 'package:pro_image_editor/shared/widgets/layer/layer_widget.dart';

import '../../mock/mock_image.dart';

void main() {
const configs = ProImageEditorConfigs(
progressIndicatorConfigs: ProgressIndicatorConfigs(
widgets: ProgressIndicatorWidgets(
circularProgressIndicator: SizedBox.shrink(),
),
),
imageGeneration: ImageGenerationConfigs(
enableIsolateGeneration: false,
enableBackgroundGeneration: false,
),
);

Future<ProImageEditorState> pumpEditor(WidgetTester tester) async {
final key = GlobalKey<ProImageEditorState>();
await tester.pumpWidget(
MaterialApp(
home: ProImageEditor.memory(
mockMemoryImage,
key: key,
configs: configs,
callbacks: ProImageEditorCallbacks(
onImageEditingComplete: (Uint8List bytes) async {},
),
),
),
);
await tester.pumpAndSettle();
return key.currentState!;
}

/// Adds three emoji layers, bottom-most first.
Future<(ProImageEditorState, List<String>)> pumpThreeLayers(
WidgetTester tester,
) async {
final state = await pumpEditor(tester);
final layers = [
EmojiLayer(emoji: '🍎'),
EmojiLayer(emoji: '🍌'),
EmojiLayer(emoji: '🍒'),
];
for (final layer in layers) {
state.addLayer(
layer,
blockSelectLayer: true,
autoCorrectZoomOffset: false,
autoCorrectZoomScale: false,
);
}
await tester.pumpAndSettle();
return (state, [for (final layer in layers) layer.id]);
}

/// The ids of the layers the editor draws, bottom-most first.
List<String> drawnLayerIds(WidgetTester tester) => [
for (final widget in tester.widgetList<LayerWidget>(
find.byType(LayerWidget),
))
widget.layer.id,
];

group('moveLayerListPosition', () {
testWidgets('records a history step and redraws the layers', (
tester,
) async {
final (state, ids) = await pumpThreeLayers(tester);
final steps = state.stateManager.stateHistory.length;

state.moveLayerListPosition(oldIndex: 2, newIndex: 0);
await tester.pump();

expect(drawnLayerIds(tester), [ids[2], ids[0], ids[1]]);
expect(state.stateManager.stateHistory, hasLength(steps + 1));
});

testWidgets('with skipUpdateHistory redraws the layers in their new '
'order without a history step', (tester) async {
final (state, ids) = await pumpThreeLayers(tester);
expect(drawnLayerIds(tester), ids);
final steps = state.stateManager.stateHistory.length;

state.moveLayerListPosition(
oldIndex: 2,
newIndex: 0,
skipUpdateHistory: true,
);
await tester.pumpAndSettle();

expect(drawnLayerIds(tester), [ids[2], ids[0], ids[1]]);
expect(state.stateManager.stateHistory, hasLength(steps));
});

testWidgets('with skipUpdateHistory after addHistory undoes in one step', (
tester,
) async {
final (state, ids) = await pumpThreeLayers(tester);

state
..addHistory()
..moveLayerListPosition(
oldIndex: 0,
newIndex: 2,
skipUpdateHistory: true,
);
await tester.pumpAndSettle();
expect(drawnLayerIds(tester), [ids[1], ids[2], ids[0]]);

state.undoAction();
await tester.pump();

expect(drawnLayerIds(tester), ids);
});

testWidgets('with skipUpdateHistory invalidates the screenshot of the '
'current step only', (tester) async {
final (state, _) = await pumpThreeLayers(tester);
// Background generation is off here, so stand in one screenshot per
// recorded step, as the editor keeps them with it on.
final screenshots = [
for (var i = 1; i < state.stateManager.stateHistory.length; i++)
ThreadCaptureState(),
];
state.stateManager.screenshots.addAll(screenshots);

state.moveLayerListPosition(
oldIndex: 2,
newIndex: 0,
skipUpdateHistory: true,
);
await tester.pumpAndSettle();

expect(state.stateManager.activeScreenshot, same(screenshots.last));
expect(
[for (final item in screenshots) item.broken],
[false, false, true],
);
});

testWidgets('ignores an index past the last layer', (tester) async {
final (state, ids) = await pumpThreeLayers(tester);
final steps = state.stateManager.stateHistory.length;

state.moveLayerListPosition(oldIndex: 0, newIndex: 3);
await tester.pump();

expect(drawnLayerIds(tester), ids);
expect(state.stateManager.stateHistory, hasLength(steps));
});
});
}
Loading