From 48020a6ac49615134dece5a59bfeb7a33381f0ab Mon Sep 17 00:00:00 2001 From: Lenkomotive <90652966+Lenkomotive@users.noreply.github.com> Date: Thu, 4 Jul 2024 15:29:27 +0200 Subject: [PATCH 01/25] PAINTROID-761:Added the bridge communication between native and dart --- .github/workflows/main.yml | 12 +- Makefile | 36 +- README.md | 19 +- integration_test/command_manager_test.dart | 91 --- integration_test/tools/line_tool_test.dart | 573 ------------------- test/integration/command_manager_test.dart | 98 ++++ test/integration/driver/driver.dart | 3 + test/integration/line_tool_test.dart | 613 +++++++++++++++++++++ test/unit/tools/line_tool_test.dart | 203 ++++--- 9 files changed, 854 insertions(+), 794 deletions(-) delete mode 100644 integration_test/command_manager_test.dart delete mode 100644 integration_test/tools/line_tool_test.dart create mode 100644 test/integration/command_manager_test.dart create mode 100644 test/integration/driver/driver.dart create mode 100644 test/integration/line_tool_test.dart diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4d6e2d00..0a4f86e4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,12 +15,6 @@ jobs: cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:" architecture: x64 - - name: Enable KVM - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - - name: Setup run: make get @@ -34,11 +28,7 @@ jobs: run: make widget - name: Integration Tests - uses: reactivecircus/android-emulator-runner@v2 - with: - api-level: 29 - profile: Nexus 6 - script: make integration + run: make integration - name: Install xmlstarlet run: sudo apt-get install -y xmlstarlet diff --git a/Makefile b/Makefile index 17c0b38c..5c3a81ab 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,13 @@ FVM_PRESENT := $(shell command -v fvm 2> /dev/null) FLUTTER_CMD := $(if $(FVM_PRESENT),fvm flutter,flutter) DART_CMD := $(if $(FVM_PRESENT),fvm dart,dart) +INTEGRATION_TEST_DIR=test/integration +DRIVER_FILE=$(INTEGRATION_TEST_DIR)/driver/driver.dart +DART_DEFINE_ARGS= +ifdef id +DART_DEFINE_ARGS += --dart-define=id=$(id) +endif clean: $(FLUTTER_CMD) clean @@ -38,21 +44,23 @@ unit: widget: $(FLUTTER_CMD) test test/widget -target ?= all integration: - @if [ "$(target)" = "all" ]; then \ - find integration_test -type f -name '*_test.dart' -print0 | xargs -0 -n1 -I {} flutter test {}; \ - else \ - FILE_PATH=$$(find integration_test -type f -name "$(target).dart"); \ - if [ -z "$$FILE_PATH" ]; then \ - echo "Test file $(target) not found."; \ - exit 1; \ - else \ - flutter test $$FILE_PATH; \ - fi \ - fi - -test: unit widget integration +ifeq ($(strip $(target)),) + $(FLUTTER_CMD) test $(INTEGRATION_TEST_DIR) +else + $(FLUTTER_CMD) test $(INTEGRATION_TEST_DIR)/$(target)_test.dart $(DART_DEFINE_ARGS) +endif + +integration-drive: +ifeq ($(strip $(target)),) + find $(INTEGRATION_TEST_DIR) -name '*_test.dart' | while read test_file; do \ + flutter drive --driver=$(DRIVER_FILE) --target=$$test_file $(DART_DEFINE_ARGS); \ + done +else + flutter drive --driver=$(DRIVER_FILE) --target=$(INTEGRATION_TEST_DIR)/$(target)_test.dart $(DART_DEFINE_ARGS) +endif + +test: $(FLUTTER_CMD) test fvm_check: @echo Using $(FLUTTER_CMD) and $(DART_CMD) based on availability of FVM diff --git a/README.md b/README.md index 9ccc0d4d..7e0b87c8 100644 --- a/README.md +++ b/README.md @@ -37,12 +37,27 @@ Alternatively `make all` can be used to: - all: `make test` - unit: `make unit` - widget: `make widget` + - integration: `make integration` **For integration tests:** +Run the integration tests **without device**: + +1. Run `make integration` to run all integration tests +2. Run `make integration target=name_test` to run a specific integration test file + example: `make integration target=line_tool` +3. Run `make integration target=name_test id=n` to run a specific test in a file + example: `make integration target=line_tool id=1` + (make sure to add the `test` suffix to the test file name) + +Run the integration tests **with device**: + 1. Make sure you have an iOS/Android device online by running `flutter devices` -2. Run `make integration` to run all integration tests - Run `make integration target=name_test` to run a specific integration test file +2. Run `make integration-drive` to run all integration tests +3. Run `make integration-drive target=name_test` to run a specific integration test file + example: `make integration-drive target=line_tool` +4. Run `make integration-drive target=name_test id=x` to run a specific test in a file + example: `make integration-drive target=line_tool id=1` (make sure to add the `test` suffix to the file name) ## Issues diff --git a/integration_test/command_manager_test.dart b/integration_test/command_manager_test.dart deleted file mode 100644 index 2a2d773b..00000000 --- a/integration_test/command_manager_test.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; -import 'package:paintroid/app.dart'; -import 'package:paintroid/core/tools/tool_data.dart'; - -import '../test/utils/test_utils.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - late Widget sut; - - setUp(() async { - sut = ProviderScope( - child: App( - showOnboardingPage: false, - ), - ); - }); - - group('[COMMAND_MANAGER]', () { - testWidgets('Test if tool changes during undo', - (WidgetTester tester) async { - UIInteraction.initialize(tester); - await tester.pumpWidget(sut); - await UIInteraction.createNewImage(); - await UIInteraction.selectTool(ToolData.BRUSH.name); - await UIInteraction.tapAt(CanvasPosition.topLeft); - - await UIInteraction.selectTool(ToolData.LINE.name); - await UIInteraction.tapAt(CanvasPosition.bottomLeft); - await UIInteraction.tapAt(CanvasPosition.bottomRight); - - await UIInteraction.selectTool(ToolData.BRUSH.name); - await UIInteraction.tapAt(CanvasPosition.topRight); - - await UIInteraction.selectTool(ToolData.LINE.name); - - var currentTool = UIInteraction.getCurrentTool(); - expect(currentTool.type, ToolData.LINE.type); - - await UIInteraction.clickUndo(); - currentTool = UIInteraction.getCurrentTool(); - expect(currentTool.type, ToolData.BRUSH.type); - - await UIInteraction.clickUndo(); - currentTool = UIInteraction.getCurrentTool(); - expect(currentTool.type, ToolData.LINE.type); - - await UIInteraction.clickUndo(); - await UIInteraction.clickUndo(); - currentTool = UIInteraction.getCurrentTool(); - expect(currentTool.type, ToolData.BRUSH.type); - }); - - testWidgets('Test if tool changes during redo', - (WidgetTester tester) async { - UIInteraction.initialize(tester); - await tester.pumpWidget(sut); - await UIInteraction.createNewImage(); - await UIInteraction.selectTool(ToolData.BRUSH.name); - await UIInteraction.tapAt(CanvasPosition.topLeft); - - await UIInteraction.selectTool(ToolData.LINE.name); - await UIInteraction.tapAt(CanvasPosition.bottomLeft); - await UIInteraction.tapAt(CanvasPosition.bottomRight); - - await UIInteraction.selectTool(ToolData.BRUSH.name); - await UIInteraction.tapAt(CanvasPosition.topRight); - - await UIInteraction.clickUndo(times: 3); - - var currentTool = UIInteraction.getCurrentTool(); - expect(currentTool.type, ToolData.BRUSH.type); - - await UIInteraction.clickRedo(); - currentTool = UIInteraction.getCurrentTool(); - expect(currentTool.type, ToolData.BRUSH.type); - - await UIInteraction.clickRedo(); - currentTool = UIInteraction.getCurrentTool(); - expect(currentTool.type, ToolData.LINE.type); - - await UIInteraction.clickRedo(); - currentTool = UIInteraction.getCurrentTool(); - expect(currentTool.type, ToolData.BRUSH.type); - }); - }); -} diff --git a/integration_test/tools/line_tool_test.dart b/integration_test/tools/line_tool_test.dart deleted file mode 100644 index 61dd7364..00000000 --- a/integration_test/tools/line_tool_test.dart +++ /dev/null @@ -1,573 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; -import 'package:paintroid/app.dart'; -import 'package:paintroid/core/tools/tool_data.dart'; - -import '../../test/utils/test_utils.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - late Widget sut; - - setUp(() async { - sut = ProviderScope( - child: App( - showOnboardingPage: false, - ), - ); - }); - - testWidgets('[LINE_TOOL]: test line on top', (WidgetTester tester) async { - UIInteraction.initialize(tester); - await tester.pumpWidget(sut); - await UIInteraction.createNewImage(); - await UIInteraction.selectTool(ToolData.LINE.name); - - var colorTopCenter = await UIInteraction.getPixelColor( - CanvasPosition.centerX, - CanvasPosition.top, - ); - expect(colorTopCenter, Colors.transparent); - - await UIInteraction.tapAt(CanvasPosition.topLeft); - - await UIInteraction.tapAt(CanvasPosition.topRight); - - await UIInteraction.clickCheckmark(); - - colorTopCenter = await UIInteraction.getPixelColor( - CanvasPosition.centerX, - CanvasPosition.top, - ); - - expect(colorTopCenter, UIInteraction.getCurrentColor()); - }); - - testWidgets('[LINE_TOOL]: test line on bottom', (WidgetTester tester) async { - UIInteraction.initialize(tester); - await tester.pumpWidget(sut); - await UIInteraction.createNewImage(); - await UIInteraction.selectTool(ToolData.LINE.name); - - var colorBottomCenter = await UIInteraction.getPixelColor( - CanvasPosition.centerX, - CanvasPosition.bottom, - ); - expect(colorBottomCenter, Colors.transparent); - - await UIInteraction.tapAt(CanvasPosition.bottomLeft); - - await UIInteraction.tapAt(CanvasPosition.bottomRight); - - await UIInteraction.clickCheckmark(); - - colorBottomCenter = await UIInteraction.getPixelColor( - CanvasPosition.centerX, - CanvasPosition.bottom, - ); - - expect(colorBottomCenter, UIInteraction.getCurrentColor()); - }); - - testWidgets('[LINE_TOOL]: test vertical line', (WidgetTester tester) async { - UIInteraction.initialize(tester); - await tester.pumpWidget(sut); - await UIInteraction.createNewImage(); - await UIInteraction.selectTool(ToolData.LINE.name); - - final colorBefore = await UIInteraction.getPixelColor( - CanvasPosition.centerX, - CanvasPosition.centerY, - ); - expect(colorBefore, Colors.transparent); - - await UIInteraction.tapAt(CanvasPosition.topCenter); - - await UIInteraction.tapAt(CanvasPosition.bottomCenter); - - await UIInteraction.clickCheckmark(); - final colorAfter = await UIInteraction.getPixelColor( - CanvasPosition.centerX, - CanvasPosition.centerY, - ); - - expect(colorAfter, UIInteraction.getCurrentColor()); - }); - - testWidgets('[LINE_TOOL]: test horizontal line', (WidgetTester tester) async { - UIInteraction.initialize(tester); - await tester.pumpWidget(sut); - await UIInteraction.createNewImage(); - await UIInteraction.selectTool(ToolData.LINE.name); - - final colorBefore = await UIInteraction.getPixelColor( - CanvasPosition.centerX, - CanvasPosition.centerY, - ); - expect(colorBefore, Colors.transparent); - - await UIInteraction.tapAt(CanvasPosition.centerLeft); - - await UIInteraction.tapAt(CanvasPosition.centerRight); - - await UIInteraction.clickCheckmark(); - final colorAfter = await UIInteraction.getPixelColor( - CanvasPosition.centerX, - CanvasPosition.centerY, - ); - - expect(colorAfter, UIInteraction.getCurrentColor()); - }); - - testWidgets('[LINE_TOOL]: test diagonal line', (WidgetTester tester) async { - UIInteraction.initialize(tester); - await tester.pumpWidget(sut); - await UIInteraction.createNewImage(); - await UIInteraction.selectTool(ToolData.LINE.name); - - final colorBefore = await UIInteraction.getPixelColor( - CanvasPosition.centerX, - CanvasPosition.centerY, - ); - expect(colorBefore, Colors.transparent); - - await UIInteraction.tapAt(CanvasPosition.topLeft); - - await UIInteraction.tapAt(CanvasPosition.bottomRight); - - await UIInteraction.clickCheckmark(); - final colorAfter = await UIInteraction.getPixelColor( - CanvasPosition.centerX, - CanvasPosition.centerY, - ); - - expect(colorAfter, UIInteraction.getCurrentColor()); - }); - - testWidgets('[LINE_TOOL]: test multiple added lines', - (WidgetTester tester) async { - UIInteraction.initialize(tester); - await tester.pumpWidget(sut); - await UIInteraction.createNewImage(); - await UIInteraction.selectTool(ToolData.LINE.name); - - var colorHalfwayTop = await UIInteraction.getPixelColor( - CanvasPosition.centerX, - CanvasPosition.halfwayTop, - ); - expect(colorHalfwayTop, Colors.transparent); - - await UIInteraction.tapAt(CanvasPosition.halfTopLeft); - await UIInteraction.clickPlus(); - - await UIInteraction.tapAt(CanvasPosition.halfTopRight); - await UIInteraction.clickPlus(); - - colorHalfwayTop = await UIInteraction.getPixelColor( - CanvasPosition.centerX, - CanvasPosition.halfwayTop, - ); - - expect(colorHalfwayTop, UIInteraction.getCurrentColor()); - - var colorHalfwayRight = await UIInteraction.getPixelColor( - CanvasPosition.halfwayRight, - CanvasPosition.centerY, - ); - - expect(colorHalfwayRight, Colors.transparent); - - await UIInteraction.tapAt(CanvasPosition.halfBottomRight); - await UIInteraction.clickPlus(); - - colorHalfwayRight = await UIInteraction.getPixelColor( - CanvasPosition.halfwayRight, - CanvasPosition.centerY, - ); - - expect(colorHalfwayRight, UIInteraction.getCurrentColor()); - - var colorHalfwayBottom = await UIInteraction.getPixelColor( - CanvasPosition.centerX, - CanvasPosition.halfwayBottom, - ); - - expect(colorHalfwayBottom, Colors.transparent); - - await UIInteraction.tapAt(CanvasPosition.halfBottomLeft); - await UIInteraction.clickCheckmark(); - - colorHalfwayBottom = await UIInteraction.getPixelColor( - CanvasPosition.centerX, - CanvasPosition.halfwayBottom, - ); - - expect(colorHalfwayBottom, UIInteraction.getCurrentColor()); - }); - - testWidgets('[LINE_TOOL]: tapping away from last tap changes last line', - (WidgetTester tester) async { - UIInteraction.initialize(tester); - await tester.pumpWidget(sut); - await UIInteraction.createNewImage(); - await UIInteraction.selectTool(ToolData.LINE.name); - UIInteraction.setColor(Colors.black); - - var actualColor = await UIInteraction.getPixelColor( - CanvasPosition.left, - CanvasPosition.centerY, - ); - expect(actualColor, Colors.transparent); - - await UIInteraction.tapAt(CanvasPosition.topLeft); - await UIInteraction.clickPlus(); - - await UIInteraction.tapAt(CanvasPosition.bottomLeft); - - actualColor = await UIInteraction.getPixelColor( - CanvasPosition.left, - CanvasPosition.centerY, - ); - expect(actualColor, Colors.black); - - await UIInteraction.tapAt(CanvasPosition.topRight); - - actualColor = await UIInteraction.getPixelColor( - CanvasPosition.left, - CanvasPosition.centerY, - ); - expect(actualColor, Colors.transparent); - - actualColor = await UIInteraction.getPixelColor( - CanvasPosition.left, - CanvasPosition.centerY, - ); - expect(actualColor, Colors.transparent); - - actualColor = await UIInteraction.getPixelColor( - CanvasPosition.centerX, - CanvasPosition.top, - ); - expect(actualColor, Colors.black); - }); - - testWidgets('[LINE_TOOL]: moving vertices changes line position', - (WidgetTester tester) async { - UIInteraction.initialize(tester); - await tester.pumpWidget(sut); - await UIInteraction.createNewImage(); - await UIInteraction.selectTool(ToolData.LINE.name); - UIInteraction.setColor(Colors.black); - - var actualColor = await UIInteraction.getPixelColor( - CanvasPosition.halfwayLeft, - CanvasPosition.centerY, - ); - expect(actualColor, Colors.transparent); - - await UIInteraction.tapAt(CanvasPosition.halfTopLeft); - - await UIInteraction.tapAt(CanvasPosition.halfCenterLeft); - await UIInteraction.clickPlus(); - - await UIInteraction.tapAt(CanvasPosition.halfBottomLeft); - - actualColor = await UIInteraction.getPixelColor( - CanvasPosition.halfwayLeft, - CanvasPosition.centerY, - ); - - expect(actualColor, Colors.black); - - actualColor = await UIInteraction.getPixelColor( - CanvasPosition.halfwayRight, - CanvasPosition.centerY, - ); - - expect(actualColor, Colors.transparent); - - await UIInteraction.dragFromTo( - CanvasPosition.halfTopLeft, - CanvasPosition.halfTopRight, - ); - - await UIInteraction.dragFromTo( - CanvasPosition.halfCenterLeft, - CanvasPosition.halfCenterRight, - ); - - await UIInteraction.dragFromTo( - CanvasPosition.halfBottomLeft, - CanvasPosition.halfBottomRight, - ); - - actualColor = await UIInteraction.getPixelColor( - CanvasPosition.halfwayLeft, - CanvasPosition.centerY, - ); - - expect(actualColor, Colors.transparent); - - actualColor = await UIInteraction.getPixelColor( - CanvasPosition.halfwayRight, - CanvasPosition.centerY, - ); - - expect(actualColor, Colors.black); - }); - - testWidgets('[LINE_TOOL]: tapping vertex activates it for moving', - (WidgetTester tester) async { - UIInteraction.initialize(tester); - await tester.pumpWidget(sut); - await UIInteraction.createNewImage(); - await UIInteraction.selectTool(ToolData.LINE.name); - UIInteraction.setColor(Colors.black); - - var actualColor = await UIInteraction.getPixelColor( - CanvasPosition.halfwayLeft, - CanvasPosition.centerY, - ); - expect(actualColor, Colors.transparent); - - await UIInteraction.tapAt(CanvasPosition.halfTopLeft); - - await UIInteraction.tapAt(CanvasPosition.center); - await UIInteraction.clickPlus(); - - await UIInteraction.tapAt(CanvasPosition.halfBottomRight); - - await UIInteraction.tapAt(CanvasPosition.center); - - await UIInteraction.tapAt(CanvasPosition.halfCenterLeft); - - await UIInteraction.tapAt(CanvasPosition.halfBottomRight); - - await UIInteraction.tapAt(CanvasPosition.halfBottomLeft); - - actualColor = await UIInteraction.getPixelColor( - CanvasPosition.halfwayLeft, - CanvasPosition.centerY, - ); - expect(actualColor, Colors.black); - }); - - testWidgets('[LINE_TOOL]: clicking checkmark completes line', - (WidgetTester tester) async { - UIInteraction.initialize(tester); - await tester.pumpWidget(sut); - await UIInteraction.createNewImage(); - await UIInteraction.selectTool(ToolData.LINE.name); - - await UIInteraction.tapAt(CanvasPosition.halfTopLeft); - - await UIInteraction.tapAt(CanvasPosition.halfBottomLeft); - - await UIInteraction.clickCheckmark(); - - await UIInteraction.tapAt(CanvasPosition.halfBottomRight); - - await UIInteraction.tapAt(CanvasPosition.halfTopRight); - - await UIInteraction.clickCheckmark(); - - var actualColor = await UIInteraction.getPixelColor( - CanvasPosition.centerX, - CanvasPosition.halfwayBottom, - ); - expect(actualColor, Colors.transparent); - }); - - testWidgets( - '[LINE_TOOL]: undoing while not in active line sequence rebuilds ' - 'the old line sequence', (WidgetTester tester) async { - UIInteraction.initialize(tester); - await tester.pumpWidget(sut); - await UIInteraction.createNewImage(); - await UIInteraction.selectTool(ToolData.LINE.name); - - await UIInteraction.tapAt(CanvasPosition.centerLeft); - await UIInteraction.tapAt(CanvasPosition.center); - await UIInteraction.clickPlus(); - await UIInteraction.tapAt(CanvasPosition.centerRight); - - UIInteraction.expectVertexStackLength(3); - - await UIInteraction.clickCheckmark(); - UIInteraction.expectVertexStackLength(0); - - await UIInteraction.clickUndo(); - UIInteraction.expectVertexStackLength(3); - }); - - testWidgets( - '[LINE_TOOL]: undoing when only two vertices resets the' - 'current vertexStack', (WidgetTester tester) async { - UIInteraction.initialize(tester); - await tester.pumpWidget(sut); - await UIInteraction.createNewImage(); - await UIInteraction.selectTool(ToolData.LINE.name); - - await UIInteraction.tapAt(CanvasPosition.centerLeft); - await UIInteraction.tapAt(CanvasPosition.center); - - UIInteraction.expectVertexStackLength(2); - - await UIInteraction.clickUndo(); - UIInteraction.expectVertexStackLength(0); - }); - - testWidgets( - '[LINE_TOOL]: undoing after completing two line sequences' - 'rebuilds each line sequence separately', (WidgetTester tester) async { - UIInteraction.initialize(tester); - await tester.pumpWidget(sut); - await UIInteraction.createNewImage(); - await UIInteraction.selectTool(ToolData.LINE.name); - - await UIInteraction.tapAt(CanvasPosition.topLeft); - await UIInteraction.tapAt(CanvasPosition.topCenter); - await UIInteraction.clickPlus(); - await UIInteraction.tapAt(CanvasPosition.topRight); - await UIInteraction.clickPlus(); - await UIInteraction.tapAt(CanvasPosition.halfTopRight); - - UIInteraction.expectVertexStackLength(4); - - await UIInteraction.clickCheckmark(); - UIInteraction.expectVertexStackLength(0); - - await UIInteraction.tapAt(CanvasPosition.bottomLeft); - await UIInteraction.tapAt(CanvasPosition.bottomCenter); - await UIInteraction.clickPlus(); - await UIInteraction.tapAt(CanvasPosition.bottomRight); - - UIInteraction.expectVertexStackLength(3); - - await UIInteraction.clickCheckmark(); - UIInteraction.expectVertexStackLength(0); - - await UIInteraction.clickUndo(); - UIInteraction.expectVertexStackLength(3); - - await UIInteraction.clickUndo(); - UIInteraction.expectVertexStackLength(2); - - await UIInteraction.clickUndo(); - UIInteraction.expectVertexStackLength(0); - - await UIInteraction.clickUndo(); - UIInteraction.expectVertexStackLength(4); - }); - - testWidgets( - '[LINE_TOOL]: redoing when only two vertices restores the ' - 'current vertexStack', (WidgetTester tester) async { - UIInteraction.initialize(tester); - await tester.pumpWidget(sut); - await UIInteraction.createNewImage(); - await UIInteraction.selectTool(ToolData.LINE.name); - - await UIInteraction.tapAt(CanvasPosition.centerLeft); - await UIInteraction.tapAt(CanvasPosition.center); - - UIInteraction.expectVertexStackLength(2); - - await UIInteraction.clickUndo(); - UIInteraction.expectVertexStackLength(0); - - await UIInteraction.clickRedo(); - UIInteraction.expectVertexStackLength(2); - }); - - testWidgets('[LINE_TOOL]: redoing after undo restores the last undone action', - (WidgetTester tester) async { - UIInteraction.initialize(tester); - await tester.pumpWidget(sut); - await UIInteraction.createNewImage(); - await UIInteraction.selectTool(ToolData.LINE.name); - - await UIInteraction.tapAt(CanvasPosition.centerLeft); - await UIInteraction.tapAt(CanvasPosition.center); - await UIInteraction.clickPlus(); - await UIInteraction.tapAt(CanvasPosition.centerRight); - - UIInteraction.expectVertexStackLength(3); - - await UIInteraction.clickUndo(); - UIInteraction.expectVertexStackLength(2); - - await UIInteraction.clickRedo(); - UIInteraction.expectVertexStackLength(3); - }); - - testWidgets( - '[LINE_TOOL]: redoing after completing a line sequences ' - 'restores line sequence', (WidgetTester tester) async { - UIInteraction.initialize(tester); - await tester.pumpWidget(sut); - await UIInteraction.createNewImage(); - await UIInteraction.selectTool(ToolData.LINE.name); - - await UIInteraction.tapAt(CanvasPosition.centerLeft); - await UIInteraction.tapAt(CanvasPosition.center); - await UIInteraction.clickPlus(); - await UIInteraction.tapAt(CanvasPosition.centerRight); - - await UIInteraction.clickCheckmark(); - - await UIInteraction.clickUndo(times: 3); - - await UIInteraction.clickRedo(); - UIInteraction.expectVertexStackLength(2); - - await UIInteraction.clickRedo(); - UIInteraction.expectVertexStackLength(3); - }); - - testWidgets( - '[LINE_TOOL]: redoing after completing two line sequences ' - 'restores each line sequence separately', (WidgetTester tester) async { - UIInteraction.initialize(tester); - await tester.pumpWidget(sut); - await UIInteraction.createNewImage(); - await UIInteraction.selectTool(ToolData.LINE.name); - - await UIInteraction.tapAt(CanvasPosition.topLeft); - await UIInteraction.tapAt(CanvasPosition.topCenter); - await UIInteraction.clickPlus(); - await UIInteraction.tapAt(CanvasPosition.topRight); - await UIInteraction.clickPlus(); - await UIInteraction.tapAt(CanvasPosition.halfTopRight); - - await UIInteraction.clickCheckmark(); - - await UIInteraction.tapAt(CanvasPosition.bottomLeft); - await UIInteraction.tapAt(CanvasPosition.bottomCenter); - await UIInteraction.clickPlus(); - await UIInteraction.tapAt(CanvasPosition.bottomRight); - - await UIInteraction.clickCheckmark(); - UIInteraction.expectVertexStackLength(0); - - await UIInteraction.clickUndo(times: 6); - - await UIInteraction.clickRedo(); - UIInteraction.expectVertexStackLength(2); - - await UIInteraction.clickRedo(); - UIInteraction.expectVertexStackLength(3); - - await UIInteraction.clickRedo(); - UIInteraction.expectVertexStackLength(4); - - await UIInteraction.clickRedo(); - UIInteraction.expectVertexStackLength(2); - - await UIInteraction.clickRedo(); - UIInteraction.expectVertexStackLength(3); - }); -} diff --git a/test/integration/command_manager_test.dart b/test/integration/command_manager_test.dart new file mode 100644 index 00000000..264961a5 --- /dev/null +++ b/test/integration/command_manager_test.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:paintroid/app.dart'; +import 'package:paintroid/core/tools/tool_data.dart'; + +import '../utils/test_utils.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + const String testIDStr = String.fromEnvironment('id', defaultValue: '-1'); + final testID = int.tryParse(testIDStr) ?? testIDStr; + + late Widget sut; + + setUp(() async { + sut = ProviderScope( + child: App( + showOnboardingPage: false, + ), + ); + }); + + group('[COMMAND_MANAGER]', () { + if (testID == -1 || testID == 0) { + testWidgets('Test if tool changes during undo', + (WidgetTester tester) async { + UIInteraction.initialize(tester); + await tester.pumpWidget(sut); + await UIInteraction.createNewImage(); + await UIInteraction.selectTool(ToolData.BRUSH.name); + await UIInteraction.tapAt(CanvasPosition.topLeft); + + await UIInteraction.selectTool(ToolData.LINE.name); + await UIInteraction.tapAt(CanvasPosition.bottomLeft); + await UIInteraction.tapAt(CanvasPosition.bottomRight); + + await UIInteraction.selectTool(ToolData.BRUSH.name); + await UIInteraction.tapAt(CanvasPosition.topRight); + + await UIInteraction.selectTool(ToolData.LINE.name); + + var currentTool = UIInteraction.getCurrentTool(); + expect(currentTool.type, ToolData.LINE.type); + + await UIInteraction.clickUndo(); + currentTool = UIInteraction.getCurrentTool(); + expect(currentTool.type, ToolData.BRUSH.type); + + await UIInteraction.clickUndo(); + currentTool = UIInteraction.getCurrentTool(); + expect(currentTool.type, ToolData.LINE.type); + + await UIInteraction.clickUndo(); + await UIInteraction.clickUndo(); + currentTool = UIInteraction.getCurrentTool(); + expect(currentTool.type, ToolData.BRUSH.type); + }); + } + + if (testID == -1 || testID == 1) { + testWidgets('Test if tool changes during redo', + (WidgetTester tester) async { + UIInteraction.initialize(tester); + await tester.pumpWidget(sut); + await UIInteraction.createNewImage(); + await UIInteraction.selectTool(ToolData.BRUSH.name); + await UIInteraction.tapAt(CanvasPosition.topLeft); + + await UIInteraction.selectTool(ToolData.LINE.name); + await UIInteraction.tapAt(CanvasPosition.bottomLeft); + await UIInteraction.tapAt(CanvasPosition.bottomRight); + + await UIInteraction.selectTool(ToolData.BRUSH.name); + await UIInteraction.tapAt(CanvasPosition.topRight); + + await UIInteraction.clickUndo(times: 3); + + var currentTool = UIInteraction.getCurrentTool(); + expect(currentTool.type, ToolData.BRUSH.type); + + await UIInteraction.clickRedo(); + currentTool = UIInteraction.getCurrentTool(); + expect(currentTool.type, ToolData.BRUSH.type); + + await UIInteraction.clickRedo(); + currentTool = UIInteraction.getCurrentTool(); + expect(currentTool.type, ToolData.LINE.type); + + await UIInteraction.clickRedo(); + currentTool = UIInteraction.getCurrentTool(); + expect(currentTool.type, ToolData.BRUSH.type); + }); + } + }); +} diff --git a/test/integration/driver/driver.dart b/test/integration/driver/driver.dart new file mode 100644 index 00000000..9b4268e1 --- /dev/null +++ b/test/integration/driver/driver.dart @@ -0,0 +1,3 @@ +import 'package:integration_test/integration_test_driver_extended.dart'; + +Future main() => integrationDriver(); diff --git a/test/integration/line_tool_test.dart b/test/integration/line_tool_test.dart new file mode 100644 index 00000000..4678d0d2 --- /dev/null +++ b/test/integration/line_tool_test.dart @@ -0,0 +1,613 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:paintroid/app.dart'; +import 'package:paintroid/core/tools/tool_data.dart'; + +import '../utils/test_utils.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + const String testIDStr = String.fromEnvironment('id', defaultValue: '-1'); + final testID = int.tryParse(testIDStr) ?? testIDStr; + + late Widget sut; + + setUp(() async { + sut = ProviderScope( + child: App( + showOnboardingPage: false, + ), + ); + }); + + if (testID == -1 || testID == 0) { + testWidgets('[LINE_TOOL]: test line on top', (WidgetTester tester) async { + UIInteraction.initialize(tester); + await tester.pumpWidget(sut); + await UIInteraction.createNewImage(); + await UIInteraction.selectTool(ToolData.LINE.name); + + var colorTopCenter = await UIInteraction.getPixelColor( + CanvasPosition.centerX, + CanvasPosition.top, + ); + expect(colorTopCenter, Colors.transparent); + + await UIInteraction.tapAt(CanvasPosition.topLeft); + + await UIInteraction.tapAt(CanvasPosition.topRight); + + await UIInteraction.clickCheckmark(); + + colorTopCenter = await UIInteraction.getPixelColor( + CanvasPosition.centerX, + CanvasPosition.top, + ); + + expect(colorTopCenter, UIInteraction.getCurrentColor()); + }); + } + + if (testID == -1 || testID == 1) { + testWidgets('[LINE_TOOL]: test line on bottom', + (WidgetTester tester) async { + UIInteraction.initialize(tester); + await tester.pumpWidget(sut); + await UIInteraction.createNewImage(); + await UIInteraction.selectTool(ToolData.LINE.name); + + var colorBottomCenter = await UIInteraction.getPixelColor( + CanvasPosition.centerX, + CanvasPosition.bottom, + ); + expect(colorBottomCenter, Colors.transparent); + + await UIInteraction.tapAt(CanvasPosition.bottomLeft); + + await UIInteraction.tapAt(CanvasPosition.bottomRight); + + await UIInteraction.clickCheckmark(); + + colorBottomCenter = await UIInteraction.getPixelColor( + CanvasPosition.centerX, + CanvasPosition.bottom, + ); + + expect(colorBottomCenter, UIInteraction.getCurrentColor()); + }); + } + + if (testID == -1 || testID == 2) { + testWidgets('[LINE_TOOL]: test vertical line', (WidgetTester tester) async { + UIInteraction.initialize(tester); + await tester.pumpWidget(sut); + await UIInteraction.createNewImage(); + await UIInteraction.selectTool(ToolData.LINE.name); + + final colorBefore = await UIInteraction.getPixelColor( + CanvasPosition.centerX, + CanvasPosition.centerY, + ); + expect(colorBefore, Colors.transparent); + + await UIInteraction.tapAt(CanvasPosition.topCenter); + + await UIInteraction.tapAt(CanvasPosition.bottomCenter); + + await UIInteraction.clickCheckmark(); + final colorAfter = await UIInteraction.getPixelColor( + CanvasPosition.centerX, + CanvasPosition.centerY, + ); + + expect(colorAfter, UIInteraction.getCurrentColor()); + }); + } + + if (testID == -1 || testID == 3) { + testWidgets('[LINE_TOOL]: test horizontal line', + (WidgetTester tester) async { + UIInteraction.initialize(tester); + await tester.pumpWidget(sut); + await UIInteraction.createNewImage(); + await UIInteraction.selectTool(ToolData.LINE.name); + + final colorBefore = await UIInteraction.getPixelColor( + CanvasPosition.centerX, + CanvasPosition.centerY, + ); + expect(colorBefore, Colors.transparent); + + await UIInteraction.tapAt(CanvasPosition.centerLeft); + + await UIInteraction.tapAt(CanvasPosition.centerRight); + + await UIInteraction.clickCheckmark(); + final colorAfter = await UIInteraction.getPixelColor( + CanvasPosition.centerX, + CanvasPosition.centerY, + ); + + expect(colorAfter, UIInteraction.getCurrentColor()); + }); + } + + if (testID == -1 || testID == 4) { + testWidgets('[LINE_TOOL]: test diagonal line', (WidgetTester tester) async { + UIInteraction.initialize(tester); + await tester.pumpWidget(sut); + await UIInteraction.createNewImage(); + await UIInteraction.selectTool(ToolData.LINE.name); + + final colorBefore = await UIInteraction.getPixelColor( + CanvasPosition.centerX, + CanvasPosition.centerY, + ); + expect(colorBefore, Colors.transparent); + + await UIInteraction.tapAt(CanvasPosition.topLeft); + + await UIInteraction.tapAt(CanvasPosition.bottomRight); + + await UIInteraction.clickCheckmark(); + final colorAfter = await UIInteraction.getPixelColor( + CanvasPosition.centerX, + CanvasPosition.centerY, + ); + + expect(colorAfter, UIInteraction.getCurrentColor()); + }); + } + + if (testID == -1 || testID == 5) { + testWidgets('[LINE_TOOL]: test multiple added lines', + (WidgetTester tester) async { + UIInteraction.initialize(tester); + await tester.pumpWidget(sut); + await UIInteraction.createNewImage(); + await UIInteraction.selectTool(ToolData.LINE.name); + + var colorHalfwayTop = await UIInteraction.getPixelColor( + CanvasPosition.centerX, + CanvasPosition.halfwayTop, + ); + expect(colorHalfwayTop, Colors.transparent); + + await UIInteraction.tapAt(CanvasPosition.halfTopLeft); + await UIInteraction.clickPlus(); + + await UIInteraction.tapAt(CanvasPosition.halfTopRight); + await UIInteraction.clickPlus(); + + colorHalfwayTop = await UIInteraction.getPixelColor( + CanvasPosition.centerX, + CanvasPosition.halfwayTop, + ); + + expect(colorHalfwayTop, UIInteraction.getCurrentColor()); + + var colorHalfwayRight = await UIInteraction.getPixelColor( + CanvasPosition.halfwayRight, + CanvasPosition.centerY, + ); + + expect(colorHalfwayRight, Colors.transparent); + + await UIInteraction.tapAt(CanvasPosition.halfBottomRight); + await UIInteraction.clickPlus(); + + colorHalfwayRight = await UIInteraction.getPixelColor( + CanvasPosition.halfwayRight, + CanvasPosition.centerY, + ); + + expect(colorHalfwayRight, UIInteraction.getCurrentColor()); + + var colorHalfwayBottom = await UIInteraction.getPixelColor( + CanvasPosition.centerX, + CanvasPosition.halfwayBottom, + ); + + expect(colorHalfwayBottom, Colors.transparent); + + await UIInteraction.tapAt(CanvasPosition.halfBottomLeft); + await UIInteraction.clickCheckmark(); + + colorHalfwayBottom = await UIInteraction.getPixelColor( + CanvasPosition.centerX, + CanvasPosition.halfwayBottom, + ); + + expect(colorHalfwayBottom, UIInteraction.getCurrentColor()); + }); + } + + if (testID == -1 || testID == 6) { + testWidgets('[LINE_TOOL]: tapping away from last tap changes last line', + (WidgetTester tester) async { + UIInteraction.initialize(tester); + await tester.pumpWidget(sut); + await UIInteraction.createNewImage(); + await UIInteraction.selectTool(ToolData.LINE.name); + UIInteraction.setColor(Colors.black); + + var actualColor = await UIInteraction.getPixelColor( + CanvasPosition.left, + CanvasPosition.centerY, + ); + expect(actualColor, Colors.transparent); + + await UIInteraction.tapAt(CanvasPosition.topLeft); + await UIInteraction.clickPlus(); + + await UIInteraction.tapAt(CanvasPosition.bottomLeft); + + actualColor = await UIInteraction.getPixelColor( + CanvasPosition.left, + CanvasPosition.centerY, + ); + expect(actualColor, Colors.black); + + await UIInteraction.tapAt(CanvasPosition.topRight); + + actualColor = await UIInteraction.getPixelColor( + CanvasPosition.left, + CanvasPosition.centerY, + ); + expect(actualColor, Colors.transparent); + + actualColor = await UIInteraction.getPixelColor( + CanvasPosition.left, + CanvasPosition.centerY, + ); + expect(actualColor, Colors.transparent); + + actualColor = await UIInteraction.getPixelColor( + CanvasPosition.centerX, + CanvasPosition.top, + ); + expect(actualColor, Colors.black); + }); + } + + if (testID == -1 || testID == 7) { + testWidgets('[LINE_TOOL]: moving vertices changes line position', + (WidgetTester tester) async { + UIInteraction.initialize(tester); + await tester.pumpWidget(sut); + await UIInteraction.createNewImage(); + await UIInteraction.selectTool(ToolData.LINE.name); + UIInteraction.setColor(Colors.black); + + var actualColor = await UIInteraction.getPixelColor( + CanvasPosition.halfwayLeft, + CanvasPosition.centerY, + ); + expect(actualColor, Colors.transparent); + + await UIInteraction.tapAt(CanvasPosition.halfTopLeft); + + await UIInteraction.tapAt(CanvasPosition.halfCenterLeft); + await UIInteraction.clickPlus(); + + await UIInteraction.tapAt(CanvasPosition.halfBottomLeft); + + actualColor = await UIInteraction.getPixelColor( + CanvasPosition.halfwayLeft, + CanvasPosition.centerY, + ); + + expect(actualColor, Colors.black); + + actualColor = await UIInteraction.getPixelColor( + CanvasPosition.halfwayRight, + CanvasPosition.centerY, + ); + + expect(actualColor, Colors.transparent); + + await UIInteraction.dragFromTo( + CanvasPosition.halfTopLeft, + CanvasPosition.halfTopRight, + ); + + await UIInteraction.dragFromTo( + CanvasPosition.halfCenterLeft, + CanvasPosition.halfCenterRight, + ); + + await UIInteraction.dragFromTo( + CanvasPosition.halfBottomLeft, + CanvasPosition.halfBottomRight, + ); + + actualColor = await UIInteraction.getPixelColor( + CanvasPosition.halfwayLeft, + CanvasPosition.centerY, + ); + + expect(actualColor, Colors.transparent); + + actualColor = await UIInteraction.getPixelColor( + CanvasPosition.halfwayRight, + CanvasPosition.centerY, + ); + + expect(actualColor, Colors.black); + }); + } + + if (testID == -1 || testID == 8) { + testWidgets('[LINE_TOOL]: tapping vertex activates it for moving', + (WidgetTester tester) async { + UIInteraction.initialize(tester); + await tester.pumpWidget(sut); + await UIInteraction.createNewImage(); + await UIInteraction.selectTool(ToolData.LINE.name); + UIInteraction.setColor(Colors.black); + + var actualColor = await UIInteraction.getPixelColor( + CanvasPosition.halfwayLeft, + CanvasPosition.centerY, + ); + expect(actualColor, Colors.transparent); + + await UIInteraction.tapAt(CanvasPosition.halfTopLeft); + + await UIInteraction.tapAt(CanvasPosition.center); + await UIInteraction.clickPlus(); + + await UIInteraction.tapAt(CanvasPosition.halfBottomRight); + + await UIInteraction.tapAt(CanvasPosition.center); + + await UIInteraction.tapAt(CanvasPosition.halfCenterLeft); + + await UIInteraction.tapAt(CanvasPosition.halfBottomRight); + + await UIInteraction.tapAt(CanvasPosition.halfBottomLeft); + + actualColor = await UIInteraction.getPixelColor( + CanvasPosition.halfwayLeft, + CanvasPosition.centerY, + ); + expect(actualColor, Colors.black); + }); + } + + if (testID == -1 || testID == 9) { + testWidgets('[LINE_TOOL]: clicking checkmark completes line', + (WidgetTester tester) async { + UIInteraction.initialize(tester); + await tester.pumpWidget(sut); + await UIInteraction.createNewImage(); + await UIInteraction.selectTool(ToolData.LINE.name); + + await UIInteraction.tapAt(CanvasPosition.halfTopLeft); + + await UIInteraction.tapAt(CanvasPosition.halfBottomLeft); + + await UIInteraction.clickCheckmark(); + + await UIInteraction.tapAt(CanvasPosition.halfBottomRight); + + await UIInteraction.tapAt(CanvasPosition.halfTopRight); + + await UIInteraction.clickCheckmark(); + + var actualColor = await UIInteraction.getPixelColor( + CanvasPosition.centerX, + CanvasPosition.halfwayBottom, + ); + expect(actualColor, Colors.transparent); + }); + } + + if (testID == -1 || testID == 10) { + testWidgets( + '[LINE_TOOL]: undoing while not in active line sequence rebuilds ' + 'the old line sequence', (WidgetTester tester) async { + UIInteraction.initialize(tester); + await tester.pumpWidget(sut); + await UIInteraction.createNewImage(); + await UIInteraction.selectTool(ToolData.LINE.name); + + await UIInteraction.tapAt(CanvasPosition.centerLeft); + await UIInteraction.tapAt(CanvasPosition.center); + await UIInteraction.clickPlus(); + await UIInteraction.tapAt(CanvasPosition.centerRight); + + UIInteraction.expectVertexStackLength(3); + + await UIInteraction.clickCheckmark(); + UIInteraction.expectVertexStackLength(0); + + await UIInteraction.clickUndo(); + UIInteraction.expectVertexStackLength(3); + }); + } + + if (testID == -1 || testID == 11) { + testWidgets( + '[LINE_TOOL]: undoing when only two vertices resets the' + 'current vertexStack', (WidgetTester tester) async { + UIInteraction.initialize(tester); + await tester.pumpWidget(sut); + await UIInteraction.createNewImage(); + await UIInteraction.selectTool(ToolData.LINE.name); + + await UIInteraction.tapAt(CanvasPosition.centerLeft); + await UIInteraction.tapAt(CanvasPosition.center); + + UIInteraction.expectVertexStackLength(2); + + await UIInteraction.clickUndo(); + UIInteraction.expectVertexStackLength(0); + }); + } + + if (testID == -1 || testID == 12) { + testWidgets( + '[LINE_TOOL]: undoing after completing two line sequences' + 'rebuilds each line sequence separately', (WidgetTester tester) async { + UIInteraction.initialize(tester); + await tester.pumpWidget(sut); + await UIInteraction.createNewImage(); + await UIInteraction.selectTool(ToolData.LINE.name); + + await UIInteraction.tapAt(CanvasPosition.topLeft); + await UIInteraction.tapAt(CanvasPosition.topCenter); + await UIInteraction.clickPlus(); + await UIInteraction.tapAt(CanvasPosition.topRight); + await UIInteraction.clickPlus(); + await UIInteraction.tapAt(CanvasPosition.halfTopRight); + + UIInteraction.expectVertexStackLength(4); + + await UIInteraction.clickCheckmark(); + UIInteraction.expectVertexStackLength(0); + + await UIInteraction.tapAt(CanvasPosition.bottomLeft); + await UIInteraction.tapAt(CanvasPosition.bottomCenter); + await UIInteraction.clickPlus(); + await UIInteraction.tapAt(CanvasPosition.bottomRight); + + UIInteraction.expectVertexStackLength(3); + + await UIInteraction.clickCheckmark(); + UIInteraction.expectVertexStackLength(0); + + await UIInteraction.clickUndo(); + UIInteraction.expectVertexStackLength(3); + + await UIInteraction.clickUndo(); + UIInteraction.expectVertexStackLength(2); + + await UIInteraction.clickUndo(); + UIInteraction.expectVertexStackLength(0); + + await UIInteraction.clickUndo(); + UIInteraction.expectVertexStackLength(4); + }); + } + + if (testID == -1 || testID == 13) { + testWidgets( + '[LINE_TOOL]: redoing when only two vertices restores the ' + 'current vertexStack', (WidgetTester tester) async { + UIInteraction.initialize(tester); + await tester.pumpWidget(sut); + await UIInteraction.createNewImage(); + await UIInteraction.selectTool(ToolData.LINE.name); + + await UIInteraction.tapAt(CanvasPosition.centerLeft); + await UIInteraction.tapAt(CanvasPosition.center); + + UIInteraction.expectVertexStackLength(2); + + await UIInteraction.clickUndo(); + UIInteraction.expectVertexStackLength(0); + + await UIInteraction.clickRedo(); + UIInteraction.expectVertexStackLength(2); + }); + } + + if (testID == -1 || testID == 14) { + testWidgets( + '[LINE_TOOL]: redoing after undo restores the last undone action', + (WidgetTester tester) async { + UIInteraction.initialize(tester); + await tester.pumpWidget(sut); + await UIInteraction.createNewImage(); + await UIInteraction.selectTool(ToolData.LINE.name); + + await UIInteraction.tapAt(CanvasPosition.centerLeft); + await UIInteraction.tapAt(CanvasPosition.center); + await UIInteraction.clickPlus(); + await UIInteraction.tapAt(CanvasPosition.centerRight); + + UIInteraction.expectVertexStackLength(3); + + await UIInteraction.clickUndo(); + UIInteraction.expectVertexStackLength(2); + + await UIInteraction.clickRedo(); + UIInteraction.expectVertexStackLength(3); + }); + } + + if (testID == -1 || testID == 15) { + testWidgets( + '[LINE_TOOL]: redoing after completing a line sequences ' + 'restores line sequence', (WidgetTester tester) async { + UIInteraction.initialize(tester); + await tester.pumpWidget(sut); + await UIInteraction.createNewImage(); + await UIInteraction.selectTool(ToolData.LINE.name); + + await UIInteraction.tapAt(CanvasPosition.centerLeft); + await UIInteraction.tapAt(CanvasPosition.center); + await UIInteraction.clickPlus(); + await UIInteraction.tapAt(CanvasPosition.centerRight); + + await UIInteraction.clickCheckmark(); + + await UIInteraction.clickUndo(times: 3); + + await UIInteraction.clickRedo(); + UIInteraction.expectVertexStackLength(2); + + await UIInteraction.clickRedo(); + UIInteraction.expectVertexStackLength(3); + }); + } + + if (testID == -1 || testID == 16) { + testWidgets( + '[LINE_TOOL]: redoing after completing two line sequences ' + 'restores each line sequence separately', (WidgetTester tester) async { + UIInteraction.initialize(tester); + await tester.pumpWidget(sut); + await UIInteraction.createNewImage(); + await UIInteraction.selectTool(ToolData.LINE.name); + + await UIInteraction.tapAt(CanvasPosition.topLeft); + await UIInteraction.tapAt(CanvasPosition.topCenter); + await UIInteraction.clickPlus(); + await UIInteraction.tapAt(CanvasPosition.topRight); + await UIInteraction.clickPlus(); + await UIInteraction.tapAt(CanvasPosition.halfTopRight); + + await UIInteraction.clickCheckmark(); + + await UIInteraction.tapAt(CanvasPosition.bottomLeft); + await UIInteraction.tapAt(CanvasPosition.bottomCenter); + await UIInteraction.clickPlus(); + await UIInteraction.tapAt(CanvasPosition.bottomRight); + + await UIInteraction.clickCheckmark(); + UIInteraction.expectVertexStackLength(0); + + await UIInteraction.clickUndo(times: 6); + + await UIInteraction.clickRedo(); + UIInteraction.expectVertexStackLength(2); + + await UIInteraction.clickRedo(); + UIInteraction.expectVertexStackLength(3); + + await UIInteraction.clickRedo(); + UIInteraction.expectVertexStackLength(4); + + await UIInteraction.clickRedo(); + UIInteraction.expectVertexStackLength(2); + + await UIInteraction.clickRedo(); + UIInteraction.expectVertexStackLength(3); + }); + } +} diff --git a/test/unit/tools/line_tool_test.dart b/test/unit/tools/line_tool_test.dart index 6a6a05b4..6b033795 100644 --- a/test/unit/tools/line_tool_test.dart +++ b/test/unit/tools/line_tool_test.dart @@ -1,7 +1,6 @@ import 'dart:ui'; import 'package:flutter_test/flutter_test.dart'; - import 'package:paintroid/core/commands/command_factory/command_factory.dart'; import 'package:paintroid/core/commands/command_manager/command_manager.dart'; import 'package:paintroid/core/commands/graphic_factory/graphic_factory.dart'; @@ -27,116 +26,114 @@ void main() { ); }); - group('On tap down event', () { - test('VertexStack should have two vertices after first click', () { - expect(sut.vertexStack.length, 0); - sut.onDown(pointA, paint); - expect(sut.vertexStack.length, 2); - }); + test('VertexStack should have two vertices after first click', () { + expect(sut.vertexStack.length, 0); + sut.onDown(pointA, paint); + expect(sut.vertexStack.length, 2); + }); - test('VertexStack should have three vertices after second click', () { - expect(sut.vertexStack.length, 0); - sut.onDown(pointA, paint); - sut.onUp(pointB, paint); - expect(sut.vertexStack.length, 2); - sut.onPlus(); - sut.onDown(pointC, paint); - expect(sut.vertexStack.length, 3); - }); + test('VertexStack should have three vertices after second click', () { + expect(sut.vertexStack.length, 0); + sut.onDown(pointA, paint); + sut.onUp(pointB, paint); + expect(sut.vertexStack.length, 2); + sut.onPlus(); + sut.onDown(pointC, paint); + expect(sut.vertexStack.length, 3); + }); - test('VertexStack should have four vertices after third click', () { - expect(sut.vertexStack.length, 0); - sut.onDown(pointA, paint); - sut.onUp(pointB, paint); - expect(sut.vertexStack.length, 2); - sut.onPlus(); - sut.onDown(pointC, paint); - expect(sut.vertexStack.length, 3); - sut.onPlus(); - sut.onDown(pointD, paint); - expect(sut.vertexStack.length, 4); - }); + test('VertexStack should have four vertices after third click', () { + expect(sut.vertexStack.length, 0); + sut.onDown(pointA, paint); + sut.onUp(pointB, paint); + expect(sut.vertexStack.length, 2); + sut.onPlus(); + sut.onDown(pointC, paint); + expect(sut.vertexStack.length, 3); + sut.onPlus(); + sut.onDown(pointD, paint); + expect(sut.vertexStack.length, 4); + }); - test('VertexStack resets after clicking checkmark', () { - expect(sut.vertexStack.length, 0); - sut.onDown(pointA, paint); - sut.onUp(pointB, paint); - expect(sut.vertexStack.length, 2); - sut.onCheckmark(); - expect(sut.vertexStack.length, 0); - }); + test('VertexStack resets after clicking checkmark', () { + expect(sut.vertexStack.length, 0); + sut.onDown(pointA, paint); + sut.onUp(pointB, paint); + expect(sut.vertexStack.length, 2); + sut.onCheckmark(); + expect(sut.vertexStack.length, 0); + }); - test('AddNewPath is true after click plus', () { - expect(sut.addNewPath, false); - sut.onDown(pointA, paint); - sut.onUp(pointB, paint); - sut.onPlus(); - expect(sut.addNewPath, true); - }); + test('AddNewPath is true after click plus', () { + expect(sut.addNewPath, false); + sut.onDown(pointA, paint); + sut.onUp(pointB, paint); + sut.onPlus(); + expect(sut.addNewPath, true); + }); - test('Last vertex is set to movingVertex after click plus', () { - sut.onDown(pointA, paint); - sut.onUp(pointB, paint); - expect(sut.movingVertex, sut.vertexStack.last); - sut.onPlus(); - sut.onDown(pointC, paint); - sut.onUp(pointC, paint); - expect(sut.movingVertex, sut.vertexStack.last); - sut.onPlus(); - sut.onDown(pointD, paint); - sut.onUp(pointD, paint); - expect(sut.movingVertex, sut.vertexStack.last); - }); + test('Last vertex is set to movingVertex after click plus', () { + sut.onDown(pointA, paint); + sut.onUp(pointB, paint); + expect(sut.movingVertex, sut.vertexStack.last); + sut.onPlus(); + sut.onDown(pointC, paint); + sut.onUp(pointC, paint); + expect(sut.movingVertex, sut.vertexStack.last); + sut.onPlus(); + sut.onDown(pointD, paint); + sut.onUp(pointD, paint); + expect(sut.movingVertex, sut.vertexStack.last); + }); - test('Click on first vertex sets it to movingVertex', () { - sut.onDown(pointA, paint); - sut.onUp(pointB, paint); - expect(sut.movingVertex, sut.vertexStack.last); - sut.onPlus(); - sut.onDown(pointC, paint); - sut.onUp(pointC, paint); - expect(sut.movingVertex, sut.vertexStack.last); - sut.onDown(pointA, paint); - sut.onUp(pointA, paint); - expect(sut.movingVertex, sut.vertexStack.first); - }); + test('Click on first vertex sets it to movingVertex', () { + sut.onDown(pointA, paint); + sut.onUp(pointB, paint); + expect(sut.movingVertex, sut.vertexStack.last); + sut.onPlus(); + sut.onDown(pointC, paint); + sut.onUp(pointC, paint); + expect(sut.movingVertex, sut.vertexStack.last); + sut.onDown(pointA, paint); + sut.onUp(pointA, paint); + expect(sut.movingVertex, sut.vertexStack.first); + }); - test('Click on middle vertex sets it to movingVertex', () { - sut.onDown(pointA, paint); - sut.onUp(pointB, paint); - expect(sut.movingVertex, sut.vertexStack.last); - sut.onPlus(); - sut.onDown(pointC, paint); - sut.onUp(pointC, paint); - expect(sut.movingVertex, sut.vertexStack.last); - sut.onDown(pointB, paint); - sut.onUp(pointB, paint); - expect(sut.movingVertex, sut.vertexStack.elementAt(1)); - }); + test('Click on middle vertex sets it to movingVertex', () { + sut.onDown(pointA, paint); + sut.onUp(pointB, paint); + expect(sut.movingVertex, sut.vertexStack.last); + sut.onPlus(); + sut.onDown(pointC, paint); + sut.onUp(pointC, paint); + expect(sut.movingVertex, sut.vertexStack.last); + sut.onDown(pointB, paint); + sut.onUp(pointB, paint); + expect(sut.movingVertex, sut.vertexStack.elementAt(1)); + }); - test('Moving a vertex changes its vertexCenter', () { - sut.onDown(pointA, paint); - sut.onUp(pointB, paint); - sut.onPlus(); - sut.onDown(pointC, paint); - sut.onUp(pointC, paint); - expect(sut.vertexStack.first.vertexCenter, pointA); - sut.onDown(pointA, paint); - sut.onDrag(pointD, paint); - sut.onUp(pointD, paint); - expect(sut.vertexStack.first.vertexCenter, pointD); - }); + test('Moving a vertex changes its vertexCenter', () { + sut.onDown(pointA, paint); + sut.onUp(pointB, paint); + sut.onPlus(); + sut.onDown(pointC, paint); + sut.onUp(pointC, paint); + expect(sut.vertexStack.first.vertexCenter, pointA); + sut.onDown(pointA, paint); + sut.onDrag(pointD, paint); + sut.onUp(pointD, paint); + expect(sut.vertexStack.first.vertexCenter, pointD); + }); - test('Last vertexCenter changes after clicking somewhere else', () { - sut.onDown(pointA, paint); - sut.onUp(pointB, paint); - sut.onPlus(); - sut.onDown(pointC, paint); - sut.onUp(pointC, paint); - expect(sut.vertexStack.last.vertexCenter, pointC); - sut.onDown(pointD, paint); - sut.onUp(pointD, paint); - expect(sut.vertexStack.last.vertexCenter, pointD); - }); + test('Last vertexCenter changes after clicking somewhere else', () { + sut.onDown(pointA, paint); + sut.onUp(pointB, paint); + sut.onPlus(); + sut.onDown(pointC, paint); + sut.onUp(pointC, paint); + expect(sut.vertexStack.last.vertexCenter, pointC); + sut.onDown(pointD, paint); + sut.onUp(pointD, paint); + expect(sut.vertexStack.last.vertexCenter, pointD); }); } From ce8b821e2317067b1f0ba83c31dbc97489c831cc Mon Sep 17 00:00:00 2001 From: Tim Celec Date: Fri, 12 Jul 2024 09:25:14 +0200 Subject: [PATCH 02/25] PAINTROID-761 - Added some functionalityy --- android/app/build.gradle | 1 + .../org/catrobat/paintroid/MainActivity.kt | 43 +++++++++++++++++++ .../graphic/path_command.dart | 2 +- .../converter/paint_converter.dart | 6 ++- lib/core/models/catrobat_image.dart | 2 +- .../object/load_image_from_file_manager.dart | 21 +++++++++ 6 files changed, 72 insertions(+), 3 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 6db03ec8..c4df110f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -50,4 +50,5 @@ flutter { dependencies { implementation "androidx.window:window:1.0.0" + implementation 'com.esotericsoftware:kryo:5.5.0' } diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/MainActivity.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/MainActivity.kt index 60ac0235..e7695a35 100644 --- a/android/app/src/main/kotlin/org/catrobat/paintroid/MainActivity.kt +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/MainActivity.kt @@ -14,7 +14,14 @@ import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import java.io.IOException + + +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output + class MainActivity : FlutterActivity() { + private val kryo = Kryo() private val hasWritePermission: Boolean get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q || ContextCompat.checkSelfPermission( @@ -72,6 +79,42 @@ class MainActivity : FlutterActivity() { } } + // TODO getHeightInPixels + private fun openOldProject(flutterEngine: FlutterEngine) { + MethodChannel( + flutterEngine.dartExecutor.binaryMessenger, "org.catrobat.paintroid/native" + ).apply { + setMethodCallHandler { call, result -> + when (call.method) { + "getNativeClassData" -> { + val parameter = call.argument("path") ?: "" + if (parameter == "") + { + /* result.error( + "PERMISSION_DENIED", + "User explicitly denied WRITE_EXTERNAL_STORAGE permission", + null + ) + return@setMethodCallHandler + */ + } + else + { + val t = "error"; + } + result.success(null); + + // return result.error("NO PARAM"); + //val stream = FileInputStream(temporaryFilePath) + //this.kryo.get + // result.success("should work"); + } + else -> result.notImplemented() + } + } + } + } + private fun saveImageToPictures(filename: String, data: ByteArray) { val picturesUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) diff --git a/lib/core/commands/command_implementation/graphic/path_command.dart b/lib/core/commands/command_implementation/graphic/path_command.dart index a26fff85..57dd899a 100644 --- a/lib/core/commands/command_implementation/graphic/path_command.dart +++ b/lib/core/commands/command_implementation/graphic/path_command.dart @@ -41,7 +41,7 @@ class PathCommand extends GraphicCommand { Map toJson() => _$PathCommandToJson(this); factory PathCommand.fromJson(Map json) { - int version = json['version'] as int; + int version = json['version'] as int; // TODO switch (version) { case Version.v1: diff --git a/lib/core/json_serialization/converter/paint_converter.dart b/lib/core/json_serialization/converter/paint_converter.dart index 5a41ef4f..220b63c3 100644 --- a/lib/core/json_serialization/converter/paint_converter.dart +++ b/lib/core/json_serialization/converter/paint_converter.dart @@ -21,8 +21,12 @@ class PaintConverter implements JsonConverter> { paint.strokeJoin = StrokeJoin.values[json['strokeJoin']]; paint.blendMode = BlendMode.values[json['blendMode']]; } - if (version >= Version.v2) { + if (version == Version.v2) { // paint.newAttribute = json['newAttribute']; + } + if ( version == Version.v3) + { + } return paint; } diff --git a/lib/core/models/catrobat_image.dart b/lib/core/models/catrobat_image.dart index 4d0a3232..02350c3e 100644 --- a/lib/core/models/catrobat_image.dart +++ b/lib/core/models/catrobat_image.dart @@ -25,7 +25,7 @@ class CatrobatImage { this.backgroundImage, { int? version, this.magicValue = 'CATROBAT', - }) : version = version ?? + }) : version = version ?? // here should be 3 VersionStrategyManager.strategy.getCatrobatImageVersion(); Uint8List toBytes() { diff --git a/lib/core/providers/object/load_image_from_file_manager.dart b/lib/core/providers/object/load_image_from_file_manager.dart index 849224ad..8476ed47 100644 --- a/lib/core/providers/object/load_image_from_file_manager.dart +++ b/lib/core/providers/object/load_image_from_file_manager.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'dart:typed_data'; import 'dart:ui'; +import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:oxidized/oxidized.dart'; @@ -60,6 +61,16 @@ class LoadImageFromFileManager with LoggableMixin { .map((img) => ImageFromFile.rasterImage(img)); case 'catrobat-image': Uint8List bytes = await file.readAsBytes(); + // check for json + // if json + // file.path + var fileValidity = checkJson(bytes); + if(!fileValidity) + { + // call kyrostatic + const methodChannel = MethodChannel('org.catrobat.paintroid/native'); + final ByteData result = await methodChannel.invokeMethod('getNativeClassData', {'path': file.uri}); + } CatrobatImage catrobatImage = CatrobatImage.fromBytes(bytes); Image? backgroundImage = await rebuildBackgroundImage(catrobatImage); @@ -79,6 +90,16 @@ class LoadImageFromFileManager with LoggableMixin { } }); } + bool checkJson(Uint8List bytes) + { + try { + String jsonString = utf8.decode(bytes); + Map jsonMap = json.decode(jsonString); + return true; + } catch (e) { + return false; + } + } Future rebuildBackgroundImage(CatrobatImage catrobatImage) async { if (catrobatImage.backgroundImage.isNotEmpty) { From d317eb0456152a2e4e583fc70e69666f7fc28ef0 Mon Sep 17 00:00:00 2001 From: Tim Celec Date: Fri, 12 Jul 2024 09:25:46 +0200 Subject: [PATCH 03/25] PAINTROID-761 - Added some native provider --- .../object/native_catrobat_service.dart | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 lib/core/providers/object/native_catrobat_service.dart diff --git a/lib/core/providers/object/native_catrobat_service.dart b/lib/core/providers/object/native_catrobat_service.dart new file mode 100644 index 00000000..b112867a --- /dev/null +++ b/lib/core/providers/object/native_catrobat_service.dart @@ -0,0 +1,43 @@ +import 'dart:io'; +import 'package:flutter/services.dart'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; + + +abstract class INativeCatrobatService{ + Future getNativeClassData(String parameter); + + static final provider = Provider((ref) + { + const channel= MethodChannel('org.catrobat.paintroid/native'); + return NativeCatrobatService(channel); + }); +} + + + + + +class NativeCatrobatService implements INativeCatrobatService { + NativeCatrobatService(this._methodChannel); + + final MethodChannel _methodChannel; + @override + Future getNativeClassData(String parameter) async { + if(Platform.isAndroid) { + try { + final ByteData data = await _methodChannel.invokeMethod('getNativeClassData', {'path': parameter}); + return data; + } catch (e) { + print('Failed to get native class data: $e'); + throw e; // Re-throw to allow caller to handle the exception. + } + } + throw UnimplementedError(); + } + +} + + + + From f4c6c65d2bd6a2a9fbd1f689debee12246acbfed Mon Sep 17 00:00:00 2001 From: Tim Celec Date: Fri, 12 Jul 2024 10:26:41 +0200 Subject: [PATCH 04/25] PAINTROID-761 - Native call works --- .../org/catrobat/paintroid/MainActivity.kt | 65 +++++++++++-------- .../object/load_image_from_file_manager.dart | 14 ++-- .../object/native_catrobat_service.dart | 3 +- 3 files changed, 48 insertions(+), 34 deletions(-) diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/MainActivity.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/MainActivity.kt index e7695a35..3b05fe9f 100644 --- a/android/app/src/main/kotlin/org/catrobat/paintroid/MainActivity.kt +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/MainActivity.kt @@ -13,13 +13,12 @@ import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import java.io.IOException - - +import android.util.Log import com.esotericsoftware.kryo.Kryo import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output - +import java.nio.ByteBuffer class MainActivity : FlutterActivity() { private val kryo = Kryo() private val hasWritePermission: Boolean @@ -33,6 +32,7 @@ class MainActivity : FlutterActivity() { super.configureFlutterEngine(flutterEngine) setupPhotoLibraryChannel(flutterEngine) setupDeviceChannel(flutterEngine) + setupNativeCatrobat(flutterEngine) } private fun setupDeviceChannel(flutterEngine: FlutterEngine) { @@ -80,36 +80,37 @@ class MainActivity : FlutterActivity() { } // TODO getHeightInPixels - private fun openOldProject(flutterEngine: FlutterEngine) { - MethodChannel( - flutterEngine.dartExecutor.binaryMessenger, "org.catrobat.paintroid/native" - ).apply { + private fun setupNativeCatrobat(flutterEngine: FlutterEngine) { + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "org.catrobat.paintroid/native").apply { setMethodCallHandler { call, result -> when (call.method) { "getNativeClassData" -> { - val parameter = call.argument("path") ?: "" - if (parameter == "") - { - /* result.error( - "PERMISSION_DENIED", - "User explicitly denied WRITE_EXTERNAL_STORAGE permission", - null - ) - return@setMethodCallHandler - */ - } - else - { - val t = "error"; + val parameter = call.argument("path") + if (parameter == null) { + Log.d("MethodChannel", "No path received") + result.error("NO_PARAM", "No path provided", null) + } else { + try { + //Log.d("MethodChannel", "Received path: $parameter") + //val data = getNativeClassData(parameter) + val byteBuffer = ByteBuffer.allocate(4) // Allocate a ByteBuffer with space for 4 bytes + byteBuffer.put(0x01) // Example byte + byteBuffer.put(0x02) // Example byte + byteBuffer.put(0x03) // Example byte + byteBuffer.put(0x04) // Example byte + val byteArray = ByteArray(byteBuffer.remaining()) + byteBuffer.get(byteArray) + result.success(byteArray) + } catch (e: Exception) { + Log.e("MethodChannel", "Error processing data", e) + result.error("ERROR_PROCESSING", "Failed to process data", e.localizedMessage) + } } - result.success(null); - - // return result.error("NO PARAM"); - //val stream = FileInputStream(temporaryFilePath) - //this.kryo.get - // result.success("should work"); } - else -> result.notImplemented() + else -> { + Log.d("MethodChannel", "Method not implemented") + result.notImplemented() + } } } } @@ -152,3 +153,11 @@ class MainActivity : FlutterActivity() { return Pair(filename, imageData) } } + + + +private fun getNativeClassData(path: String): ByteArray { + // Assuming you have a method to process the data, e.g., using Kryo for serialization + // Replace with actual data processing logic + return byteArrayOf() // Placeholder for actual data processing +} \ No newline at end of file diff --git a/lib/core/providers/object/load_image_from_file_manager.dart b/lib/core/providers/object/load_image_from_file_manager.dart index 8476ed47..14b4f5f8 100644 --- a/lib/core/providers/object/load_image_from_file_manager.dart +++ b/lib/core/providers/object/load_image_from_file_manager.dart @@ -17,6 +17,8 @@ import 'package:paintroid/core/utils/failure.dart'; import 'package:paintroid/core/utils/load_image_failure.dart'; import 'package:paintroid/core/utils/save_image_failure.dart'; +import 'native_catrobat_service.dart'; + extension on File { String? get extension { final list = path.split('.'); @@ -29,16 +31,18 @@ class LoadImageFromFileManager with LoggableMixin { final IFileService fileService; final IImageService imageService; final IPermissionService permissionService; + final INativeCatrobatService nativeCatrobatService; LoadImageFromFileManager( - this.fileService, this.imageService, this.permissionService); + this.fileService, this.imageService, this.permissionService, this.nativeCatrobatService); static final provider = Provider((ref) { final imageService = ref.watch(IImageService.provider); final fileService = ref.watch(IFileService.provider); final permissionService = ref.watch(IPermissionService.provider); + final nativeService = ref.watch(INativeCatrobatService.provider); return LoadImageFromFileManager( - fileService, imageService, permissionService); + fileService, imageService, permissionService,nativeService); }); Future> call( @@ -67,9 +71,9 @@ class LoadImageFromFileManager with LoggableMixin { var fileValidity = checkJson(bytes); if(!fileValidity) { - // call kyrostatic - const methodChannel = MethodChannel('org.catrobat.paintroid/native'); - final ByteData result = await methodChannel.invokeMethod('getNativeClassData', {'path': file.uri}); + final ByteData result = await nativeCatrobatService.getNativeClassData(file.uri.path); + var t = 10; + return const Result.err(LoadImageFailure.invalidImage); } CatrobatImage catrobatImage = CatrobatImage.fromBytes(bytes); Image? backgroundImage = diff --git a/lib/core/providers/object/native_catrobat_service.dart b/lib/core/providers/object/native_catrobat_service.dart index b112867a..46bad400 100644 --- a/lib/core/providers/object/native_catrobat_service.dart +++ b/lib/core/providers/object/native_catrobat_service.dart @@ -26,7 +26,8 @@ class NativeCatrobatService implements INativeCatrobatService { Future getNativeClassData(String parameter) async { if(Platform.isAndroid) { try { - final ByteData data = await _methodChannel.invokeMethod('getNativeClassData', {'path': parameter}); + final Uint8List bytes = await _methodChannel.invokeMethod('getNativeClassData', {'path': parameter}); + final ByteData data = ByteData.view(bytes.buffer); return data; } catch (e) { print('Failed to get native class data: $e'); From 36e4c4fec984311db045d9fb891479e939515c83 Mon Sep 17 00:00:00 2001 From: Tim Celec Date: Fri, 12 Jul 2024 15:57:23 +0200 Subject: [PATCH 05/25] PAINTROID-761 - Command works --- .../org/catrobat/paintroid/FileReader.kt | 85 ++++++++++ .../org/catrobat/paintroid/command/Command.kt | 28 ++++ .../serialization/VersionSerializer.kt | 56 +++++++ .../paintroid/contract/LayerContracts.kt | 155 ++++++++++++++++++ .../org/catrobat/paintroid/model/Layer.kt | 32 ++++ .../res/drawable/pocketpaint_checkeredbg.png | Bin 0 -> 86 bytes android/app/src/main/res/values/colors.xml | 27 +++ 7 files changed, 383 insertions(+) create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/Command.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/VersionSerializer.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/contract/LayerContracts.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/model/Layer.kt create mode 100644 android/app/src/main/res/drawable/pocketpaint_checkeredbg.png diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt new file mode 100644 index 00000000..78bf5d43 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt @@ -0,0 +1,85 @@ +package org.catrobat.paintroid + +import android.content.Context +import android.net.Uri +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import java.io.InputStream +import org.catrobat.paintroid.model.Layer + +import org.catrobat.paintroid.command.serialization.VersionSerializer +import org.catrobat.paintroid.command.Command +class FileReader(private val context : Context) +{ + private val kryo = Kryo() + private val registerMap = LinkedHashMap, VersionSerializer<*>?>() + companion object { + const val MAGIC_VALUE = "CatrobatImg" + const val CURRENT_IMAGE_VERSION = 2 // handle 1 look up how to do it in the native verson + } + init { + setRegisterMapVersion(CURRENT_IMAGE_VERSION) + // registerClasses() + } + /* fun readFromFile(uri: String): CatrobatFileContent{ + var commandModel: CommandManagerModel + var colorHistory: ColorHistory? = null + }*/ + private fun setRegisterMapVersion(version: Int) { + // Only add new classes at the end + // because Kryo assigns an ID to each class + with(registerMap) { + put(Command::class.java, null) + /*put(CompositeCommand::class.java, CompositeCommandSerializer(version)) + put(FloatArray::class.java, DataStructuresSerializer.FloatArraySerializer(version)) + put(PointF::class.java, DataStructuresSerializer.PointFSerializer(version)) + put(Point::class.java, DataStructuresSerializer.PointSerializer(version)) + put(CommandManagerModel::class.java, CommandManagerModelSerializer(version)) + put(SetDimensionCommand::class.java, SetDimensionCommandSerializer(version)) + put(SprayCommand::class.java, SprayCommandSerializer(version)) + put(Paint::class.java, PaintSerializer(version, activityContext)) + put(AddEmptyLayerCommand::class.java, AddLayerCommandSerializer(version)) + put(SelectLayerCommand::class.java, SelectLayerCommandSerializer(version)) + put(LoadCommand::class.java, LoadCommandSerializer(version)) + put(TextToolCommand::class.java, TextToolCommandSerializer(version, activityContext)) + put(Array::class.java, DataStructuresSerializer.StringArraySerializer(version)) + put(FillCommand::class.java, FillCommandSerializer(version)) + put(FlipCommand::class.java, FlipCommandSerializer(version)) + put(CropCommand::class.java, CropCommandSerializer(version)) + put(CutCommand::class.java, CutCommandSerializer(version)) + put(ResizeCommand::class.java, ResizeCommandSerializer(version)) + put(RotateCommand::class.java, RotateCommandSerializer(version)) + put(ResetCommand::class.java, ResetCommandSerializer(version)) + put(ReorderLayersCommand::class.java, ReorderLayersCommandSerializer(version)) + put(RemoveLayerCommand::class.java, RemoveLayerCommandSerializer(version)) + put(MergeLayersCommand::class.java, MergeLayersCommandSerializer(version)) + put(PathCommand::class.java, PathCommandSerializer(version)) + put(SerializablePath::class.java, SerializablePath.PathSerializer(version)) + put(SerializablePath.Move::class.java, SerializablePath.PathActionMoveSerializer(version)) + put(SerializablePath.Line::class.java, SerializablePath.PathActionLineSerializer(version)) + put(SerializablePath.Quad::class.java, SerializablePath.PathActionQuadSerializer(version)) + put(SerializablePath.Rewind::class.java, SerializablePath.PathActionRewindSerializer(version)) + put(LoadLayerListCommand::class.java, LoadLayerListCommandSerializer(version)) + put(GeometricFillCommand::class.java, GeometricFillCommandSerializer(version)) + put(HeartDrawable::class.java, GeometricFillCommandSerializer.HeartDrawableSerializer(version)) + put(OvalDrawable::class.java, GeometricFillCommandSerializer.OvalDrawableSerializer(version)) + put(RectangleDrawable::class.java, GeometricFillCommandSerializer.RectangleDrawableSerializer(version)) + put(StarDrawable::class.java, GeometricFillCommandSerializer.StarDrawableSerializer(version)) + put(ShapeDrawable::class.java, null) + put(RectF::class.java, DataStructuresSerializer.RectFSerializer(version)) + put(ClipboardCommand::class.java, ClipboardCommandSerializer(version)) + put(SerializableTypeface::class.java, SerializableTypeface.TypefaceSerializer(version)) + put(PointCommand::class.java, PointCommandSerializer(version)) + put(SerializablePath.Cube::class.java, SerializablePath.PathActionCubeSerializer(version)) + put(Bitmap::class.java, BitmapSerializer(version)) + put(SmudgePathCommand::class.java, SmudgePathCommandSerializer(version)) + put(ColorHistory::class.java, ColorHistorySerializer(version)) + put(ClippingCommand::class.java, ClippingCommandSerializer(version)) + put(LayerOpacityCommand::class.java, LayerOpacityCommandSerializer(version)) + */ } + } + + + +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/Command.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/Command.kt new file mode 100644 index 00000000..bae630b5 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/Command.kt @@ -0,0 +1,28 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.paintroid.command + +import android.graphics.Canvas +import org.catrobat.paintroid.contract.LayerContracts + +interface Command { + fun run(canvas: Canvas, layerModel: LayerContracts.Model) + fun freeResources() +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/VersionSerializer.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/VersionSerializer.kt new file mode 100644 index 00000000..fac0c552 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/VersionSerializer.kt @@ -0,0 +1,56 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.serialization + +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.KryoException +import com.esotericsoftware.kryo.Serializer +import com.esotericsoftware.kryo.io.Input + +abstract class VersionSerializer(val version: Int) : Serializer() { + + companion object { + private const val V1 = 1 + private const val V2 = 2 + private const val V3 = 3 + } + + protected fun handleVersions(serializer: VersionSerializer, kryo: Kryo, input: Input, type: Class): T { + return when (version) { + // Currently just here to see the intended pattern + V1 -> serializer.readV1(serializer, kryo, input, type) + V2 -> serializer.readV2(serializer, kryo, input, type) + V3 -> serializer.readV3(serializer, kryo, input, type) + // Enable when CURRENT_IMAGE_VERSION reached 4 + // CommandSerializationUtilities.CURRENT_IMAGE_VERSION -> serializer.readCurrentVersion(kryo, input, type) + else -> throw KryoException() + } + } + + protected open fun readV1(serializer: VersionSerializer, kryo: Kryo, input: Input, type: Class): T = + serializer.readV2(serializer, kryo, input, type) + + protected open fun readV2(serializer: VersionSerializer, kryo: Kryo, input: Input, type: Class): T = + serializer.readV3(serializer, kryo, input, type) + + protected open fun readV3(serializer: VersionSerializer, kryo: Kryo, input: Input, type: Class): T = + serializer.readCurrentVersion(kryo, input, type) + + abstract fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): T +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/contract/LayerContracts.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/contract/LayerContracts.kt new file mode 100644 index 00000000..99a2b86e --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/contract/LayerContracts.kt @@ -0,0 +1,155 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.contract + +import android.graphics.Bitmap +import android.view.View +import android.widget.LinearLayout +import androidx.annotation.StringRes + + +interface LayerContracts { + /* interface Adapter { + fun notifyDataSetChanged() + + fun getViewHolderAt(position: Int): LayerViewHolder? + } + + interface Presenter { + val layerCount: Int + val presenter: Presenter + + fun onSelectedLayerInvisible() + + fun onSelectedLayerVisible() + + fun getListItemDragHandler(): ListItemDragHandler + + fun refreshLayerMenuViewHolder() + + fun disableVisibilityAndOpacityButtons() + + fun getLayerItem(position: Int): Layer? + + fun getLayerItemId(position: Int): Long + + fun addLayer() + + fun removeLayer() + + fun changeLayerOpacity(position: Int, opacityPercentage: Int) + + fun setLayerVisibility(position: Int, isVisible: Boolean) + + fun refreshDrawingSurface() + + fun setAdapter(layerAdapter: Adapter) + + fun setDrawingSurface(drawingSurface: DrawingSurface) + + fun invalidate() + + fun setDefaultToolController(defaultToolController: DefaultToolController) + + fun setBottomNavigationViewHolder(bottomNavigationViewHolder: BottomNavigationViewHolder) + + fun isShown(): Boolean + + fun onStartDragging(position: Int, view: View) + + fun onStopDragging() + + fun setLayerSelected(position: Int) + + fun getSelectedLayer(): Layer? + } + + interface LayerViewHolder { + val bitmap: Bitmap? + val view: View + + fun setSelected(isSelected: Boolean) + + fun updateImageView(layer: Layer) + + fun setMergable() + + fun isSelected(): Boolean + + fun getViewLayout(): LinearLayout + + fun bindView() + + fun setLayerVisibilityCheckbox(setTo: Boolean) + } + + interface LayerMenuViewHolder { + fun disableAddLayerButton() + + fun enableAddLayerButton() + + fun disableRemoveLayerButton() + + fun enableRemoveLayerButton() + + fun disableLayerOpacityButton() + + fun disableLayerVisibilityButton() + + fun isShown(): Boolean + } +*/ + interface Layer { + var bitmap: Bitmap + var isVisible: Boolean + var opacityPercentage: Int + + fun getValueForOpacityPercentage(): Int + } + + interface Model { + val layers: List + var currentLayer: Layer? + var width: Int + var height: Int + val layerCount: Int + + fun reset() + + fun getLayerAt(index: Int): Layer? + + fun getLayerIndexOf(layer: Layer): Int + + fun addLayerAt(index: Int, layer: Layer): Boolean + + fun listIterator(index: Int): ListIterator + + fun setLayerAt(position: Int, layer: Layer) + + fun removeLayerAt(position: Int): Boolean + + fun getBitmapOfAllLayers(): Bitmap? + + fun getBitmapListOfAllLayers(): List + } + + interface Navigator { + fun showToast(@StringRes id: Int, length: Int) + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/model/Layer.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/model/Layer.kt new file mode 100644 index 00000000..acb8c356 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/model/Layer.kt @@ -0,0 +1,32 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.model + +import android.graphics.Bitmap +import org.catrobat.paintroid.contract.LayerContracts + +const val MAX_LAYER_OPACITY_PERCENTAGE = 100 +const val MAX_LAYER_OPACITY_VALUE = 255 + +open class Layer(override var bitmap: Bitmap) : LayerContracts.Layer { + override var isVisible: Boolean = true + override var opacityPercentage: Int = MAX_LAYER_OPACITY_PERCENTAGE + + override fun getValueForOpacityPercentage(): Int = (opacityPercentage.toFloat() / MAX_LAYER_OPACITY_PERCENTAGE * MAX_LAYER_OPACITY_VALUE).toInt() +} diff --git a/android/app/src/main/res/drawable/pocketpaint_checkeredbg.png b/android/app/src/main/res/drawable/pocketpaint_checkeredbg.png new file mode 100644 index 0000000000000000000000000000000000000000..d31b4a8fc64e8a1e1788273df5044ff9cabf832e GIT binary patch literal 86 zcmeAS@N?(olHy`uVBq!ia0vp^Y#_`5Bp8k^-!KJ8$$GjthHzX@wmEPh;rxLEM-Ch~ jz_486zp;Ugi#Q8|=L@D&DH1K8K^i<={an^LB{Ts5lhqjz literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml index e4f6f88a..d58b68f2 100644 --- a/android/app/src/main/res/values/colors.xml +++ b/android/app/src/main/res/values/colors.xml @@ -1,4 +1,31 @@ #bff8fb + + #138293 + #0D6775 + #157DA2 + #99157DA2 + #BD5800 + + + #c8e8ff + #007A99 + #00000000 + #80000000 + #D3D3D3 + #99555555 + #9933B5E5 + #99E56B33 + #555555 + + + #ffffffff + #64ffffff + + #DFDADA + + #33ac86 + + #33B5E5 \ No newline at end of file From d7dea37359abe01d316766c91805ed74bf6a2d82 Mon Sep 17 00:00:00 2001 From: Tim Celec Date: Fri, 12 Jul 2024 16:10:14 +0200 Subject: [PATCH 06/25] PAINTROID-761 - CompositeCommand works --- .../org/catrobat/paintroid/FileReader.kt | 6 ++- .../implementation/CompositeCommand.kt | 47 +++++++++++++++++++ .../CompositeCommandSerializer.kt | 46 ++++++++++++++++++ 3 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/CompositeCommand.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/CompositeCommandSerializer.kt diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt index 78bf5d43..1ccf8f1c 100644 --- a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt @@ -10,6 +10,8 @@ import org.catrobat.paintroid.model.Layer import org.catrobat.paintroid.command.serialization.VersionSerializer import org.catrobat.paintroid.command.Command +import org.catrobat.paintroid.command.implementation.CompositeCommand +import org.catrobat.paintroid.command.serialization.CompositeCommandSerializer class FileReader(private val context : Context) { private val kryo = Kryo() @@ -31,8 +33,8 @@ class FileReader(private val context : Context) // because Kryo assigns an ID to each class with(registerMap) { put(Command::class.java, null) - /*put(CompositeCommand::class.java, CompositeCommandSerializer(version)) - put(FloatArray::class.java, DataStructuresSerializer.FloatArraySerializer(version)) + put(CompositeCommand::class.java, CompositeCommandSerializer(version)) + /* put(FloatArray::class.java, DataStructuresSerializer.FloatArraySerializer(version)) put(PointF::class.java, DataStructuresSerializer.PointFSerializer(version)) put(Point::class.java, DataStructuresSerializer.PointSerializer(version)) put(CommandManagerModel::class.java, CommandManagerModelSerializer(version)) diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/CompositeCommand.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/CompositeCommand.kt new file mode 100644 index 00000000..76b7b0c1 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/CompositeCommand.kt @@ -0,0 +1,47 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.implementation + +import android.graphics.Canvas +import org.catrobat.paintroid.command.Command +import org.catrobat.paintroid.contract.LayerContracts + +class CompositeCommand : Command { + + var commands = mutableListOf(); private set + + fun addCommand(command: Command) { + commands.add(command) + } + + override fun run(canvas: Canvas, layerModel: LayerContracts.Model) { + commands.forEach { command -> + layerModel.currentLayer?.let { layer -> + canvas.setBitmap(layer.bitmap) + } + command.run(canvas, layerModel) + } + } + + override fun freeResources() { + commands.forEach { command -> + command.freeResources() + } + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/CompositeCommandSerializer.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/CompositeCommandSerializer.kt new file mode 100644 index 00000000..74c69450 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/CompositeCommandSerializer.kt @@ -0,0 +1,46 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.serialization + +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import org.catrobat.paintroid.command.Command +import org.catrobat.paintroid.command.implementation.CompositeCommand + +class CompositeCommandSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, command: CompositeCommand) { + output.writeInt(command.commands.size) + command.commands.forEach { cmd -> + kryo.writeClassAndObject(output, cmd) + } + } + + override fun read(kryo: Kryo, input: Input, type: Class): CompositeCommand = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): CompositeCommand { + val size = input.readInt() + return CompositeCommand().apply { + repeat(size) { + addCommand(kryo.readClassAndObject(input) as Command) + } + } + } +} From ad54183413caa1d9bf8486c010163189a6d1a86f Mon Sep 17 00:00:00 2001 From: Tim Celec Date: Fri, 12 Jul 2024 16:20:41 +0200 Subject: [PATCH 07/25] PAINTROID-761 - FloatArray works --- .../org/catrobat/paintroid/FileReader.kt | 5 +- .../serialization/DataStructuresSerializer.kt | 134 ++++++++++++++++++ 2 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/DataStructuresSerializer.kt diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt index 1ccf8f1c..9fcdf085 100644 --- a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt @@ -12,6 +12,7 @@ import org.catrobat.paintroid.command.serialization.VersionSerializer import org.catrobat.paintroid.command.Command import org.catrobat.paintroid.command.implementation.CompositeCommand import org.catrobat.paintroid.command.serialization.CompositeCommandSerializer +import org.catrobat.paintroid.command.serialization.DataStructuresSerializer class FileReader(private val context : Context) { private val kryo = Kryo() @@ -34,8 +35,8 @@ class FileReader(private val context : Context) with(registerMap) { put(Command::class.java, null) put(CompositeCommand::class.java, CompositeCommandSerializer(version)) - /* put(FloatArray::class.java, DataStructuresSerializer.FloatArraySerializer(version)) - put(PointF::class.java, DataStructuresSerializer.PointFSerializer(version)) + put(FloatArray::class.java, DataStructuresSerializer.FloatArraySerializer(version)) + /* put(PointF::class.java, DataStructuresSerializer.PointFSerializer(version)) put(Point::class.java, DataStructuresSerializer.PointSerializer(version)) put(CommandManagerModel::class.java, CommandManagerModelSerializer(version)) put(SetDimensionCommand::class.java, SetDimensionCommandSerializer(version)) diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/DataStructuresSerializer.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/DataStructuresSerializer.kt new file mode 100644 index 00000000..2886c436 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/DataStructuresSerializer.kt @@ -0,0 +1,134 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.serialization + +import android.graphics.Point +import android.graphics.PointF +import android.graphics.RectF +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output + +class DataStructuresSerializer { + class FloatArraySerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, array: FloatArray) { + with(output) { + writeInt(array.size) + array.forEach { floatValue -> + writeFloat(floatValue) + } + } + } + + override fun read(kryo: Kryo, input: Input, type: Class): FloatArray = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): FloatArray { + return with(input) { + val size = readInt() + val floatList = ArrayList() + repeat(size) { + floatList.add(readFloat()) + } + floatList.toFloatArray() + } + } + } + + class StringArraySerializer(version: Int) : VersionSerializer>(version) { + override fun write(kryo: Kryo, output: Output, array: Array) { + with(output) { + writeInt(array.size) + array.forEach { str -> + writeString(str) + } + } + } + + override fun read(kryo: Kryo, input: Input, type: Class>): Array = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class>): Array { + return with(input) { + val size = readInt() + val strList = ArrayList() + repeat(size) { + strList.add(readString()) + } + strList.toTypedArray() + } + } + } + + class RectFSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, rect: RectF) { + with(output) { + writeFloat(rect.left) + writeFloat(rect.top) + writeFloat(rect.right) + writeFloat(rect.bottom) + } + } + + override fun read(kryo: Kryo, input: Input, type: Class): RectF = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): RectF { + return with(input) { + RectF(readFloat(), readFloat(), readFloat(), readFloat()) + } + } + } + + class PointFSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, rect: PointF) { + with(output) { + writeFloat(rect.x) + writeFloat(rect.y) + } + } + + override fun read(kryo: Kryo, input: Input, type: Class): PointF = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): PointF { + return with(input) { + PointF(readFloat(), readFloat()) + } + } + } + + class PointSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, rect: Point) { + with(output) { + writeInt(rect.x) + writeInt(rect.y) + } + } + + override fun read(kryo: Kryo, input: Input, type: Class): Point = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): Point { + return with(input) { + Point(readInt(), readInt()) + } + } + } +} From 352e51df9280a805b7d3645b51736874c6093f9c Mon Sep 17 00:00:00 2001 From: Tim Celec Date: Fri, 12 Jul 2024 16:22:07 +0200 Subject: [PATCH 08/25] PAINTROID-761 - Point/PointF works --- .../src/main/kotlin/org/catrobat/paintroid/FileReader.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt index 9fcdf085..98dcb574 100644 --- a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt @@ -13,6 +13,10 @@ import org.catrobat.paintroid.command.Command import org.catrobat.paintroid.command.implementation.CompositeCommand import org.catrobat.paintroid.command.serialization.CompositeCommandSerializer import org.catrobat.paintroid.command.serialization.DataStructuresSerializer + + +import android.graphics.Point +import android.graphics.PointF class FileReader(private val context : Context) { private val kryo = Kryo() @@ -36,9 +40,9 @@ class FileReader(private val context : Context) put(Command::class.java, null) put(CompositeCommand::class.java, CompositeCommandSerializer(version)) put(FloatArray::class.java, DataStructuresSerializer.FloatArraySerializer(version)) - /* put(PointF::class.java, DataStructuresSerializer.PointFSerializer(version)) + put(PointF::class.java, DataStructuresSerializer.PointFSerializer(version)) put(Point::class.java, DataStructuresSerializer.PointSerializer(version)) - put(CommandManagerModel::class.java, CommandManagerModelSerializer(version)) + /* put(CommandManagerModel::class.java, CommandManagerModelSerializer(version)) put(SetDimensionCommand::class.java, SetDimensionCommandSerializer(version)) put(SprayCommand::class.java, SprayCommandSerializer(version)) put(Paint::class.java, PaintSerializer(version, activityContext)) From c66913af285e924b532c16ce34215a7a8f27ae24 Mon Sep 17 00:00:00 2001 From: Tim Celec Date: Fri, 12 Jul 2024 16:31:10 +0200 Subject: [PATCH 09/25] PAINTROID-761 - CommandManagerModel works --- .../org/catrobat/paintroid/FileReader.kt | 7 ++- .../CommandManagerModelSerializer.kt | 52 +++++++++++++++++++ .../paintroid/model/CommandManagerModel.kt | 5 ++ 3 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/CommandManagerModelSerializer.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/model/CommandManagerModel.kt diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt index 98dcb574..9776f9fa 100644 --- a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt @@ -13,6 +13,9 @@ import org.catrobat.paintroid.command.Command import org.catrobat.paintroid.command.implementation.CompositeCommand import org.catrobat.paintroid.command.serialization.CompositeCommandSerializer import org.catrobat.paintroid.command.serialization.DataStructuresSerializer +import org.catrobat.paintroid.model.CommandManagerModel +import org.catrobat.paintroid.command.serialization.CommandManagerModelSerializer + import android.graphics.Point @@ -42,8 +45,8 @@ class FileReader(private val context : Context) put(FloatArray::class.java, DataStructuresSerializer.FloatArraySerializer(version)) put(PointF::class.java, DataStructuresSerializer.PointFSerializer(version)) put(Point::class.java, DataStructuresSerializer.PointSerializer(version)) - /* put(CommandManagerModel::class.java, CommandManagerModelSerializer(version)) - put(SetDimensionCommand::class.java, SetDimensionCommandSerializer(version)) + put(CommandManagerModel::class.java, CommandManagerModelSerializer(version)) + /* put(SetDimensionCommand::class.java, SetDimensionCommandSerializer(version)) put(SprayCommand::class.java, SprayCommandSerializer(version)) put(Paint::class.java, PaintSerializer(version, activityContext)) put(AddEmptyLayerCommand::class.java, AddLayerCommandSerializer(version)) diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/CommandManagerModelSerializer.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/CommandManagerModelSerializer.kt new file mode 100644 index 00000000..65326c65 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/CommandManagerModelSerializer.kt @@ -0,0 +1,52 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.serialization + +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import org.catrobat.paintroid.command.Command +import org.catrobat.paintroid.model.CommandManagerModel + +class CommandManagerModelSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, model: CommandManagerModel) { + with(kryo) { + writeClassAndObject(output, model.initialCommand) + output.writeInt(model.commands.size) + model.commands.forEach { command -> + writeClassAndObject(output, command) + } + } + } + + override fun read(kryo: Kryo, input: Input, type: Class): CommandManagerModel = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): CommandManagerModel { + return with(kryo) { + val initCommand = kryo.readClassAndObject(input) as Command + val size = input.readInt() + val commandList = ArrayList() + repeat(size) { + commandList.add(kryo.readClassAndObject(input) as Command) + } + CommandManagerModel(initCommand, commandList) + } + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/model/CommandManagerModel.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/model/CommandManagerModel.kt new file mode 100644 index 00000000..b19c23b8 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/model/CommandManagerModel.kt @@ -0,0 +1,5 @@ +package org.catrobat.paintroid.model + +import org.catrobat.paintroid.command.Command + +data class CommandManagerModel(val initialCommand: Command, val commands: MutableList) From 313a262c403240e1a58107f82590659e243075f0 Mon Sep 17 00:00:00 2001 From: Tim Celec Date: Fri, 12 Jul 2024 16:35:01 +0200 Subject: [PATCH 10/25] PAINTROID-761 - SetDimensionCommand works --- .../org/catrobat/paintroid/FileReader.kt | 7 +-- .../implementation/SetDimensionCommand.kt | 39 ++++++++++++++++ .../SetDimensionCommandSerializer.kt | 44 +++++++++++++++++++ 3 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/SetDimensionCommand.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/SetDimensionCommandSerializer.kt diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt index 9776f9fa..41ba3ddc 100644 --- a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt @@ -15,7 +15,8 @@ import org.catrobat.paintroid.command.serialization.CompositeCommandSerializer import org.catrobat.paintroid.command.serialization.DataStructuresSerializer import org.catrobat.paintroid.model.CommandManagerModel import org.catrobat.paintroid.command.serialization.CommandManagerModelSerializer - +import org.catrobat.paintroid.command.implementation.SetDimensionCommand +import org.catrobat.paintroid.command.serialization.SetDimensionCommandSerializer import android.graphics.Point @@ -46,8 +47,8 @@ class FileReader(private val context : Context) put(PointF::class.java, DataStructuresSerializer.PointFSerializer(version)) put(Point::class.java, DataStructuresSerializer.PointSerializer(version)) put(CommandManagerModel::class.java, CommandManagerModelSerializer(version)) - /* put(SetDimensionCommand::class.java, SetDimensionCommandSerializer(version)) - put(SprayCommand::class.java, SprayCommandSerializer(version)) + put(SetDimensionCommand::class.java, SetDimensionCommandSerializer(version)) + /* put(SprayCommand::class.java, SprayCommandSerializer(version)) put(Paint::class.java, PaintSerializer(version, activityContext)) put(AddEmptyLayerCommand::class.java, AddLayerCommandSerializer(version)) put(SelectLayerCommand::class.java, SelectLayerCommandSerializer(version)) diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/SetDimensionCommand.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/SetDimensionCommand.kt new file mode 100644 index 00000000..be8ee3c0 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/SetDimensionCommand.kt @@ -0,0 +1,39 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.paintroid.command.implementation + +import android.graphics.Canvas +import org.catrobat.paintroid.command.Command +import org.catrobat.paintroid.contract.LayerContracts + +class SetDimensionCommand(width: Int, height: Int) : Command { + + var width = width; private set + var height = height; private set + + override fun run(canvas: Canvas, layerModel: LayerContracts.Model) { + layerModel.width = width + layerModel.height = height + } + + override fun freeResources() { + // No resources to free + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/SetDimensionCommandSerializer.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/SetDimensionCommandSerializer.kt new file mode 100644 index 00000000..d967c4a2 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/SetDimensionCommandSerializer.kt @@ -0,0 +1,44 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.serialization + +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import org.catrobat.paintroid.command.implementation.SetDimensionCommand + +class SetDimensionCommandSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, command: SetDimensionCommand) { + with(output) { + writeInt(command.width) + writeInt(command.height) + } + } + + override fun read(kryo: Kryo, input: Input, type: Class): SetDimensionCommand = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): SetDimensionCommand { + return with(input) { + val width = readInt() + val height = readInt() + SetDimensionCommand(width, height) + } + } +} From 0e4c89aba7da31aa8fb29dac271667f872b227d4 Mon Sep 17 00:00:00 2001 From: Tim Celec Date: Fri, 12 Jul 2024 19:11:52 +0200 Subject: [PATCH 11/25] PAINTROID-761 - Paint works --- .../org/catrobat/paintroid/FileReader.kt | 18 +- .../paintroid/command/CommandFactory.kt | 126 ++++++++ .../paintroid/command/CommandManager.kt | 79 +++++ .../command/MainActivityConstants.kt | 94 ++++++ .../command/implementation/FlipCommand.kt | 61 ++++ .../command/implementation/RotateCommand.kt | 64 +++++ .../command/implementation/SprayCommand.kt | 38 +++ .../command/serialization/PaintSerializer.kt | 85 ++++++ .../command/serialization/SerializablePath.kt | 210 ++++++++++++++ .../serialization/SerializableTypeface.kt | 49 ++++ .../serialization/SprayCommandSerializer.kt | 45 +++ .../common/CommonBrushChangedListener.kt | 33 +++ .../common/CommonBrushPreviewListener.kt | 42 +++ .../catrobat/paintroid/common/Constants.kt | 70 +++++ .../paintroid/tools/ContextCallback.kt | 55 ++++ .../org/catrobat/paintroid/tools/FontType.kt | 12 + .../org/catrobat/paintroid/tools/Tool.kt | 71 +++++ .../org/catrobat/paintroid/tools/ToolPaint.kt | 37 +++ .../org/catrobat/paintroid/tools/ToolType.kt | 226 +++++++++++++++ .../org/catrobat/paintroid/tools/Workspace.kt | 52 ++++ .../paintroid/tools/common/Constants.kt | 22 ++ .../tools/common/PointScrollBehavior.kt | 46 +++ .../paintroid/tools/common/ScrollBehavior.kt | 25 ++ .../paintroid/tools/drawable/DrawableShape.kt | 23 ++ .../paintroid/tools/drawable/DrawableStyle.kt | 23 ++ .../paintroid/tools/drawable/ShapeDrawable.kt | 27 ++ .../helper/AdvancedSettingsAlgorithms.kt | 82 ++++++ .../tools/implementation/BaseTool.kt | 124 ++++++++ .../tools/implementation/BrushTool.kt | 269 ++++++++++++++++++ .../tools/implementation/DefaultToolPaint.kt | 119 ++++++++ .../tools/implementation/WatercolorTool.kt | 79 +++++ .../tools/options/BrushToolOptionsView.kt | 57 ++++ .../tools/options/BrushToolPreview.kt | 27 ++ .../tools/options/ClipboardToolOptionsView.kt | 41 +++ .../tools/options/FillToolOptionsView.kt | 27 ++ .../tools/options/ImportToolOptionsView.kt | 27 ++ .../tools/options/ShapeToolOptionsView.kt | 47 +++ .../tools/options/SmudgeToolOptionsView.kt | 29 ++ .../tools/options/SprayToolOptionsView.kt | 35 +++ .../tools/options/TextToolOptionsView.kt | 63 ++++ .../options/ToolOptionsViewController.kt | 48 ++++ .../ToolOptionsVisibilityController.kt | 37 +++ .../tools/options/TransformToolOptionsView.kt | 55 ++++ .../org/catrobat/paintroid/ui/Perspective.kt | 223 +++++++++++++++ .../paintroid/ui/tools/NumberRangeFilter.kt | 35 +++ .../res/drawable/ic_pocketpaint_layers.xml | 15 + .../main/res/drawable/ic_pocketpaint_redo.xml | 9 + .../drawable/ic_pocketpaint_tool_brush.xml | 9 + ...c_pocketpaint_tool_center_focus_strong.xml | 12 + .../drawable/ic_pocketpaint_tool_circle.png | Bin 0 -> 415 bytes .../ic_pocketpaint_tool_clipboard.xml | 9 + .../drawable/ic_pocketpaint_tool_clipping.xml | 7 + .../drawable/ic_pocketpaint_tool_cursor.xml | 31 ++ .../drawable/ic_pocketpaint_tool_eraser.xml | 9 + .../res/drawable/ic_pocketpaint_tool_fill.xml | 9 + .../ic_pocketpaint_tool_flip_horizontal.xml | 9 + .../ic_pocketpaint_tool_flip_vertical.xml | 9 + .../res/drawable/ic_pocketpaint_tool_hand.xml | 12 + .../drawable/ic_pocketpaint_tool_import.xml | 9 + .../res/drawable/ic_pocketpaint_tool_line.xml | 12 + .../drawable/ic_pocketpaint_tool_pipette.xml | 9 + .../ic_pocketpaint_tool_rectangle.xml | 14 + .../ic_pocketpaint_tool_resize_adjust.xml | 9 + .../ic_pocketpaint_tool_rotate_left.xml | 24 ++ .../drawable/ic_pocketpaint_tool_smudge.xml | 12 + .../ic_pocketpaint_tool_spray_can.xml | 44 +++ .../drawable/ic_pocketpaint_tool_square.png | Bin 0 -> 157 bytes .../res/drawable/ic_pocketpaint_tool_text.xml | 9 + .../ic_pocketpaint_tool_transform.xml | 9 + .../ic_pocketpaint_tool_watercolor.xml | 18 ++ .../main/res/drawable/ic_pocketpaint_undo.xml | 9 + android/app/src/main/res/values/string.xml | 253 ++++++++++++++++ 72 files changed, 3625 insertions(+), 3 deletions(-) create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/CommandFactory.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/CommandManager.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/MainActivityConstants.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/FlipCommand.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/RotateCommand.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/SprayCommand.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/PaintSerializer.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/SerializablePath.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/SerializableTypeface.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/SprayCommandSerializer.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/common/CommonBrushChangedListener.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/common/CommonBrushPreviewListener.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/common/Constants.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/ContextCallback.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/FontType.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/Tool.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/ToolPaint.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/ToolType.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/Workspace.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/common/Constants.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/common/PointScrollBehavior.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/common/ScrollBehavior.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/drawable/DrawableShape.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/drawable/DrawableStyle.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/drawable/ShapeDrawable.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/helper/AdvancedSettingsAlgorithms.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/implementation/BaseTool.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/implementation/BrushTool.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/implementation/DefaultToolPaint.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/implementation/WatercolorTool.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/BrushToolOptionsView.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/BrushToolPreview.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/ClipboardToolOptionsView.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/FillToolOptionsView.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/ImportToolOptionsView.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/ShapeToolOptionsView.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/SmudgeToolOptionsView.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/SprayToolOptionsView.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/TextToolOptionsView.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/ToolOptionsViewController.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/ToolOptionsVisibilityController.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/TransformToolOptionsView.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/ui/Perspective.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/ui/tools/NumberRangeFilter.kt create mode 100644 android/app/src/main/res/drawable/ic_pocketpaint_layers.xml create mode 100644 android/app/src/main/res/drawable/ic_pocketpaint_redo.xml create mode 100644 android/app/src/main/res/drawable/ic_pocketpaint_tool_brush.xml create mode 100644 android/app/src/main/res/drawable/ic_pocketpaint_tool_center_focus_strong.xml create mode 100644 android/app/src/main/res/drawable/ic_pocketpaint_tool_circle.png create mode 100644 android/app/src/main/res/drawable/ic_pocketpaint_tool_clipboard.xml create mode 100644 android/app/src/main/res/drawable/ic_pocketpaint_tool_clipping.xml create mode 100644 android/app/src/main/res/drawable/ic_pocketpaint_tool_cursor.xml create mode 100644 android/app/src/main/res/drawable/ic_pocketpaint_tool_eraser.xml create mode 100644 android/app/src/main/res/drawable/ic_pocketpaint_tool_fill.xml create mode 100644 android/app/src/main/res/drawable/ic_pocketpaint_tool_flip_horizontal.xml create mode 100644 android/app/src/main/res/drawable/ic_pocketpaint_tool_flip_vertical.xml create mode 100644 android/app/src/main/res/drawable/ic_pocketpaint_tool_hand.xml create mode 100644 android/app/src/main/res/drawable/ic_pocketpaint_tool_import.xml create mode 100644 android/app/src/main/res/drawable/ic_pocketpaint_tool_line.xml create mode 100644 android/app/src/main/res/drawable/ic_pocketpaint_tool_pipette.xml create mode 100644 android/app/src/main/res/drawable/ic_pocketpaint_tool_rectangle.xml create mode 100644 android/app/src/main/res/drawable/ic_pocketpaint_tool_resize_adjust.xml create mode 100644 android/app/src/main/res/drawable/ic_pocketpaint_tool_rotate_left.xml create mode 100644 android/app/src/main/res/drawable/ic_pocketpaint_tool_smudge.xml create mode 100644 android/app/src/main/res/drawable/ic_pocketpaint_tool_spray_can.xml create mode 100644 android/app/src/main/res/drawable/ic_pocketpaint_tool_square.png create mode 100644 android/app/src/main/res/drawable/ic_pocketpaint_tool_text.xml create mode 100644 android/app/src/main/res/drawable/ic_pocketpaint_tool_transform.xml create mode 100644 android/app/src/main/res/drawable/ic_pocketpaint_tool_watercolor.xml create mode 100644 android/app/src/main/res/drawable/ic_pocketpaint_undo.xml create mode 100644 android/app/src/main/res/values/string.xml diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt index 41ba3ddc..d17aba60 100644 --- a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt @@ -16,13 +16,25 @@ import org.catrobat.paintroid.command.serialization.DataStructuresSerializer import org.catrobat.paintroid.model.CommandManagerModel import org.catrobat.paintroid.command.serialization.CommandManagerModelSerializer import org.catrobat.paintroid.command.implementation.SetDimensionCommand +import org.catrobat.paintroid.command.implementation.SprayCommand import org.catrobat.paintroid.command.serialization.SetDimensionCommandSerializer +import org.catrobat.paintroid.command.serialization.SprayCommandSerializer +import org.catrobat.paintroid.command.serialization.PaintSerializer +import android.graphics.Paint import android.graphics.Point import android.graphics.PointF +import android.graphics.RectF +import android.graphics.Bitmap + +import android.content.ContentResolver +import android.content.ContentUris +import android.content.ContentValues + class FileReader(private val context : Context) { + private lateinit var activityContext: Context // MAYBE CAUSE A CRASH private val kryo = Kryo() private val registerMap = LinkedHashMap, VersionSerializer<*>?>() companion object { @@ -48,9 +60,9 @@ class FileReader(private val context : Context) put(Point::class.java, DataStructuresSerializer.PointSerializer(version)) put(CommandManagerModel::class.java, CommandManagerModelSerializer(version)) put(SetDimensionCommand::class.java, SetDimensionCommandSerializer(version)) - /* put(SprayCommand::class.java, SprayCommandSerializer(version)) - put(Paint::class.java, PaintSerializer(version, activityContext)) - put(AddEmptyLayerCommand::class.java, AddLayerCommandSerializer(version)) + put(SprayCommand::class.java, SprayCommandSerializer(version)) + put(Paint::class.java, PaintSerializer(version, activityContext)) // maybe will cause a crash ? activityContext is lateinnit + /* put(AddEmptyLayerCommand::class.java, AddLayerCommandSerializer(version)) put(SelectLayerCommand::class.java, SelectLayerCommandSerializer(version)) put(LoadCommand::class.java, LoadCommandSerializer(version)) put(TextToolCommand::class.java, TextToolCommandSerializer(version, activityContext)) diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/CommandFactory.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/CommandFactory.kt new file mode 100644 index 00000000..73cfecc5 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/CommandFactory.kt @@ -0,0 +1,126 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Paint +import android.graphics.Point +import android.graphics.PointF +import android.graphics.RectF +import org.catrobat.paintroid.command.implementation.FlipCommand.FlipDirection +import org.catrobat.paintroid.command.implementation.RotateCommand.RotateDirection +import org.catrobat.paintroid.command.serialization.SerializablePath +import org.catrobat.paintroid.command.serialization.SerializableTypeface +import org.catrobat.paintroid.contract.LayerContracts +import org.catrobat.paintroid.tools.Tool +import org.catrobat.paintroid.tools.drawable.ShapeDrawable + +interface CommandFactory { + fun createInitCommand(width: Int, height: Int): Command + + fun createInitCommand(bitmap: Bitmap): Command + + fun createInitCommand(layers: List): Command + + fun createResetCommand(): Command + + fun createAddEmptyLayerCommand(): Command + + fun createSelectLayerCommand(position: Int): Command + + fun createLayerOpacityCommand(position: Int, opacityPercentage: Int): Command + + fun createRemoveLayerCommand(index: Int): Command + + fun createReorderLayersCommand(position: Int, swapWith: Int): Command + + fun createMergeLayersCommand(position: Int, mergeWith: Int): Command + + fun createRotateCommand(rotateDirection: RotateDirection): Command + + fun createFlipCommand(flipDirection: FlipDirection): Command + + fun createCropCommand( + resizeCoordinateXLeft: Int, + resizeCoordinateYTop: Int, + resizeCoordinateXRight: Int, + resizeCoordinateYBottom: Int, + maximumBitmapResolution: Int + ): Command + + fun createPointCommand(paint: Paint, coordinate: PointF): Command + + fun createFillCommand(x: Int, y: Int, paint: Paint, colorTolerance: Float): Command + + fun createGeometricFillCommand( + shapeDrawable: ShapeDrawable, + position: Point, + box: RectF, + boxRotation: Float, + paint: Paint + ): Command + + fun createPathCommand(paint: Paint, path: SerializablePath): Command + + fun createSmudgePathCommand( + bitmap: Bitmap, + pointPath: MutableList, + maxPressure: Float, + maxSize: Float, + minSize: Float + ): Command + + fun createTextToolCommand( + multilineText: Array, + textPaint: Paint, + boxOffset: Int, + boxWidth: Float, + boxHeight: Float, + toolPosition: PointF, + boxRotation: Float, + typefaceInfo: SerializableTypeface + ): Command + + fun createResizeCommand(newWidth: Int, newHeight: Int): Command + + fun createClipboardCommand( + bitmap: Bitmap, + toolPosition: PointF, + boxWidth: Float, + boxHeight: Float, + boxRotation: Float + ): Command + + fun createSprayCommand(sprayedPoints: FloatArray, paint: Paint): Command + + fun createCutCommand( + toolPosition: PointF, + boxWidth: Float, + boxHeight: Float, + boxRotation: Float + ): Command + + fun createColorChangedCommand(tool: Tool?, context: Context, color: Int): Command + + fun createClippingCommand( + bitmap: Bitmap, + pathBitmap: Bitmap + ): Command +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/CommandManager.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/CommandManager.kt new file mode 100644 index 00000000..2c32275b --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/CommandManager.kt @@ -0,0 +1,79 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command + +import org.catrobat.paintroid.model.CommandManagerModel + +interface CommandManager { + val isUndoAvailable: Boolean + val isRedoAvailable: Boolean + val lastExecutedCommand: Command? + val isBusy: Boolean + val commandManagerModel: CommandManagerModel? + + fun addCommandListener(commandListener: CommandListener) + + fun removeCommandListener(commandListener: CommandListener) + + fun addCommand(command: Command?) + + fun addCommandWithoutUndo(command: Command?) + + fun setInitialStateCommand(command: Command) + + fun loadCommandsCatrobatImage(model: CommandManagerModel?) + + fun undo() + + fun redo() + + fun reset() + + fun shutdown() + + fun undoIgnoringColorChanges() + + fun undoIgnoringColorChangesAndAddCommand(command: Command) + + fun undoInConnectedLinesMode() + + fun redoInConnectedLinesMode() + + fun getCommandManagerModelForCatrobatImage(): CommandManagerModel? + + fun adjustUndoListForClippingTool() + + fun undoInClippingTool() + + fun popFirstCommandInUndo() + + fun popFirstCommandInRedo() + + fun executeAllCommands() + + fun getUndoCommandCount(): Int + + fun getColorCommandCount(): Int + + fun isLastColorCommandOnTop(): Boolean + + interface CommandListener { + fun commandPostExecute() + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/MainActivityConstants.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/MainActivityConstants.kt new file mode 100644 index 00000000..263e9e0f --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/MainActivityConstants.kt @@ -0,0 +1,94 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.common + +import androidx.annotation.IntDef +import java.lang.AssertionError + +const val SAVE_IMAGE_DEFAULT = 1 +const val SAVE_IMAGE_NEW_EMPTY = 2 +const val SAVE_IMAGE_LOAD_NEW = 3 +const val SAVE_IMAGE_FINISH = 4 + +const val LOAD_IMAGE_DEFAULT = 1 +const val LOAD_IMAGE_IMPORT_PNG = 2 +const val LOAD_IMAGE_CATROID = 3 + +const val CREATE_FILE_DEFAULT = 1 + +const val REQUEST_CODE_IMPORT_PNG = 1 +const val REQUEST_CODE_LOAD_PICTURE = 2 +const val REQUEST_CODE_INTRO = 3 + +const val PERMISSION_EXTERNAL_STORAGE_SAVE = 1 +const val PERMISSION_EXTERNAL_STORAGE_SAVE_COPY = 2 +const val PERMISSION_EXTERNAL_STORAGE_SAVE_CONFIRMED_LOAD_NEW = 3 +const val PERMISSION_EXTERNAL_STORAGE_SAVE_CONFIRMED_NEW_EMPTY = 4 +const val PERMISSION_EXTERNAL_STORAGE_SAVE_CONFIRMED_FINISH = 5 +const val PERMISSION_REQUEST_CODE_REPLACE_PICTURE = 6 +const val PERMISSION_REQUEST_CODE_IMPORT_PICTURE = 7 + +const val RESULT_INTRO_MW_NOT_SUPPORTED = 10 + +class MainActivityConstants private constructor() { + @IntDef( + SAVE_IMAGE_DEFAULT, + SAVE_IMAGE_NEW_EMPTY, + SAVE_IMAGE_LOAD_NEW, + SAVE_IMAGE_FINISH + ) + @Retention(AnnotationRetention.SOURCE) + annotation class SaveImageRequestCode + + @IntDef( + LOAD_IMAGE_DEFAULT, + LOAD_IMAGE_IMPORT_PNG, + LOAD_IMAGE_CATROID + ) + @Retention(AnnotationRetention.SOURCE) + annotation class LoadImageRequestCode + + @IntDef(CREATE_FILE_DEFAULT) + @Retention(AnnotationRetention.SOURCE) + annotation class CreateFileRequestCode + + @IntDef( + REQUEST_CODE_IMPORT_PNG, + REQUEST_CODE_LOAD_PICTURE, + REQUEST_CODE_INTRO + ) + @Retention(AnnotationRetention.SOURCE) + annotation class ActivityRequestCode + + @IntDef( + PERMISSION_EXTERNAL_STORAGE_SAVE, + PERMISSION_EXTERNAL_STORAGE_SAVE_COPY, + PERMISSION_EXTERNAL_STORAGE_SAVE_CONFIRMED_LOAD_NEW, + PERMISSION_EXTERNAL_STORAGE_SAVE_CONFIRMED_NEW_EMPTY, + PERMISSION_EXTERNAL_STORAGE_SAVE_CONFIRMED_FINISH, + PERMISSION_REQUEST_CODE_REPLACE_PICTURE, + PERMISSION_REQUEST_CODE_IMPORT_PICTURE + ) + @Retention(AnnotationRetention.SOURCE) + annotation class PermissionRequestCode + + init { + throw AssertionError() + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/FlipCommand.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/FlipCommand.kt new file mode 100644 index 00000000..aefd9a5d --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/FlipCommand.kt @@ -0,0 +1,61 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.paintroid.command.implementation + +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Matrix +import android.graphics.Paint +import org.catrobat.paintroid.command.Command +import org.catrobat.paintroid.contract.LayerContracts + +class FlipCommand(flipDirection: FlipDirection) : Command { + + var flipDirection = flipDirection; private set + + override fun run(canvas: Canvas, layerModel: LayerContracts.Model) { + val flipMatrix = Matrix().apply { + when (flipDirection) { + FlipDirection.FLIP_HORIZONTAL -> { + setScale(1f, -1f) + postTranslate(0f, layerModel.height.toFloat()) + } + FlipDirection.FLIP_VERTICAL -> { + setScale(-1f, 1f) + postTranslate(layerModel.width.toFloat(), 0f) + } + } + } + layerModel.currentLayer?.bitmap?.let { bitmap -> + val bitmapCopy = bitmap.copy(bitmap.config, bitmap.isMutable) + val flipCanvas = Canvas(bitmap) + bitmap.eraseColor(Color.TRANSPARENT) + flipCanvas.drawBitmap(bitmapCopy, flipMatrix, Paint()) + } + } + + override fun freeResources() { + // No resources to free + } + + enum class FlipDirection { + FLIP_HORIZONTAL, FLIP_VERTICAL + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/RotateCommand.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/RotateCommand.kt new file mode 100644 index 00000000..7a2d2764 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/RotateCommand.kt @@ -0,0 +1,64 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.paintroid.command.implementation + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Matrix +import org.catrobat.paintroid.command.Command +import org.catrobat.paintroid.contract.LayerContracts + +class RotateCommand(rotateDirection: RotateDirection) : Command { + + var rotateDirection = rotateDirection; private set + + companion object { + private const val ANGLE = 90f + } + + override fun run(canvas: Canvas, layerModel: LayerContracts.Model) { + val rotateMatrix = Matrix().apply { + when (rotateDirection) { + RotateDirection.ROTATE_RIGHT -> postRotate(ANGLE) + RotateDirection.ROTATE_LEFT -> postRotate(-ANGLE) + } + } + val iterator: Iterator = layerModel.listIterator(0) + while (iterator.hasNext()) { + val currentLayer = iterator.next() + val rotatedBitmap = Bitmap.createBitmap( + currentLayer.bitmap, 0, 0, + layerModel.width, layerModel.height, rotateMatrix, true + ) + currentLayer.bitmap = rotatedBitmap + } + val tmpWidth = layerModel.width + layerModel.width = layerModel.height + layerModel.height = tmpWidth + } + + override fun freeResources() { + // No resources to free + } + + enum class RotateDirection { + ROTATE_LEFT, ROTATE_RIGHT + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/SprayCommand.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/SprayCommand.kt new file mode 100644 index 00000000..711db0fd --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/SprayCommand.kt @@ -0,0 +1,38 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.implementation + +import android.graphics.Canvas +import android.graphics.Paint +import org.catrobat.paintroid.command.Command +import org.catrobat.paintroid.contract.LayerContracts + +class SprayCommand( + val sprayedPoints: FloatArray, + val paint: Paint +) : Command { + + override fun run(canvas: Canvas, layerModel: LayerContracts.Model) { + canvas.drawPoints(sprayedPoints, paint) + } + + override fun freeResources() { + // nothing to free + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/PaintSerializer.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/PaintSerializer.kt new file mode 100644 index 00000000..bf4e1e2d --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/PaintSerializer.kt @@ -0,0 +1,85 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.serialization + +import android.content.Context +import android.graphics.BlurMaskFilter +import android.graphics.Paint +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output + +import org.catrobat.paintroid.tools.implementation.DefaultToolPaint +import org.catrobat.paintroid.tools.implementation.WatercolorTool +class PaintSerializer(version: Int, private val activityContext: Context) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, paint: Paint) { + with(output) { + writeInt(paint.color) + writeFloat(paint.strokeWidth) + writeInt(paint.strokeCap.ordinal) + writeBoolean(paint.isAntiAlias) + writeInt(paint.style.ordinal) + writeInt(paint.strokeJoin.ordinal) + writeBoolean(paint.maskFilter != null) + writeInt(paint.alpha) + } + } + + override fun read(kryo: Kryo, input: Input, type: Class): Paint = + super.handleVersions(this, kryo, input, type) + + override fun readV1(serializer: VersionSerializer, kryo: Kryo, input: Input, type: Class): Paint { + val toolPaint = DefaultToolPaint(activityContext).apply { + with(input) { + color = readInt() + strokeWidth = readFloat() + strokeCap = Paint.Cap.values()[readInt()] + } + } + + return toolPaint.paint.apply { + with(input) { + isAntiAlias = readBoolean() + style = Paint.Style.values()[readInt()] + strokeJoin = Paint.Join.values()[readInt()] + } + } + } + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): Paint { + val toolPaint = DefaultToolPaint(activityContext).apply { + with(input) { + color = readInt() + strokeWidth = readFloat() + strokeCap = Paint.Cap.values()[readInt()] + } + } + + return toolPaint.paint.apply { + with(input) { + isAntiAlias = readBoolean() + style = Paint.Style.values()[readInt()] + strokeJoin = Paint.Join.values()[readInt()] + val hadFilter: Boolean = input.readBoolean() + alpha = input.readInt() + if (hadFilter) maskFilter = BlurMaskFilter(WatercolorTool.calcRange(alpha), BlurMaskFilter.Blur.INNER) + } + } + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/SerializablePath.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/SerializablePath.kt new file mode 100644 index 00000000..64c86aff --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/SerializablePath.kt @@ -0,0 +1,210 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.serialization + +import android.graphics.Path +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output + +open class SerializablePath : Path { + + var serializableActions = ArrayList() + + constructor() : super() + + constructor(actions: ArrayList) : super() { + actions.forEach { + it.perform(this) + } + } + + constructor(src: SerializablePath) : super(src) { + this.serializableActions.addAll(src.serializableActions) + } + + override fun moveTo(x: Float, y: Float) { + serializableActions.add(Move(x, y)) + super.moveTo(x, y) + } + + override fun lineTo(x: Float, y: Float) { + serializableActions.add(Line(x, y)) + super.lineTo(x, y) + } + + override fun quadTo(x1: Float, y1: Float, x2: Float, y2: Float) { + serializableActions.add(Quad(x1, y1, x2, y2)) + super.quadTo(x1, y1, x2, y2) + } + + override fun cubicTo(x1: Float, y1: Float, x2: Float, y2: Float, x3: Float, y3: Float) { + serializableActions.add(Cube(x1, y1, x2, y2, x3, y3)) + super.cubicTo(x1, y1, x2, y2, x3, y3) + } + + override fun rewind() { + serializableActions.clear() + super.rewind() + } + + interface SerializableAction { + fun perform(path: Path) + } + + class Move(val x: Float, val y: Float) : SerializableAction { + override fun perform(path: Path) { + path.moveTo(x, y) + } + } + + class Line(val x: Float, val y: Float) : SerializableAction { + override fun perform(path: Path) { + path.lineTo(x, y) + } + } + + class Quad(val x1: Float, val y1: Float, val x2: Float, val y2: Float) : SerializableAction { + override fun perform(path: Path) { + path.quadTo(x1, y1, x2, y2) + } + } + + class Cube(val x1: Float, val y1: Float, val x2: Float, val y2: Float, val x3: Float, val y3: Float) : SerializableAction { + override fun perform(path: Path) { + path.cubicTo(x1, y1, x2, y2, x3, y3) + } + } + + class Rewind : SerializableAction { + override fun perform(path: Path) { + path.rewind() + } + } + + class PathSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, path: SerializablePath) { + output.writeInt(path.serializableActions.size) + path.serializableActions.forEach { action -> + kryo.writeClassAndObject(output, action) + } + } + + override fun read(kryo: Kryo, input: Input, type: Class): SerializablePath = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): SerializablePath { + val size = input.readInt() + val actionList = ArrayList() + repeat(size) { + actionList.add(kryo.readClassAndObject(input) as SerializableAction) + } + return SerializablePath(actionList) + } + } + + class PathActionMoveSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, action: Move) { + with(output) { + writeFloat(action.x) + writeFloat(action.y) + } + } + + override fun read(kryo: Kryo, input: Input, type: Class): Move = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): Move { + return with(input) { + Move(readFloat(), readFloat()) + } + } + } + + class PathActionLineSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, action: Line) { + with(output) { + writeFloat(action.x) + writeFloat(action.y) + } + } + + override fun read(kryo: Kryo, input: Input, type: Class): Line = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): Line { + return with(input) { + Line(readFloat(), readFloat()) + } + } + } + + class PathActionQuadSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, action: Quad) { + with(output) { + writeFloat(action.x1) + writeFloat(action.y1) + writeFloat(action.x2) + writeFloat(action.y2) + } + } + + override fun read(kryo: Kryo, input: Input, type: Class): Quad = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): Quad { + return with(input) { + Quad(readFloat(), readFloat(), readFloat(), readFloat()) + } + } + } + + class PathActionCubeSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, action: Cube) { + with(output) { + writeFloat(action.x1) + writeFloat(action.y1) + writeFloat(action.x2) + writeFloat(action.y2) + writeFloat(action.x3) + writeFloat(action.y3) + } + } + + override fun read(kryo: Kryo, input: Input, type: Class): Cube = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class) = + with(input) { + Cube(readFloat(), readFloat(), readFloat(), readFloat(), readFloat(), readFloat()) + } + } + + class PathActionRewindSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, action: Rewind) { + // Has no member variables to save + } + + override fun read(kryo: Kryo, input: Input, type: Class): Rewind = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): Rewind = + Rewind() + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/SerializableTypeface.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/SerializableTypeface.kt new file mode 100644 index 00000000..1aea05d5 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/SerializableTypeface.kt @@ -0,0 +1,49 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.serialization + +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import org.catrobat.paintroid.tools.FontType + +data class SerializableTypeface(val font: FontType, val bold: Boolean, val underline: Boolean, val italic: Boolean, val textSize: Float, val textSkewX: Float) { + + class TypefaceSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, typeface: SerializableTypeface) { + with(output) { + writeString(typeface.font.name) + writeBoolean(typeface.bold) + writeBoolean(typeface.underline) + writeBoolean(typeface.italic) + writeFloat(typeface.textSize) + writeFloat(typeface.textSkewX) + } + } + + override fun read(kryo: Kryo, input: Input, type: Class): SerializableTypeface = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): SerializableTypeface { + return with(input) { + SerializableTypeface(FontType.valueOf(readString()), readBoolean(), readBoolean(), readBoolean(), readFloat(), readFloat()) + } + } + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/SprayCommandSerializer.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/SprayCommandSerializer.kt new file mode 100644 index 00000000..3d85125c --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/SprayCommandSerializer.kt @@ -0,0 +1,45 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.serialization + +import android.graphics.Paint +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import org.catrobat.paintroid.command.implementation.SprayCommand + +class SprayCommandSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, command: SprayCommand) { + with(kryo) { + writeObject(output, command.sprayedPoints) + writeObject(output, command.paint) + } + } + + override fun read(kryo: Kryo, input: Input, type: Class): SprayCommand = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): SprayCommand { + return with(kryo) { + val sprayPoints = kryo.readObject(input, FloatArray::class.java) + val paint = kryo.readObject(input, Paint::class.java) + SprayCommand(sprayPoints, paint) + } + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/common/CommonBrushChangedListener.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/common/CommonBrushChangedListener.kt new file mode 100644 index 00000000..6cb26158 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/common/CommonBrushChangedListener.kt @@ -0,0 +1,33 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools.common + +import android.graphics.Paint.Cap +import org.catrobat.paintroid.tools.Tool +import org.catrobat.paintroid.tools.options.BrushToolOptionsView.OnBrushChangedListener + +class CommonBrushChangedListener(private val tool: Tool) : OnBrushChangedListener { + override fun setCap(strokeCap: Cap) { + tool.changePaintStrokeCap(strokeCap) + } + + override fun setStrokeWidth(strokeWidth: Int) { + tool.changePaintStrokeWidth(strokeWidth) + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/common/CommonBrushPreviewListener.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/common/CommonBrushPreviewListener.kt new file mode 100644 index 00000000..4143945b --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/common/CommonBrushPreviewListener.kt @@ -0,0 +1,42 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools.common + +import android.graphics.MaskFilter +import android.graphics.Paint.Cap +import org.catrobat.paintroid.tools.ToolPaint +import org.catrobat.paintroid.tools.ToolType +import org.catrobat.paintroid.tools.options.BrushToolOptionsView.OnBrushPreviewListener + +class CommonBrushPreviewListener( + private val toolPaint: ToolPaint, + override val toolType: ToolType +) : OnBrushPreviewListener { + override val strokeWidth: Float + get() = toolPaint.strokeWidth + + override val strokeCap: Cap + get() = toolPaint.strokeCap + + override val color: Int + get() = toolPaint.color + + override val maskFilter: MaskFilter? + get() = toolPaint.paint.maskFilter +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/common/Constants.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/common/Constants.kt new file mode 100644 index 00000000..19b121b8 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/common/Constants.kt @@ -0,0 +1,70 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.common + +import android.os.Environment +import java.io.File + +const val PAINTROID_PICTURE_PATH = "org.catrobat.extra.PAINTROID_PICTURE_PATH" +const val PAINTROID_PICTURE_NAME = "org.catrobat.extra.PAINTROID_PICTURE_NAME" +const val TEMP_PICTURE_NAME = "catroidTemp" +const val MEDIA_GALLEY_URL = "https://share.catrob.at/pocketcode/media-library/looks" +const val ABOUT_DIALOG_FRAGMENT_TAG = "aboutdialogfragment" +const val LIKE_US_DIALOG_FRAGMENT_TAG = "likeusdialogfragment" +const val RATE_US_DIALOG_FRAGMENT_TAG = "rateusdialogfragment" +const val FEEDBACK_DIALOG_FRAGMENT_TAG = "feedbackdialogfragment" +const val ZOOM_WINDOW_SETTINGS_DIALOG_FRAGMENT_TAG = "zoomwindowsettingsdialogfragment" +const val ADVANCED_SETTINGS_DIALOG_FRAGMENT_TAG = "advancedsettingsdialogfragment" +const val SAVE_DIALOG_FRAGMENT_TAG = "savedialogerror" +const val LOAD_DIALOG_FRAGMENT_TAG = "loadbitmapdialogerror" +const val COLOR_PICKER_DIALOG_TAG = "ColorPickerDialogTag" +const val SAVE_QUESTION_FRAGMENT_TAG = "savebeforequitfragment" +const val SAVE_INFORMATION_DIALOG_TAG = "saveinformationdialogfragment" +const val OVERWRITE_INFORMATION_DIALOG_TAG = "saveinformationdialogfragment" +const val PNG_INFORMATION_DIALOG_TAG = "pnginformationdialogfragment" +const val JPG_INFORMATION_DIALOG_TAG = "jpginformationdialogfragment" +const val ORA_INFORMATION_DIALOG_TAG = "orainformationdialogfragment" +const val CATROBAT_INFORMATION_DIALOG_TAG = "catrobatinformationdialogfragment" +const val CATROID_MEDIA_GALLERY_FRAGMENT_TAG = "catroidmediagalleryfragment" +const val PERMISSION_DIALOG_FRAGMENT_TAG = "permissiondialogfragment" +const val SHOW_LIKE_US_DIALOG_SHARED_PREFERENCES_TAG = "showlikeusdialog" +const val ZOOM_WINDOW_ENABLED_SHARED_PREFERENCES_TAG = "zoomwindowenabled" +const val ZOOM_WINDOW_ZOOM_PERCENTAGE_SHARED_PREFERENCES_TAG = "zoomwindowzoompercentage" +const val IMAGE_NUMBER_SHARED_PREFERENCES_TAG = "imagenumbertag" +const val SCALE_IMAGE_FRAGMENT_TAG = "showscaleimagedialog" +const val INDETERMINATE_PROGRESS_DIALOG_TAG = "indeterminateprogressdialogfragment" +const val INVALID_RESOURCE_ID = 0 +const val MAX_LAYERS = 100 +const val MEGABYTE_IN_BYTE = 1_048_576L +const val MINIMUM_HEAP_SPACE_FOR_NEW_LAYER = 40 +const val CATROBAT_IMAGE_ENDING = "catrobat-image" +const val TEMP_IMAGE_DIRECTORY_NAME = "TemporaryImages" +const val TEMP_IMAGE_NAME = "temporaryImage" +const val ITALIC_FONT_BOX_ADJUSTMENT = 1.2f +const val TEMP_IMAGE_PATH = "$TEMP_IMAGE_DIRECTORY_NAME/$TEMP_IMAGE_NAME.$CATROBAT_IMAGE_ENDING" +const val TEMP_IMAGE_TEMP_PATH = "$TEMP_IMAGE_DIRECTORY_NAME/${TEMP_IMAGE_NAME}1.$CATROBAT_IMAGE_ENDING" +const val SPECIFIC_FILETYPE_SHARED_PREFERENCES_NAME = "Ownfiletypepreferences" +const val ANIMATION_DURATION: Long = 250 + +object Constants { + @JvmField + val PICTURES_DIRECTORY = File(Environment.getExternalStorageDirectory(), Environment.DIRECTORY_PICTURES) + @JvmField + val DOWNLOADS_DIRECTORY = File(Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOWNLOADS) +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/ContextCallback.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/ContextCallback.kt new file mode 100644 index 00000000..bd5172ef --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/ContextCallback.kt @@ -0,0 +1,55 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools + +import android.graphics.Shader +import android.graphics.Typeface +import android.graphics.drawable.Drawable +import android.util.DisplayMetrics +import androidx.annotation.ColorInt +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.annotation.FontRes +import androidx.annotation.StringRes + +interface ContextCallback { + val scrollTolerance: Int + val orientation: ScreenOrientation? + val displayMetrics: DisplayMetrics + val checkeredBitmapShader: Shader? + + fun showNotification(@StringRes resId: Int) + + fun showNotificationWithDuration(@StringRes resId: Int, duration: NotificationDuration) + + fun getFont(@FontRes id: Int): Typeface? + + @ColorInt + fun getColor(@ColorRes id: Int): Int + + fun getDrawable(@DrawableRes resource: Int): Drawable? + + enum class ScreenOrientation { + PORTRAIT, LANDSCAPE + } + + enum class NotificationDuration { + SHORT, LONG + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/FontType.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/FontType.kt new file mode 100644 index 00000000..dbaeea08 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/FontType.kt @@ -0,0 +1,12 @@ +package org.catrobat.paintroid.tools + +import androidx.annotation.StringRes +import org.catrobat.paintroid.R + +enum class FontType(@StringRes val nameResource: Int) { + SANS_SERIF(R.string.text_tool_dialog_font_sans_serif), + SERIF(R.string.text_tool_dialog_font_serif), + MONOSPACE(R.string.text_tool_dialog_font_monospace), + STC(R.string.text_tool_dialog_font_arabic_stc), + DUBAI(R.string.text_tool_dialog_font_dubai); +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/Tool.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/Tool.kt new file mode 100644 index 00000000..f9014481 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/Tool.kt @@ -0,0 +1,71 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools + +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Paint.Cap +import android.graphics.Point +import android.graphics.PointF +import android.os.Bundle + +interface Tool { + val toolType: ToolType + + val drawPaint: Paint + + var drawTime: Long + + fun handToolMode(): Boolean + + fun handleDown(coordinate: PointF?): Boolean + + fun handleMove(coordinate: PointF?, shouldAnimate: Boolean = false): Boolean + + fun handleUp(coordinate: PointF?): Boolean + + fun changePaintColor(color: Int, invalidate: Boolean = true) + + fun changePaintStrokeWidth(strokeWidth: Int) + + fun changePaintStrokeCap(cap: Cap) + + fun draw(canvas: Canvas) + + fun resetInternalState(stateChange: StateChange) + + fun getAutoScrollDirection( + pointX: Float, + pointY: Float, + screenWidth: Int, + screenHeight: Int + ): Point + + fun handleUpAnimations(coordinate: PointF?) + fun handleDownAnimations(coordinate: PointF?) + fun onSaveInstanceState(bundle: Bundle?) + + fun onRestoreInstanceState(bundle: Bundle?) + + enum class StateChange { + ALL, RESET_INTERNAL_STATE, NEW_IMAGE_LOADED, MOVE_CANCELED + } + + fun toolPositionCoordinates(coordinate: PointF): PointF +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/ToolPaint.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/ToolPaint.kt new file mode 100644 index 00000000..9d98c5fd --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/ToolPaint.kt @@ -0,0 +1,37 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools + +import android.graphics.Paint +import android.graphics.Paint.Cap +import android.graphics.PorterDuffXfermode +import android.graphics.Shader + +interface ToolPaint { + var paint: Paint + val previewPaint: Paint + var color: Int + val eraseXfermode: PorterDuffXfermode + val previewColor: Int + var strokeWidth: Float + var strokeCap: Cap + val checkeredShader: Shader? + + fun setAntialiasing() +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/ToolType.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/ToolType.kt new file mode 100644 index 00000000..f5b649fa --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/ToolType.kt @@ -0,0 +1,226 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools + +import androidx.annotation.DrawableRes +import androidx.annotation.IdRes +import androidx.annotation.StringRes +import org.catrobat.paintroid.R +import org.catrobat.paintroid.common.INVALID_RESOURCE_ID +import org.catrobat.paintroid.tools.Tool.StateChange +import java.util.EnumSet + +enum class ToolType( + @get:StringRes val nameResource: Int, + @get:StringRes val helpTextResource: Int, + @get:DrawableRes val drawableResource: Int, + private val stateChangeBehaviour: EnumSet, + @get:IdRes val toolButtonID: Int, + @get:DrawableRes val overlayDrawableResource: Int, + private val hasOptions: Boolean +) { + // TODO MAY CAUSE PROBLEMS + PIPETTE( + R.string.button_pipette, + R.string.help_content_eyedropper, + R.drawable.ic_pocketpaint_tool_pipette, + EnumSet.of(StateChange.ALL), + 1, + INVALID_RESOURCE_ID, + false + ), + BRUSH( + R.string.button_brush, + R.string.help_content_brush, + R.drawable.ic_pocketpaint_tool_brush, + EnumSet.of(StateChange.ALL), + 2, + INVALID_RESOURCE_ID, + true + ), + UNDO( + R.string.button_undo, + R.string.help_content_undo, + R.drawable.ic_pocketpaint_undo, + EnumSet.of(StateChange.ALL), + 3, + INVALID_RESOURCE_ID, + false + ), + REDO( + R.string.button_redo, + R.string.help_content_redo, + R.drawable.ic_pocketpaint_redo, + EnumSet.of(StateChange.ALL), + 4, + INVALID_RESOURCE_ID, + false + ), + FILL( + R.string.button_fill, + R.string.help_content_fill, + R.drawable.ic_pocketpaint_tool_fill, + EnumSet.of(StateChange.ALL), + 5, + INVALID_RESOURCE_ID, + true + ), + CLIPBOARD( + R.string.button_clipboard, + R.string.help_content_clipboard, + R.drawable.ic_pocketpaint_tool_clipboard, + EnumSet.of(StateChange.ALL), + 6, + INVALID_RESOURCE_ID, + true + ), + LINE( + R.string.button_line, + R.string.help_content_line, + R.drawable.ic_pocketpaint_tool_line, + EnumSet.of(StateChange.ALL), + 7, + INVALID_RESOURCE_ID, + true + ), + CURSOR( + R.string.button_cursor, + R.string.help_content_cursor, + R.drawable.ic_pocketpaint_tool_cursor, + EnumSet.of(StateChange.ALL), + 8, + INVALID_RESOURCE_ID, + true + ), + IMPORTPNG( + R.string.button_import_image, + R.string.help_content_import_png, + R.drawable.ic_pocketpaint_tool_import, + EnumSet.of(StateChange.ALL), + 9, + INVALID_RESOURCE_ID, + false + ), + TRANSFORM( + R.string.button_transform, + R.string.help_content_transform, + R.drawable.ic_pocketpaint_tool_transform, + EnumSet.of(StateChange.RESET_INTERNAL_STATE, StateChange.NEW_IMAGE_LOADED), + 10, + INVALID_RESOURCE_ID, + true + ), + ERASER( + R.string.button_eraser, + R.string.help_content_eraser, + R.drawable.ic_pocketpaint_tool_eraser, + EnumSet.of(StateChange.ALL), + 11, + INVALID_RESOURCE_ID, + true + ), + SHAPE( + R.string.button_shape, + R.string.help_content_shape, + R.drawable.ic_pocketpaint_tool_rectangle, + EnumSet.of(StateChange.ALL), + 12, + INVALID_RESOURCE_ID, + true + ), + TEXT( + R.string.button_text, + R.string.help_content_text, + R.drawable.ic_pocketpaint_tool_text, + EnumSet.of(StateChange.ALL), + 13, + INVALID_RESOURCE_ID, + true + ), + LAYER( + R.string.layers_title, + R.string.help_content_layer, + R.drawable.ic_pocketpaint_layers, + EnumSet.of(StateChange.ALL), + INVALID_RESOURCE_ID, + INVALID_RESOURCE_ID, + false + ), + COLORCHOOSER( + 4386, + R.string.help_content_color_chooser, + 746, + EnumSet.of(StateChange.ALL), + INVALID_RESOURCE_ID, + INVALID_RESOURCE_ID, + false + ), + HAND( + R.string.button_hand, + R.string.help_content_hand, + R.drawable.ic_pocketpaint_tool_hand, + EnumSet.of(StateChange.ALL), + 14, + INVALID_RESOURCE_ID, + false + ), + SPRAY( + R.string.button_spray_can, + R.string.help_content_spray_can, + R.drawable.ic_pocketpaint_tool_spray_can, + EnumSet.of(StateChange.ALL), + 15, + INVALID_RESOURCE_ID, + true + ), + WATERCOLOR( + R.string.button_watercolor, + R.string.help_content_watercolor, + R.drawable.ic_pocketpaint_tool_watercolor, + EnumSet.of(StateChange.ALL), + 16, + INVALID_RESOURCE_ID, + true + ), + SMUDGE( + R.string.button_smudge, + R.string.help_content_smudge, + R.drawable.ic_pocketpaint_tool_smudge, + EnumSet.of(StateChange.ALL), + 17, + INVALID_RESOURCE_ID, + true + ), + CLIP( + R.string.button_clip, + R.string.help_content_clip, + R.drawable.ic_pocketpaint_tool_clipping, + EnumSet.of(StateChange.RESET_INTERNAL_STATE), + 18, + INVALID_RESOURCE_ID, + true + ); + + fun shouldReactToStateChange(stateChange: StateChange): Boolean = + stateChangeBehaviour.contains(StateChange.ALL) || stateChangeBehaviour.contains( + stateChange + ) + + fun hasOptions(): Boolean = hasOptions +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/Workspace.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/Workspace.kt new file mode 100644 index 00000000..d3b7a95d --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/Workspace.kt @@ -0,0 +1,52 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools + +import android.graphics.Bitmap +import android.graphics.PointF +import android.graphics.RectF +import org.catrobat.paintroid.contract.LayerContracts +import org.catrobat.paintroid.ui.Perspective + +interface Workspace { + val height: Int + val width: Int + val surfaceWidth: Int + val surfaceHeight: Int + val bitmapOfAllLayers: Bitmap? + val bitmapListOfAllLayers: List + var bitmapOfCurrentLayer: Bitmap? + val currentLayerIndex: Int + val scaleForCenterBitmap: Float + var scale: Float + var perspective: Perspective + val layerModel: LayerContracts.Model + + fun contains(point: PointF): Boolean + + fun intersectsWith(rectF: RectF): Boolean + + fun resetPerspective() + + fun getSurfacePointFromCanvasPoint(coordinate: PointF): PointF + + fun getCanvasPointFromSurfacePoint(surfacePoint: PointF): PointF + + fun invalidate() +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/common/Constants.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/common/Constants.kt new file mode 100644 index 00000000..57c4d1cb --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/common/Constants.kt @@ -0,0 +1,22 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools.common + +const val SCROLL_TOLERANCE_PERCENTAGE = 0.1f +const val MOVE_TOLERANCE = 5f diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/common/PointScrollBehavior.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/common/PointScrollBehavior.kt new file mode 100644 index 00000000..08f1eba9 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/common/PointScrollBehavior.kt @@ -0,0 +1,46 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools.common + +import android.graphics.Point + +class PointScrollBehavior(private val scrollTolerance: Int) : ScrollBehavior { + override fun getScrollDirection( + pointX: Float, + pointY: Float, + viewWidth: Int, + viewHeight: Int + ): Point { + var deltaX = 0 + var deltaY = 0 + if (pointX < scrollTolerance) { + deltaX = 1 + } + if (pointX > viewWidth - scrollTolerance) { + deltaX = -1 + } + if (pointY < scrollTolerance) { + deltaY = 1 + } + if (pointY > viewHeight - scrollTolerance) { + deltaY = -1 + } + return Point(deltaX, deltaY) + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/common/ScrollBehavior.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/common/ScrollBehavior.kt new file mode 100644 index 00000000..7edd6311 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/common/ScrollBehavior.kt @@ -0,0 +1,25 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools.common + +import android.graphics.Point + +interface ScrollBehavior { + fun getScrollDirection(pointX: Float, pointY: Float, viewWidth: Int, viewHeight: Int): Point +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/drawable/DrawableShape.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/drawable/DrawableShape.kt new file mode 100644 index 00000000..cc76fae8 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/drawable/DrawableShape.kt @@ -0,0 +1,23 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools.drawable + +enum class DrawableShape { + RECTANGLE, OVAL, HEART, STAR +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/drawable/DrawableStyle.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/drawable/DrawableStyle.kt new file mode 100644 index 00000000..4f37c0e6 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/drawable/DrawableStyle.kt @@ -0,0 +1,23 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools.drawable + +enum class DrawableStyle { + STROKE, FILL +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/drawable/ShapeDrawable.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/drawable/ShapeDrawable.kt new file mode 100644 index 00000000..10d85c80 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/drawable/ShapeDrawable.kt @@ -0,0 +1,27 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools.drawable + +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.RectF + +interface ShapeDrawable { + fun draw(canvas: Canvas, shapeRect: RectF, drawPaint: Paint) +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/helper/AdvancedSettingsAlgorithms.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/helper/AdvancedSettingsAlgorithms.kt new file mode 100644 index 00000000..4da88117 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/helper/AdvancedSettingsAlgorithms.kt @@ -0,0 +1,82 @@ +package org.catrobat.paintroid.tools.helper + +import android.graphics.PointF +import org.catrobat.paintroid.command.serialization.SerializablePath + +// MAYBE CAN A STUB WORK +object AdvancedSettingsAlgorithms { + @kotlin.jvm.JvmField + var smoothing = true + + const val threshold = 0.2 + const val divider = 3 + + @JvmStatic + fun smoothingAlgorithm(pointArray: List): SerializablePath { + + val diffPointArray = mutableListOf() + if (pointArray.size > 1) { + for (i in pointArray.indices) { + if (i >= 0) { + val point: PointF = pointArray[i] + when (i) { + 0 -> { + val next: PointF = pointArray[i + 1] + val differenceX = next.x - point.x + val differenceY = next.y - point.y + val newPoint = PointF(differenceX / divider, differenceY / divider) + diffPointArray.add(newPoint) + } + pointArray.size - 1 -> { + val prev: PointF = pointArray[i - 1] + val differenceX = point.x - prev.x + val differenceY = point.y - prev.y + val newPoint = PointF(differenceX / divider, differenceY / divider) + diffPointArray.add(newPoint) + } + else -> { + val next: PointF = pointArray[i + 1] + val prev: PointF = pointArray[i - 1] + val differenceX = next.x - prev.x + val differenceY = next.y - prev.y + val newPoint = PointF(differenceX / divider, differenceY / divider) + diffPointArray.add(newPoint) + } + } + } + } + } + + val trueList = mutableListOf() + trueList.add(PointF(pointArray[0].x, pointArray[0].y)) + for (i in 1 until pointArray.size) { + + val point: PointF = pointArray[i] + val diff: PointF = diffPointArray[i] + + val erg1 = point.x + diff.x + val erg2 = point.y + diff.y + + trueList.add(PointF(erg1, erg2)) + } + + val pathNew = SerializablePath() + pathNew.incReserve(1) + pathNew.moveTo(trueList[0].x, trueList[0].y) + + for (i in 1 until trueList.size - 1 step 2) { + val point: PointF = trueList[i] + + val prev: PointF = trueList[i - 1] + val next: PointF = trueList[i + 1] + + pathNew.cubicTo(prev.x, prev.y, point.x, point.y, next.x, next.y) + pathNew.incReserve(1) + } + + trueList.clear() + diffPointArray.clear() + + return pathNew + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/implementation/BaseTool.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/implementation/BaseTool.kt new file mode 100644 index 00000000..c5093d16 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/implementation/BaseTool.kt @@ -0,0 +1,124 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools.implementation + +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Paint.Cap +import android.graphics.Point +import android.graphics.PointF +import android.os.Bundle +import androidx.annotation.ColorInt +import androidx.test.espresso.idling.CountingIdlingResource +import org.catrobat.paintroid.command.CommandManager +import org.catrobat.paintroid.tools.ContextCallback +import org.catrobat.paintroid.tools.Tool +import org.catrobat.paintroid.tools.Tool.StateChange +import org.catrobat.paintroid.tools.ToolPaint +import org.catrobat.paintroid.tools.Workspace +import org.catrobat.paintroid.tools.common.PointScrollBehavior +import org.catrobat.paintroid.tools.common.ScrollBehavior +import org.catrobat.paintroid.tools.options.ToolOptionsViewController +import org.catrobat.paintroid.command.CommandFactory + +// TODO MAY CAUSE CRASH, as we set some stubs +abstract class BaseTool( + open var contextCallback: ContextCallback, + @JvmField var toolOptionsViewController: ToolOptionsViewController, + @JvmField + protected var toolPaint: ToolPaint, + @JvmField + protected var workspace: Workspace, + @JvmField + protected var idlingResource: CountingIdlingResource, + @JvmField + protected var commandManager: CommandManager +) : Tool { + @JvmField + protected val movedDistance: PointF + + @JvmField + protected var scrollBehavior: ScrollBehavior + + @JvmField + var previousEventCoordinate: PointF? + + protected lateinit var commandFactory: CommandFactory + + init { + val scrollTolerance = contextCallback.scrollTolerance + scrollBehavior = PointScrollBehavior(scrollTolerance) + movedDistance = PointF(0f, 0f) + previousEventCoordinate = PointF(0f, 0f) + if (toolPaint != null && toolPaint.paint != null && toolPaint.paint.pathEffect != null) { + toolPaint.paint.pathEffect = null + } + } + + override fun onSaveInstanceState(bundle: Bundle?) = Unit + + override fun onRestoreInstanceState(bundle: Bundle?) = Unit + + override fun changePaintColor(@ColorInt color: Int, invalidate: Boolean) { + toolPaint.color = color + } + + override fun changePaintStrokeWidth(strokeWidth: Int) { + toolPaint.strokeWidth = strokeWidth.toFloat() + } + + override fun changePaintStrokeCap(cap: Cap) { + toolPaint.strokeCap = cap + } + + override fun handleDown(coordinate: PointF?): Boolean = true + + override fun handleUp(coordinate: PointF?): Boolean { + toolOptionsViewController.animateBottomAndTopNavigation(false) + return true + } + + override fun handleMove(coordinate: PointF?, shouldAnimate: Boolean): Boolean { + if (shouldAnimate) { + toolOptionsViewController.animateBottomAndTopNavigation(true) + } + return true + } + override val drawPaint + get() = Paint(toolPaint.paint) + + abstract override fun draw(canvas: Canvas) + + protected open fun resetInternalState() {} + + override fun resetInternalState(stateChange: StateChange) { + if (toolType.shouldReactToStateChange(stateChange)) { + resetInternalState() + } + } + + override fun getAutoScrollDirection( + pointX: Float, + pointY: Float, + screenWidth: Int, + screenHeight: Int + ): Point = scrollBehavior.getScrollDirection(pointX, pointY, screenWidth, screenHeight) + + override fun handToolMode(): Boolean = false +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/implementation/BrushTool.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/implementation/BrushTool.kt new file mode 100644 index 00000000..dfcdd5f3 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/implementation/BrushTool.kt @@ -0,0 +1,269 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools.implementation + +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.PointF +import android.view.View +import androidx.test.espresso.idling.CountingIdlingResource +import org.catrobat.paintroid.command.CommandManager +import org.catrobat.paintroid.command.serialization.SerializablePath +import org.catrobat.paintroid.tools.ContextCallback +import org.catrobat.paintroid.tools.Tool.StateChange +import org.catrobat.paintroid.tools.ToolPaint +import org.catrobat.paintroid.tools.ToolType +import org.catrobat.paintroid.tools.Workspace +import org.catrobat.paintroid.tools.common.CommonBrushChangedListener +import org.catrobat.paintroid.tools.common.CommonBrushPreviewListener +import org.catrobat.paintroid.tools.common.MOVE_TOLERANCE +import org.catrobat.paintroid.tools.helper.AdvancedSettingsAlgorithms +import org.catrobat.paintroid.tools.helper.AdvancedSettingsAlgorithms.smoothing +import org.catrobat.paintroid.tools.helper.AdvancedSettingsAlgorithms.threshold +import org.catrobat.paintroid.tools.options.BrushToolOptionsView +import org.catrobat.paintroid.tools.options.ToolOptionsViewController +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.sqrt + +open class BrushTool( + val brushToolOptionsView: BrushToolOptionsView, + contextCallback: ContextCallback, + toolOptionsViewController: ToolOptionsViewController, + toolPaint: ToolPaint, + workspace: Workspace, + idlingResource: CountingIdlingResource, + commandManager: CommandManager, + override var drawTime: Long +) : BaseTool(contextCallback, toolOptionsViewController, toolPaint, workspace, idlingResource, commandManager) { + protected open val previewPaint: Paint + get() = toolPaint.previewPaint + + protected open val bitmapPaint: Paint + get() = toolPaint.paint + + override val toolType: ToolType + get() = ToolType.BRUSH + + @JvmField + var pathToDraw: SerializablePath = SerializablePath() + var initialEventCoordinate: PointF? = null + private var pathInsideBitmap = false + private val drawToolMovedDistance = PointF(0f, 0f) + + val pointArray = mutableListOf() + + init { + toolOptionsViewController.enable() + pathToDraw.incReserve(1) + brushToolOptionsView.setBrushChangedListener(CommonBrushChangedListener(this)) + brushToolOptionsView.setBrushPreviewListener( + CommonBrushPreviewListener( + toolPaint, + toolType + ) + ) + brushToolOptionsView.setCurrentPaint(toolPaint.paint) + brushToolOptionsView.setStrokeCapButtonChecked(toolPaint.strokeCap) + } + + override fun draw(canvas: Canvas) { + canvas.run { + save() + clipRect(0, 0, workspace.width, workspace.height) + drawPath(pathToDraw, previewPaint) + restore() + } + } + + private fun hideBrushSpecificLayoutOnHandleDown() { + if (toolOptionsViewController.isVisible) { + if (brushToolOptionsView.getTopToolOptions().visibility == View.VISIBLE) { + toolOptionsViewController.slideUp( + brushToolOptionsView.getTopToolOptions(), + willHide = true, + showOptionsView = false + ) + } + + if (brushToolOptionsView.getBottomToolOptions().visibility == View.VISIBLE) { + toolOptionsViewController.slideDown( + brushToolOptionsView.getBottomToolOptions(), + willHide = true, + showOptionsView = false + ) + } + } + } + + private fun showBrushSpecificLayoutOnHandleUp() { + if (!toolOptionsViewController.isVisible) { + if (brushToolOptionsView.getBottomToolOptions().visibility == View.INVISIBLE) { + toolOptionsViewController.slideDown( + brushToolOptionsView.getTopToolOptions(), + willHide = false, + showOptionsView = true + ) + } + + if (brushToolOptionsView.getBottomToolOptions().visibility == View.INVISIBLE) { + toolOptionsViewController.slideUp( + brushToolOptionsView.getBottomToolOptions(), + willHide = false, + showOptionsView = true + ) + } + } + } + + override fun handleDown(coordinate: PointF?): Boolean { + coordinate ?: return false + super.handleDown(coordinate) + initialEventCoordinate = PointF(coordinate.x, coordinate.y) + previousEventCoordinate = PointF(coordinate.x, coordinate.y) + pathToDraw.moveTo(coordinate.x, coordinate.y) + drawToolMovedDistance.set(0f, 0f) + pointArray.add(PointF(coordinate.x, coordinate.y)) + pathInsideBitmap = workspace.contains(coordinate) + return true + } + + override fun handleDownAnimations(coordinate: PointF?) { + hideBrushSpecificLayoutOnHandleDown() + } + + override fun handleUpAnimations(coordinate: PointF?) { + showBrushSpecificLayoutOnHandleUp() + super.handleUp(coordinate) + } + + override fun handleMove(coordinate: PointF?, shouldAnimate: Boolean): Boolean { + if (eventCoordinatesAreNull() || coordinate == null) { + return false + } + super.handleMove(coordinate, shouldAnimate) + if (shouldAnimate) { + hideBrushSpecificLayoutOnHandleDown() + } + previousEventCoordinate?.let { + pathToDraw.quadTo(it.x, it.y, coordinate.x, coordinate.y) + pathToDraw.incReserve(1) + drawToolMovedDistance.set( + drawToolMovedDistance.x + abs(coordinate.x - it.x), + drawToolMovedDistance.y + abs(coordinate.y - it.y) + ) + pointArray.add(PointF(coordinate.x, coordinate.y)) + it.set(coordinate.x, coordinate.y) + } + if (!pathInsideBitmap && workspace.contains(coordinate)) { + pathInsideBitmap = true + } + return true + } + + override fun handleUp(coordinate: PointF?): Boolean { + if (eventCoordinatesAreNull() || coordinate == null) { + return false + } + showBrushSpecificLayoutOnHandleUp() + super.handleUp(coordinate) + + if (!pathInsideBitmap && workspace.contains(coordinate)) { + pathInsideBitmap = true + } + + previousEventCoordinate?.let { + drawToolMovedDistance.set( + drawToolMovedDistance.x + abs(coordinate.x - it.x), + drawToolMovedDistance.y + abs(coordinate.y - it.y) + ) + } + + return if (MOVE_TOLERANCE < max(drawToolMovedDistance.x, drawToolMovedDistance.y)) { + addPathCommand(coordinate) + } else { + initialEventCoordinate?.let { + return addPointCommand(it) + } + false + } + } + + override fun toolPositionCoordinates(coordinate: PointF): PointF = coordinate + + override fun resetInternalState() { + pathToDraw.rewind() + pointArray.clear() + initialEventCoordinate = null + previousEventCoordinate = null + } + + override fun changePaintColor(color: Int, invalidate: Boolean) { + super.changePaintColor(color, invalidate) + if (invalidate) brushToolOptionsView.invalidate() + } + + private fun eventCoordinatesAreNull(): Boolean = + initialEventCoordinate == null || previousEventCoordinate == null + + private fun addPathCommand(coordinate: PointF): Boolean { + pathToDraw.lineTo(coordinate.x, coordinate.y) + + if (!pathInsideBitmap) { + resetInternalState(StateChange.RESET_INTERNAL_STATE) + return false + } + + if (toolType == ToolType.ERASER) { + val command = commandFactory.createPathCommand(bitmapPaint, pathToDraw) + commandManager.addCommand(command) + } else { + var distance: Double? = null + initialEventCoordinate?.apply { + distance = + sqrt(((coordinate.x - x) * (coordinate.x - x) + (coordinate.y - y) * (coordinate.y - y)).toDouble()) + } + val speed = distance?.div(drawTime) + + if (!smoothing || speed != null && speed < threshold) { + val command = commandFactory.createPathCommand(bitmapPaint, pathToDraw) + commandManager.addCommand(command) + } else { + val pathNew = AdvancedSettingsAlgorithms.smoothingAlgorithm(pointArray) + val command = commandFactory.createPathCommand(bitmapPaint, pathNew) + commandManager.addCommand(command) + } + } + + pointArray.clear() + return true + } + + private fun addPointCommand(coordinate: PointF): Boolean { + if (!pathInsideBitmap) { + resetInternalState(StateChange.RESET_INTERNAL_STATE) + return false + } + + pointArray.clear() + val command = commandFactory.createPointCommand(bitmapPaint, coordinate) + commandManager.addCommand(command) + return true + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/implementation/DefaultToolPaint.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/implementation/DefaultToolPaint.kt new file mode 100644 index 00000000..230792c5 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/implementation/DefaultToolPaint.kt @@ -0,0 +1,119 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools.implementation + +import android.content.Context +import android.graphics.BitmapFactory +import android.graphics.BitmapShader +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Paint.Cap +import android.graphics.PorterDuff +import android.graphics.PorterDuffXfermode +import android.graphics.Shader +import org.catrobat.paintroid.R +import org.catrobat.paintroid.tools.ToolPaint + +const val STROKE_25 = 25f +const val STROKE_10 = 10f + +class DefaultToolPaint(private val context: Context) : ToolPaint { + private val bitmapPaint = Paint().apply { + reset() + isAntiAlias = antialiasing + color = Color.BLACK + style = Paint.Style.STROKE + strokeJoin = Paint.Join.ROUND + strokeCap = Cap.ROUND + strokeWidth = STROKE_25 + } + + override val checkeredShader: Shader? + get() { + val checkerboard = + BitmapFactory.decodeResource(context.resources, R.drawable.pocketpaint_checkeredbg) + if (checkerboard != null) { + return BitmapShader(checkerboard, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT) + } + return null + } + + override val previewPaint: Paint + get() = Paint().apply { set(bitmapPaint) } + + override val previewColor: Int + get() = previewPaint.color + + override var color: Int + get() = bitmapPaint.color + set(color) { + bitmapPaint.color = color + previewPaint.set(bitmapPaint) + previewPaint.xfermode = null + if (Color.alpha(color) == 0) { + previewPaint.shader = checkeredShader + previewPaint.color = Color.BLACK + bitmapPaint.xfermode = eraseXfermode + bitmapPaint.alpha = 0 + } else { + bitmapPaint.xfermode = null + } + } + + override var strokeWidth: Float + get() = bitmapPaint.strokeWidth + set(strokeWidth) { + bitmapPaint.strokeWidth = strokeWidth + previewPaint.strokeWidth = strokeWidth + var antiAliasing = antialiasing + if (strokeWidth <= 1) { + antiAliasing = false + } + bitmapPaint.isAntiAlias = antiAliasing + previewPaint.isAntiAlias = antiAliasing + } + + override var strokeCap: Cap + get() = bitmapPaint.strokeCap + set(strokeCap) { + bitmapPaint.strokeCap = strokeCap + previewPaint.strokeCap = strokeCap + } + + override var paint: Paint + get() = bitmapPaint + set(paint) { + bitmapPaint.set(paint) + previewPaint.set(paint) + } + + override val eraseXfermode: PorterDuffXfermode + get() = PorterDuffXfermode(PorterDuff.Mode.CLEAR) + + companion object { + var antialiasing = true + fun arePaintEquals(paint1: Paint, paint2: Paint): Boolean = + paint1.color == paint2.color && paint1.strokeCap == paint2.strokeCap && paint1.isAntiAlias == paint2.isAntiAlias && paint1.strokeJoin == paint2.strokeJoin && paint1.style == paint2.style + } + + override fun setAntialiasing() { + bitmapPaint.isAntiAlias = antialiasing + previewPaint.isAntiAlias = antialiasing + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/implementation/WatercolorTool.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/implementation/WatercolorTool.kt new file mode 100644 index 00000000..764eba62 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/implementation/WatercolorTool.kt @@ -0,0 +1,79 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools.implementation + +import android.graphics.BlurMaskFilter +import androidx.test.espresso.idling.CountingIdlingResource +import org.catrobat.paintroid.command.CommandManager +import org.catrobat.paintroid.tools.ContextCallback +import org.catrobat.paintroid.tools.ToolPaint +import org.catrobat.paintroid.tools.ToolType +import org.catrobat.paintroid.tools.Workspace +import org.catrobat.paintroid.tools.options.BrushToolOptionsView +import org.catrobat.paintroid.tools.options.ToolOptionsViewController + +const val MAX_ALPHA_VALUE = 255 +const val MAX_NEW_RANGE = 150 +const val MIN_NEW_RANGE = 20 + +class WatercolorTool( + brushToolOptionsView: BrushToolOptionsView, + contextCallback: ContextCallback, + toolOptionsViewController: ToolOptionsViewController, + toolPaint: ToolPaint, + workspace: Workspace, + idlingResource: CountingIdlingResource, + commandManager: CommandManager, + drawTime: Long +) : BrushTool( + brushToolOptionsView, + contextCallback, + toolOptionsViewController, + toolPaint, + workspace, + idlingResource, + commandManager, + drawTime +) { + override val toolType: ToolType + get() = ToolType.WATERCOLOR + + init { + bitmapPaint.maskFilter = BlurMaskFilter(calcRange(bitmapPaint.alpha), BlurMaskFilter.Blur.INNER) + previewPaint.maskFilter = BlurMaskFilter(calcRange(previewPaint.alpha), BlurMaskFilter.Blur.INNER) + } + + override fun changePaintColor(color: Int, invalidate: Boolean) { + super.changePaintColor(color, invalidate) + + if (invalidate) brushToolOptionsView.invalidate() + bitmapPaint.maskFilter = BlurMaskFilter(calcRange(bitmapPaint.alpha), BlurMaskFilter.Blur.INNER) + previewPaint.maskFilter = BlurMaskFilter(calcRange(previewPaint.alpha), BlurMaskFilter.Blur.INNER) + } + companion object { + fun calcRange(value: Int): Float { + val oldRange = MAX_ALPHA_VALUE + val newRange = MAX_NEW_RANGE - MIN_NEW_RANGE + var newValue = value * newRange / oldRange + MIN_NEW_RANGE + + newValue = MAX_NEW_RANGE - newValue + MIN_NEW_RANGE + return newValue.toFloat() + } + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/BrushToolOptionsView.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/BrushToolOptionsView.kt new file mode 100644 index 00000000..c86679ed --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/BrushToolOptionsView.kt @@ -0,0 +1,57 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools.options + +import android.graphics.MaskFilter +import android.graphics.Paint +import android.graphics.Paint.Cap +import android.view.View +import org.catrobat.paintroid.tools.ToolType + +interface BrushToolOptionsView { + fun invalidate() + + fun setCurrentPaint(paint: Paint) + + fun setStrokeCapButtonChecked(strokeCap: Cap) + + fun setBrushChangedListener(onBrushChangedListener: OnBrushChangedListener) + + fun setBrushPreviewListener(onBrushPreviewListener: OnBrushPreviewListener) + + fun getTopToolOptions(): View + + fun getBottomToolOptions(): View + + fun hideCaps() + + interface OnBrushChangedListener { + fun setCap(strokeCap: Cap) + + fun setStrokeWidth(strokeWidth: Int) + } + + interface OnBrushPreviewListener { + val strokeWidth: Float + val strokeCap: Cap + val color: Int + val toolType: ToolType + val maskFilter: MaskFilter? + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/BrushToolPreview.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/BrushToolPreview.kt new file mode 100644 index 00000000..2cf4572b --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/BrushToolPreview.kt @@ -0,0 +1,27 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools.options + +import org.catrobat.paintroid.tools.options.BrushToolOptionsView.OnBrushPreviewListener + +interface BrushToolPreview { + fun setListener(callback: OnBrushPreviewListener) + + fun invalidate() +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/ClipboardToolOptionsView.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/ClipboardToolOptionsView.kt new file mode 100644 index 00000000..77a6255d --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/ClipboardToolOptionsView.kt @@ -0,0 +1,41 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools.options + +import android.view.View + +interface ClipboardToolOptionsView { + fun setCallback(callback: Callback) + + fun enablePaste(enable: Boolean) + + fun setShapeSizeText(shapeSize: String) + + fun toggleShapeSizeVisibility(isVisible: Boolean) + + fun getClipboardToolOptionsLayout(): View + + interface Callback { + fun copyClicked() + + fun cutClicked() + + fun pasteClicked() + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/FillToolOptionsView.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/FillToolOptionsView.kt new file mode 100644 index 00000000..ddbe1558 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/FillToolOptionsView.kt @@ -0,0 +1,27 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools.options + +interface FillToolOptionsView { + fun setCallback(callback: Callback) + + interface Callback { + fun onColorToleranceChanged(colorTolerance: Int) + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/ImportToolOptionsView.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/ImportToolOptionsView.kt new file mode 100644 index 00000000..1f5b372e --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/ImportToolOptionsView.kt @@ -0,0 +1,27 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2024 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools.options + +interface ImportToolOptionsView { + fun setShapeSizeText(shapeSize: String) + + fun setShapeSizeInvisble() + + fun toggleShapeSizeVisibility(isVisible: Boolean) +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/ShapeToolOptionsView.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/ShapeToolOptionsView.kt new file mode 100644 index 00000000..9e7cb562 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/ShapeToolOptionsView.kt @@ -0,0 +1,47 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools.options + +import android.view.View +import org.catrobat.paintroid.tools.drawable.DrawableShape +import org.catrobat.paintroid.tools.drawable.DrawableStyle + +interface ShapeToolOptionsView { + fun setShapeActivated(shape: DrawableShape) + + fun setDrawTypeActivated(drawType: DrawableStyle) + + fun setShapeOutlineWidth(outlineWidth: Int) + + fun setCallback(callback: Callback) + + fun setShapeSizeText(shapeSize: String) + + fun toggleShapeSizeVisibility(isVisible: Boolean) + + fun getShapeToolOptionsLayout(): View + + interface Callback { + fun setToolType(shape: DrawableShape) + + fun setDrawType(drawType: DrawableStyle) + + fun setOutlineWidth(outlineWidth: Int) + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/SmudgeToolOptionsView.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/SmudgeToolOptionsView.kt new file mode 100644 index 00000000..8ef47759 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/SmudgeToolOptionsView.kt @@ -0,0 +1,29 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools.options + +interface SmudgeToolOptionsView : BrushToolOptionsView { + fun setCallback(callback: Callback) + + interface Callback { + fun onPressureChanged(pressure: Int) + + fun onDragChanged(drag: Int) + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/SprayToolOptionsView.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/SprayToolOptionsView.kt new file mode 100644 index 00000000..5095c960 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/SprayToolOptionsView.kt @@ -0,0 +1,35 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools.options + +import android.graphics.Paint + +interface SprayToolOptionsView { + fun setCallback(callback: Callback?) + + fun setRadius(radius: Int) + + fun setCurrentPaint(paint: Paint) + + fun getRadius(): Float + + interface Callback { + fun radiusChanged(radius: Int) + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/TextToolOptionsView.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/TextToolOptionsView.kt new file mode 100644 index 00000000..948d36d1 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/TextToolOptionsView.kt @@ -0,0 +1,63 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools.options + +import android.view.View +import org.catrobat.paintroid.tools.FontType + +interface TextToolOptionsView { + fun setState( + bold: Boolean, + italic: Boolean, + underlined: Boolean, + text: String, + textSize: Int, + fontType: FontType + ) + + fun setCallback(listener: Callback) + + fun hideKeyboard() + + fun showKeyboard() + + fun getTopLayout(): View + + fun getBottomLayout(): View + + fun setShapeSizeText(shapeSize: String) + + fun toggleShapeSizeVisibility(isVisible: Boolean) + + interface Callback { + fun setText(text: String) + + fun setFont(fontType: FontType) + + fun setUnderlined(underlined: Boolean) + + fun setItalic(italic: Boolean) + + fun setBold(bold: Boolean) + + fun setTextSize(size: Int) + + fun hideToolOptions() + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/ToolOptionsViewController.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/ToolOptionsViewController.kt new file mode 100644 index 00000000..93f049d7 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/ToolOptionsViewController.kt @@ -0,0 +1,48 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools.options + +import android.view.View +import android.view.ViewGroup + +interface ToolOptionsViewController : ToolOptionsVisibilityController { + val toolSpecificOptionsLayout: ViewGroup + + fun disable() + + fun enable() + + fun disableHide() + + fun enableHide() + + fun resetToOrigin() + + fun removeToolViews() + + fun showCheckmark() + + fun hideCheckmark() + + fun slideUp(view: View, willHide: Boolean, showOptionsView: Boolean, setViewGone: Boolean = false) + + fun slideDown(view: View, willHide: Boolean, showOptionsView: Boolean, setViewGone: Boolean = false) + + fun animateBottomAndTopNavigation(hide: Boolean) +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/ToolOptionsVisibilityController.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/ToolOptionsVisibilityController.kt new file mode 100644 index 00000000..c379c138 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/ToolOptionsVisibilityController.kt @@ -0,0 +1,37 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools.options + +interface ToolOptionsVisibilityController { + val isVisible: Boolean + + fun hide() + + fun setCallback(callback: Callback) + + fun show(isFullScreen: Boolean = false) + + fun showDelayed() + + interface Callback { + fun onHide() + + fun onShow() + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/TransformToolOptionsView.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/TransformToolOptionsView.kt new file mode 100644 index 00000000..68b367b0 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/options/TransformToolOptionsView.kt @@ -0,0 +1,55 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools.options + +import org.catrobat.paintroid.ui.tools.NumberRangeFilter + +interface TransformToolOptionsView { + fun setWidthFilter(numberRangeFilter: NumberRangeFilter) + + fun setHeightFilter(numberRangeFilter: NumberRangeFilter) + + fun setCallback(callback: Callback) + + fun setWidth(width: Int) + + fun setHeight(height: Int) + + interface Callback { + fun autoCropClicked() + + fun setCenterClicked() + + fun rotateCounterClockwiseClicked() + + fun rotateClockwiseClicked() + + fun flipHorizontalClicked() + + fun flipVerticalClicked() + + fun applyResizeClicked(resizePercentage: Int) + + fun setBoxWidth(boxWidth: Float) + + fun setBoxHeight(boxHeight: Float) + + fun hideToolOptions() + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/ui/Perspective.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/ui/Perspective.kt new file mode 100644 index 00000000..a04254d4 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/ui/Perspective.kt @@ -0,0 +1,223 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.ui + +import android.graphics.Canvas +import android.graphics.PointF +import android.graphics.Rect +import androidx.annotation.VisibleForTesting +import org.catrobat.paintroid.MainActivity +import kotlin.jvm.Synchronized +import kotlin.math.max +import kotlin.math.min + +const val MIN_SCALE = 0.1f +const val MAX_SCALE = 100f +private const val SCROLL_BORDER = 50f + +open class Perspective(private var bitmapWidth: Int, private var bitmapHeight: Int) { + @JvmField + var surfaceWidth = 0 + + @JvmField + var surfaceHeight = 0 + + @VisibleForTesting + @JvmField + var surfaceCenterX = 0f + + @VisibleForTesting + @JvmField + var surfaceCenterY = 0f + + @VisibleForTesting + var surfaceScale = 1f + + @JvmField + var surfaceTranslationX = 0f + + @JvmField + var surfaceTranslationY = 0f + + @VisibleForTesting + var initialTranslationY = 0f + + @set:Synchronized + var scale: Float + get() = surfaceScale + set(scale) { + surfaceScale = max(MIN_SCALE, min(MAX_SCALE, scale)) + } + + val scaleForCenterBitmap: Float + get() { + var ratioDependentScale = 0f + val displayHeight: Int? = mainActivity?.resources?.displayMetrics?.heightPixels + var screenSizeRatio = 0f + + if (displayHeight != null) { + screenSizeRatio = surfaceWidth.toFloat() / displayHeight.toFloat() + } + + val bitmapSizeRatio = bitmapWidth.toFloat() / bitmapHeight + if (screenSizeRatio > bitmapSizeRatio) { + if (displayHeight != null) { + ratioDependentScale = displayHeight.toFloat() / bitmapHeight.toFloat() + } + } else { + ratioDependentScale = surfaceWidth.toFloat() / bitmapWidth.toFloat() + } + + ratioDependentScale = min(ratioDependentScale, 1f) + ratioDependentScale = max(ratioDependentScale, MIN_SCALE) + + return ratioDependentScale + } + + // counts to 2 at the start of the app. makes it so that the reset method will + // be called at the start of the app in Drawingsurface.kt surfaceChanged. + var callResetScaleAndTransformationOnStartUp = 0 + private var initialTranslationX = 0f + var oldHeight = 0f + var mainActivity: MainActivity? = null + + @Synchronized + fun setSurfaceFrame(surfaceFrame: Rect) { + if (surfaceHeight == 0) oldHeight = surfaceFrame.bottom.toFloat() + surfaceFrame.apply { + surfaceWidth = right + surfaceCenterX = exactCenterX() + surfaceHeight = bottom + surfaceCenterY = getExactCenterYIgnoreWindowResize(surfaceFrame.exactCenterY()) + } + } + + @Synchronized + fun setBitmapDimensions(width: Int, height: Int) { + bitmapWidth = width + bitmapHeight = height + } + + @Synchronized + fun resetScaleAndTranslation() { + surfaceScale = 1f + if (surfaceWidth == 0 || surfaceHeight == 0) { + surfaceTranslationX = 0f + surfaceTranslationY = 0f + } else { + surfaceTranslationX = surfaceWidth / 2f - bitmapWidth / 2f + initialTranslationX = surfaceTranslationX + surfaceTranslationY = surfaceHeight / 2f - bitmapHeight / 2f + initialTranslationY = surfaceTranslationY + } + val zoomFactor = calculateZoomFactor() + + surfaceScale = scaleForCenterBitmap * zoomFactor + } + + @Synchronized + fun calculateZoomFactor(): Float { + val displayHeight: Int? = mainActivity?.resources?.displayMetrics?.heightPixels + if (bitmapHeight > bitmapWidth) { + if (bitmapHeight > surfaceHeight) { + return 1.0f + } else { + if (displayHeight != null) { + return displayHeight.toFloat() / bitmapHeight.toFloat() + } else { + return surfaceHeight.toFloat() / bitmapHeight.toFloat() + } + } + } else { + if (bitmapWidth >= surfaceWidth) { + return 1.0f + } else { + return surfaceWidth.toFloat() / bitmapWidth.toFloat() + } + } + } + + @Synchronized + fun multiplyScale(factor: Float) { + scale = surfaceScale * factor + } + + @Synchronized + fun translate(dx: Float, dy: Float) { + surfaceTranslationX += dx / surfaceScale + surfaceTranslationY += dy / surfaceScale + val xmax = bitmapWidth / 2f + (surfaceWidth / 2f - SCROLL_BORDER) / surfaceScale + if (surfaceTranslationX > xmax + initialTranslationX) { + surfaceTranslationX = xmax + initialTranslationX + } else if (surfaceTranslationX < -xmax + initialTranslationX) { + surfaceTranslationX = -xmax + initialTranslationX + } + val ymax = bitmapHeight / 2f + (surfaceHeight / 2f - SCROLL_BORDER) / surfaceScale + if (surfaceTranslationY > ymax + initialTranslationY) { + surfaceTranslationY = ymax + initialTranslationY + } else if (surfaceTranslationY < -ymax + initialTranslationY) { + surfaceTranslationY = -ymax + initialTranslationY + } + } + + @Synchronized + fun convertToCanvasFromSurface(surfacePoint: PointF) { + surfacePoint.x = + (surfacePoint.x - surfaceCenterX) / surfaceScale + surfaceCenterX - surfaceTranslationX + surfacePoint.y = + (surfacePoint.y - surfaceCenterY) / surfaceScale + surfaceCenterY - surfaceTranslationY + } + + @Synchronized + fun convertToSurfaceFromCanvas(canvasPoint: PointF) { + canvasPoint.x = + (canvasPoint.x + surfaceTranslationX - surfaceCenterX) * surfaceScale + surfaceCenterX + canvasPoint.y = + (canvasPoint.y + surfaceTranslationY - surfaceCenterY) * surfaceScale + surfaceCenterY + } + + @Synchronized + fun getCanvasPointFromSurfacePoint(surfacePoint: PointF): PointF { + val canvasPoint = PointF(surfacePoint.x, surfacePoint.y) + convertToCanvasFromSurface(canvasPoint) + return canvasPoint + } + + @Synchronized + fun getSurfacePointFromCanvasPoint(canvasPoint: PointF): PointF { + val surfacePoint = PointF(canvasPoint.x, canvasPoint.y) + convertToSurfaceFromCanvas(surfacePoint) + return surfacePoint + } + + @Synchronized + fun applyToCanvas(canvas: Canvas) { + canvas.scale(surfaceScale, surfaceScale, surfaceCenterX, surfaceCenterY) + canvas.translate(surfaceTranslationX, surfaceTranslationY) + } + + private fun getExactCenterYIgnoreWindowResize(actualExactCenterY: Float): Float { + var exactCenterYIgnoreWindowResize = if (surfaceCenterY != 0.0f && surfaceCenterY > actualExactCenterY) { + surfaceCenterY + } else { + actualExactCenterY + } + return exactCenterYIgnoreWindowResize + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/ui/tools/NumberRangeFilter.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/ui/tools/NumberRangeFilter.kt new file mode 100644 index 00000000..9aa81e41 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/ui/tools/NumberRangeFilter.kt @@ -0,0 +1,35 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.ui.tools + +import android.text.InputFilter +import android.text.Spanned + +interface NumberRangeFilter : InputFilter { + var max: Int + + override fun filter( + source: CharSequence, + start: Int, + end: Int, + dest: Spanned, + dstart: Int, + dend: Int + ): CharSequence? +} diff --git a/android/app/src/main/res/drawable/ic_pocketpaint_layers.xml b/android/app/src/main/res/drawable/ic_pocketpaint_layers.xml new file mode 100644 index 00000000..7173d0ba --- /dev/null +++ b/android/app/src/main/res/drawable/ic_pocketpaint_layers.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/android/app/src/main/res/drawable/ic_pocketpaint_redo.xml b/android/app/src/main/res/drawable/ic_pocketpaint_redo.xml new file mode 100644 index 00000000..8125a34f --- /dev/null +++ b/android/app/src/main/res/drawable/ic_pocketpaint_redo.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_pocketpaint_tool_brush.xml b/android/app/src/main/res/drawable/ic_pocketpaint_tool_brush.xml new file mode 100644 index 00000000..eae56253 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_pocketpaint_tool_brush.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_pocketpaint_tool_center_focus_strong.xml b/android/app/src/main/res/drawable/ic_pocketpaint_tool_center_focus_strong.xml new file mode 100644 index 00000000..04da4972 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_pocketpaint_tool_center_focus_strong.xml @@ -0,0 +1,12 @@ + + + + diff --git a/android/app/src/main/res/drawable/ic_pocketpaint_tool_circle.png b/android/app/src/main/res/drawable/ic_pocketpaint_tool_circle.png new file mode 100644 index 0000000000000000000000000000000000000000..020878ad97fb060ffc03a367922e34d6cd111ab3 GIT binary patch literal 415 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbB*pj^6U4S$Y{B+)352QE?JR*yM zvQ&rUPlPeufHm>3#+VMlDYl$B>A_Z*Tbe9SRUR`cZz;iZgebS1W8{ z_bt%g#JucH^TKWeokJE4f(Dlssx6qjZ^<4PjivJaEtBUsUfwgWzH0XOce>(pXIiOV zE4g@FS+1ae_nUj_b|2cp&um!9y{}z-&4=gP-f+KH5Ox(08{@*DGz9d(G;HF%S9%;CK*!&@M=S*+n(+O~a$$Flyu=gTe~DWM4fo$;w0 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable/ic_pocketpaint_tool_clipboard.xml b/android/app/src/main/res/drawable/ic_pocketpaint_tool_clipboard.xml new file mode 100644 index 00000000..3cf755a3 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_pocketpaint_tool_clipboard.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/ic_pocketpaint_tool_clipping.xml b/android/app/src/main/res/drawable/ic_pocketpaint_tool_clipping.xml new file mode 100644 index 00000000..4778f274 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_pocketpaint_tool_clipping.xml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/ic_pocketpaint_tool_cursor.xml b/android/app/src/main/res/drawable/ic_pocketpaint_tool_cursor.xml new file mode 100644 index 00000000..9abf694b --- /dev/null +++ b/android/app/src/main/res/drawable/ic_pocketpaint_tool_cursor.xml @@ -0,0 +1,31 @@ + + + + + + + diff --git a/android/app/src/main/res/drawable/ic_pocketpaint_tool_eraser.xml b/android/app/src/main/res/drawable/ic_pocketpaint_tool_eraser.xml new file mode 100644 index 00000000..66c9e1ff --- /dev/null +++ b/android/app/src/main/res/drawable/ic_pocketpaint_tool_eraser.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_pocketpaint_tool_fill.xml b/android/app/src/main/res/drawable/ic_pocketpaint_tool_fill.xml new file mode 100644 index 00000000..566b535b --- /dev/null +++ b/android/app/src/main/res/drawable/ic_pocketpaint_tool_fill.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_pocketpaint_tool_flip_horizontal.xml b/android/app/src/main/res/drawable/ic_pocketpaint_tool_flip_horizontal.xml new file mode 100644 index 00000000..43b4a196 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_pocketpaint_tool_flip_horizontal.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_pocketpaint_tool_flip_vertical.xml b/android/app/src/main/res/drawable/ic_pocketpaint_tool_flip_vertical.xml new file mode 100644 index 00000000..8bf61990 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_pocketpaint_tool_flip_vertical.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_pocketpaint_tool_hand.xml b/android/app/src/main/res/drawable/ic_pocketpaint_tool_hand.xml new file mode 100644 index 00000000..829d3e9b --- /dev/null +++ b/android/app/src/main/res/drawable/ic_pocketpaint_tool_hand.xml @@ -0,0 +1,12 @@ + + + + diff --git a/android/app/src/main/res/drawable/ic_pocketpaint_tool_import.xml b/android/app/src/main/res/drawable/ic_pocketpaint_tool_import.xml new file mode 100644 index 00000000..804484a0 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_pocketpaint_tool_import.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_pocketpaint_tool_line.xml b/android/app/src/main/res/drawable/ic_pocketpaint_tool_line.xml new file mode 100644 index 00000000..691c964e --- /dev/null +++ b/android/app/src/main/res/drawable/ic_pocketpaint_tool_line.xml @@ -0,0 +1,12 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_pocketpaint_tool_pipette.xml b/android/app/src/main/res/drawable/ic_pocketpaint_tool_pipette.xml new file mode 100644 index 00000000..9b254c71 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_pocketpaint_tool_pipette.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_pocketpaint_tool_rectangle.xml b/android/app/src/main/res/drawable/ic_pocketpaint_tool_rectangle.xml new file mode 100644 index 00000000..4939b243 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_pocketpaint_tool_rectangle.xml @@ -0,0 +1,14 @@ + + + + diff --git a/android/app/src/main/res/drawable/ic_pocketpaint_tool_resize_adjust.xml b/android/app/src/main/res/drawable/ic_pocketpaint_tool_resize_adjust.xml new file mode 100644 index 00000000..6ec0ddbc --- /dev/null +++ b/android/app/src/main/res/drawable/ic_pocketpaint_tool_resize_adjust.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_pocketpaint_tool_rotate_left.xml b/android/app/src/main/res/drawable/ic_pocketpaint_tool_rotate_left.xml new file mode 100644 index 00000000..19ab4aa6 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_pocketpaint_tool_rotate_left.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/android/app/src/main/res/drawable/ic_pocketpaint_tool_smudge.xml b/android/app/src/main/res/drawable/ic_pocketpaint_tool_smudge.xml new file mode 100644 index 00000000..4f772765 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_pocketpaint_tool_smudge.xml @@ -0,0 +1,12 @@ + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/ic_pocketpaint_tool_spray_can.xml b/android/app/src/main/res/drawable/ic_pocketpaint_tool_spray_can.xml new file mode 100644 index 00000000..597e942c --- /dev/null +++ b/android/app/src/main/res/drawable/ic_pocketpaint_tool_spray_can.xml @@ -0,0 +1,44 @@ + + + + + + + diff --git a/android/app/src/main/res/drawable/ic_pocketpaint_tool_square.png b/android/app/src/main/res/drawable/ic_pocketpaint_tool_square.png new file mode 100644 index 0000000000000000000000000000000000000000..c7c0a45ad1bcee2114c4a195af9ef71e934bb1d4 GIT binary patch literal 157 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjY)RhkE(}6TtKSPDj(q%x7hNp{Th{y5d1PN9a24$)L_FK=UF51fPrYo^E i;Yw2=H<%$Jz{)V;Dx*sH%bFsf9tKZWKbLh*2~7Z**d);a literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable/ic_pocketpaint_tool_text.xml b/android/app/src/main/res/drawable/ic_pocketpaint_tool_text.xml new file mode 100644 index 00000000..751b78c9 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_pocketpaint_tool_text.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_pocketpaint_tool_transform.xml b/android/app/src/main/res/drawable/ic_pocketpaint_tool_transform.xml new file mode 100644 index 00000000..abf0ce8a --- /dev/null +++ b/android/app/src/main/res/drawable/ic_pocketpaint_tool_transform.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_pocketpaint_tool_watercolor.xml b/android/app/src/main/res/drawable/ic_pocketpaint_tool_watercolor.xml new file mode 100644 index 00000000..17042928 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_pocketpaint_tool_watercolor.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/ic_pocketpaint_undo.xml b/android/app/src/main/res/drawable/ic_pocketpaint_undo.xml new file mode 100644 index 00000000..c7e9eddc --- /dev/null +++ b/android/app/src/main/res/drawable/ic_pocketpaint_undo.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/values/string.xml b/android/app/src/main/res/values/string.xml new file mode 100644 index 00000000..280f901d --- /dev/null +++ b/android/app/src/main/res/values/string.xml @@ -0,0 +1,253 @@ + + + + Pocket Paint + Brush + Cursor + Pipette + Undo + Redo + Information + Clipboard + Import image + Eraser + Transform + Fill + Line + Text + Shapes + Hand + Watercolor + Spray can + Smudge + Clip area + Apply + Checkmark + Done + + Gallery + + Stickers not available, check your internet connection. + + Paste + Copy + Cut + + Stickers + @string/button_import_image + @string/menu_save_image + Tools + Error load/save file + Check image or SD-card! + Stroke width + Save changes? + Help + Remove parts of the image like with an eraser. + Tap on the symbols on the bottom bar to change the color or the brush size. + Similar to the brush tool with a watercolor effect. However you can also change the strength of the brush with the slider in the color menu. + Tap on the image to select a color. + Tap to undo your previous action. + Tap to redo an undone action. + Tap on the image to fill an area with the selected color. + Position the cursor where you want to draw. Tap to activate the cursor. Move your finger to draw. Tap again to deactivate. + Use to transform the image. + Move and resize the rectangle to cover the area you want to stamp. Tap on copy or cut to select the area. Move it, then tap on paste to stamp. + Import an image from the gallery to the clipboard. + Draw a straight line. + Write text and format it. Resize the text box afterwards. Tap on the checkmark to insert the text on the image. + Choose a shape and tap on the checkmark to insert the selected shape. + Create new layers or modify existing ones. + Select or adjust a color. + Move your finger to move the canvas. + Move your finger on the image to create a spray can pattern. + Move your finger on the image on different drawings to smudge them. + Mark area which should not be erased. + Quit + Save changes? + New image + Discard image + You are only able to merge or reorder if all layers are visible + No tools are available on hidden layer + Load image + Replace image + Add to current layer + Save image + Save copy + Hide buttons + Share image + Send image via + @string/pocketpaint_about_title + Rate us! + Export + Feedback + Advanced settings + Zoom window settings + Image saved to\n + Image saved + Copy saved to\n + Copy saved + Quit + Save + Discard + Cancel + Overwrite + + nothing to resize + cannot resize to this size + max image resolution reached + + U + I + B + Tap here to write + Monospace + Serif + Sans Serif + Dubai + STC + + Rectangle + Ellipse + Star + Heart + Fill + Outline + + Color tolerance + + Pressure + Drag + + rotate left + rotate right + flip vertical + flip horizontal + resize + crop/enlarge + Width + Height + Auto + Set center + px + + Tap on copy to copy content + + Layers + New layer + Delete layer + Too many layers + Layers merged + Layer background + Layer preview + + Error on loading image + Not a valid image + Settings + + Image name + Image format + + Antialiasing + Smoothing + + Enabled + 100 + 300 + 100 + + Quality: + + jpg + Takes up minimal storage space. No transparency is remembered. + png + Lossless compression. Transparency is preserved. + Ora + This format remembers layers. It can be opened by apps that support the Openraster format. + catrobat-image + Pocket Paint\'s native image format. This format remembers commands and layers. + + + This app needs the requested permission to function properly. In order to save images to the local memory, the app needs read and write access to it. + This app needs the requested permission to function properly. In order to save images to the local memory, the app needs read and write access to it. + As you have denied permission with do not ask again, please go to your phone settings and grant the required permissions if you wish to use the associated functions. + + Tap the screen to define the new center position. + Drag edges to their new position, tap on the checkmark to enlarge or crop the image area. + Pan to position, then tap to start painting. + Pan to draw, then tap again to stop painting. + Welcome To Pocket Paint + With Pocket Paint there are no limits to your creativity. If you are new, start the intro, or skip it if you are already familiar with Pocket Paint. + Tap on a tool to get more information + More possibilities + Use the top bar to open the overflow menu and to undo or redo changes + Landscape + Pocket Paint also supports drawing in landscape mode to give you the best painting experience. + You are all set. Enjoy Pocket Paint. + Get started and create a new masterpiece. + Let\'s go + Next + Skip + + About + Version %s + GNU Affero General Public License, v3 + Pocket Paint is a picture editing library that is part of the Catrobat project.\n\nCatrobat is a visual programming language and set of creativity tools for smartphones.\n\nThe source code of Pocket Paint is mainly licensed under the %s.\nFor precise details of the license see the link below. + Pocket Paint source code license + About Catrobat + <a href=\"https://catrob.at/licenses\">%s</a> + <a href=\"https://catrobat.org/\">%s</a> + + Intro + + Intro does not support split screen. + + Overwrite File? + You are about to overwrite an existing image (hint: To save it as a new image, use the \"%s\" option). Save anyway? + + Do you like Pocket Paint? + Would you like to rate Pocket Paint? + We are sorry to hear that. If you want to share your experience with us, please write to contact@catrobat.org + Yes + No + @android:string/ok + Cancel + Not now + Rate Pocket Paint + + Tools + Current + Color + Layers + Item for bottom navigation + + Switch to the tool you want to use. + Shows the currently used tool and opens its options. + Shows the currently used color and opens the color picker. + Opens the layer menu and lets you manage your layers. + + Current tool icon + + Image is too big to load + The image is too big to load. Tap OK to scale down the image automatically. + + Used to display a zoomed in part of the drawing surface + + From c279a8917ce96a23350a1bea024fded9c84ea180 Mon Sep 17 00:00:00 2001 From: Tim Celec Date: Sat, 3 Aug 2024 11:20:49 +0200 Subject: [PATCH 12/25] PAINTROID-761 - Layers work --- .../org/catrobat/paintroid/FileReader.kt | 9 +++- .../implementation/AddEmptyLayerCommand.kt | 44 ++++++++++++++++++ .../implementation/SelectLayerCommand.kt | 37 +++++++++++++++ .../AddLayerCommandSerializer.kt | 37 +++++++++++++++ .../SelectLayerCommandSerializer.kt | 38 ++++++++++++++++ .../paintroid/common/CommonFactory.kt | 45 +++++++++++++++++++ 6 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/AddEmptyLayerCommand.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/SelectLayerCommand.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/AddLayerCommandSerializer.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/SelectLayerCommandSerializer.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/common/CommonFactory.kt diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt index d17aba60..c6ba7fec 100644 --- a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt @@ -21,6 +21,11 @@ import org.catrobat.paintroid.command.serialization.SetDimensionCommandSerialize import org.catrobat.paintroid.command.serialization.SprayCommandSerializer import org.catrobat.paintroid.command.serialization.PaintSerializer +import org.catrobat.paintroid.command.implementation.AddEmptyLayerCommand +import org.catrobat.paintroid.command.serialization.AddLayerCommandSerializer + +import org.catrobat.paintroid.command.implementation.SelectLayerCommand +import org.catrobat.paintroid.command.serialization.SelectLayerCommandSerializer import android.graphics.Paint import android.graphics.Point @@ -62,9 +67,9 @@ class FileReader(private val context : Context) put(SetDimensionCommand::class.java, SetDimensionCommandSerializer(version)) put(SprayCommand::class.java, SprayCommandSerializer(version)) put(Paint::class.java, PaintSerializer(version, activityContext)) // maybe will cause a crash ? activityContext is lateinnit - /* put(AddEmptyLayerCommand::class.java, AddLayerCommandSerializer(version)) + put(AddEmptyLayerCommand::class.java, AddLayerCommandSerializer(version)) put(SelectLayerCommand::class.java, SelectLayerCommandSerializer(version)) - put(LoadCommand::class.java, LoadCommandSerializer(version)) + /* put(LoadCommand::class.java, LoadCommandSerializer(version)) put(TextToolCommand::class.java, TextToolCommandSerializer(version, activityContext)) put(Array::class.java, DataStructuresSerializer.StringArraySerializer(version)) put(FillCommand::class.java, FillCommandSerializer(version)) diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/AddEmptyLayerCommand.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/AddEmptyLayerCommand.kt new file mode 100644 index 00000000..f711a1c6 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/AddEmptyLayerCommand.kt @@ -0,0 +1,44 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.implementation + +import android.graphics.Bitmap +import android.graphics.Canvas +import org.catrobat.paintroid.command.Command +import org.catrobat.paintroid.common.CommonFactory +import org.catrobat.paintroid.contract.LayerContracts +import org.catrobat.paintroid.model.Layer + +class AddEmptyLayerCommand(private val commonFactory: CommonFactory) : Command { + + override fun run(canvas: Canvas, layerModel: LayerContracts.Model) { + val layer = Layer( + commonFactory.createBitmap( + layerModel.width, layerModel.height, + Bitmap.Config.ARGB_8888 + ) + ) + layerModel.addLayerAt(0, layer) + layerModel.currentLayer = layer + } + + override fun freeResources() { + // No resources to free + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/SelectLayerCommand.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/SelectLayerCommand.kt new file mode 100644 index 00000000..d476ad10 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/SelectLayerCommand.kt @@ -0,0 +1,37 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.paintroid.command.implementation + +import android.graphics.Canvas +import org.catrobat.paintroid.command.Command +import org.catrobat.paintroid.contract.LayerContracts + +class SelectLayerCommand(position: Int) : Command { + + var position = position; private set + + override fun run(canvas: Canvas, layerModel: LayerContracts.Model) { + layerModel.currentLayer = layerModel.layers[position] + } + + override fun freeResources() { + // No resources to free + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/AddLayerCommandSerializer.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/AddLayerCommandSerializer.kt new file mode 100644 index 00000000..7574d019 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/AddLayerCommandSerializer.kt @@ -0,0 +1,37 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.serialization + +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import org.catrobat.paintroid.command.implementation.AddEmptyLayerCommand +import org.catrobat.paintroid.common.CommonFactory + +class AddLayerCommandSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, command: AddEmptyLayerCommand) { + // Has no member variables to save + } + + override fun read(kryo: Kryo, input: Input, type: Class): AddEmptyLayerCommand = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): AddEmptyLayerCommand = + AddEmptyLayerCommand(CommonFactory()) +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/SelectLayerCommandSerializer.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/SelectLayerCommandSerializer.kt new file mode 100644 index 00000000..d443ce84 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/SelectLayerCommandSerializer.kt @@ -0,0 +1,38 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.serialization + +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import org.catrobat.paintroid.command.implementation.SelectLayerCommand + +class SelectLayerCommandSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, command: SelectLayerCommand) { + output.writeInt(command.position) + } + + override fun read(kryo: Kryo, input: Input, type: Class): SelectLayerCommand = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): SelectLayerCommand { + val position = input.readInt() + return SelectLayerCommand(position) + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/common/CommonFactory.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/common/CommonFactory.kt new file mode 100644 index 00000000..cf002e94 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/common/CommonFactory.kt @@ -0,0 +1,45 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.common + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Point +import android.graphics.PointF +import android.graphics.RectF +import org.catrobat.paintroid.command.serialization.SerializablePath + +open class CommonFactory { + open fun createCanvas() = Canvas() + + open fun createBitmap(width: Int, height: Int, config: Bitmap.Config): Bitmap = + Bitmap.createBitmap(width, height, config) + + fun createPaint(paint: Paint?) = Paint(paint) + + fun createPointF(point: PointF) = PointF(point.x, point.y) + + fun createPoint(x: Int, y: Int) = Point(x, y) + + open fun createSerializablePath(path: SerializablePath): SerializablePath = + SerializablePath(path) + + fun createRectF(rect: RectF?) = RectF(rect) +} From 418cde153fcf499100d558f7a34f79fd9c28f5ff Mon Sep 17 00:00:00 2001 From: Tim Celec Date: Sat, 3 Aug 2024 11:22:38 +0200 Subject: [PATCH 13/25] PAINTROID-761 - Load work --- .../org/catrobat/paintroid/FileReader.kt | 7 ++- .../command/implementation/LoadCommand.kt | 41 +++++++++++++++++ .../serialization/LoadCommandSerializer.kt | 45 +++++++++++++++++++ 3 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/LoadCommand.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/LoadCommandSerializer.kt diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt index c6ba7fec..3345aa1e 100644 --- a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt @@ -27,6 +27,9 @@ import org.catrobat.paintroid.command.serialization.AddLayerCommandSerializer import org.catrobat.paintroid.command.implementation.SelectLayerCommand import org.catrobat.paintroid.command.serialization.SelectLayerCommandSerializer +import org.catrobat.paintroid.command.implementation.LoadCommand +import org.catrobat.paintroid.command.serialization.LoadCommandSerializer + import android.graphics.Paint import android.graphics.Point import android.graphics.PointF @@ -69,8 +72,8 @@ class FileReader(private val context : Context) put(Paint::class.java, PaintSerializer(version, activityContext)) // maybe will cause a crash ? activityContext is lateinnit put(AddEmptyLayerCommand::class.java, AddLayerCommandSerializer(version)) put(SelectLayerCommand::class.java, SelectLayerCommandSerializer(version)) - /* put(LoadCommand::class.java, LoadCommandSerializer(version)) - put(TextToolCommand::class.java, TextToolCommandSerializer(version, activityContext)) + put(LoadCommand::class.java, LoadCommandSerializer(version)) + /* put(TextToolCommand::class.java, TextToolCommandSerializer(version, activityContext)) put(Array::class.java, DataStructuresSerializer.StringArraySerializer(version)) put(FillCommand::class.java, FillCommandSerializer(version)) put(FlipCommand::class.java, FlipCommandSerializer(version)) diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/LoadCommand.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/LoadCommand.kt new file mode 100644 index 00000000..b1ff89bb --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/LoadCommand.kt @@ -0,0 +1,41 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.paintroid.command.implementation + +import android.graphics.Bitmap +import android.graphics.Canvas +import org.catrobat.paintroid.command.Command +import org.catrobat.paintroid.contract.LayerContracts +import org.catrobat.paintroid.model.Layer + +class LoadCommand(loadedBitmap: Bitmap) : Command { + + var loadedBitmap = loadedBitmap; private set + + override fun run(canvas: Canvas, layerModel: LayerContracts.Model) { + val currentLayer = Layer(loadedBitmap.copy(Bitmap.Config.ARGB_8888, true)) + layerModel.addLayerAt(0, currentLayer) + layerModel.currentLayer = currentLayer + } + + override fun freeResources() { + // No resources to free + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/LoadCommandSerializer.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/LoadCommandSerializer.kt new file mode 100644 index 00000000..1ddba215 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/LoadCommandSerializer.kt @@ -0,0 +1,45 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.serialization + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import org.catrobat.paintroid.command.implementation.LoadCommand + +class LoadCommandSerializer(version: Int) : VersionSerializer(version) { + + companion object { + private const val COMPRESSION_QUALITY = 100 + } + + override fun write(kryo: Kryo, output: Output, command: LoadCommand) { + command.loadedBitmap.compress(Bitmap.CompressFormat.PNG, COMPRESSION_QUALITY, output) + } + + override fun read(kryo: Kryo, input: Input, type: Class): LoadCommand = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): LoadCommand { + val bitmap = BitmapFactory.decodeStream(input) + return LoadCommand(bitmap) + } +} From 6d5d9b1da4d8042490dec2aaf2231b130bb98dd6 Mon Sep 17 00:00:00 2001 From: Tim Celec Date: Sat, 3 Aug 2024 11:36:49 +0200 Subject: [PATCH 14/25] PAINTROID-761 - TextToolCommand work --- .../org/catrobat/paintroid/FileReader.kt | 7 +- .../command/implementation/TextToolCommand.kt | 92 ++++++++++++++++++ .../TextToolCommandSerializer.kt | 87 +++++++++++++++++ android/app/src/main/res/font/dubai.ttf | Bin 0 -> 181484 bytes android/app/src/main/res/font/stc_regular.otf | Bin 0 -> 21448 bytes 5 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/TextToolCommand.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/TextToolCommandSerializer.kt create mode 100644 android/app/src/main/res/font/dubai.ttf create mode 100644 android/app/src/main/res/font/stc_regular.otf diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt index 3345aa1e..31eebdb9 100644 --- a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt @@ -30,6 +30,9 @@ import org.catrobat.paintroid.command.serialization.SelectLayerCommandSerializer import org.catrobat.paintroid.command.implementation.LoadCommand import org.catrobat.paintroid.command.serialization.LoadCommandSerializer +import org.catrobat.paintroid.command.implementation.TextToolCommand +import org.catrobat.paintroid.command.serialization.TextToolCommandSerializer + import android.graphics.Paint import android.graphics.Point import android.graphics.PointF @@ -73,8 +76,8 @@ class FileReader(private val context : Context) put(AddEmptyLayerCommand::class.java, AddLayerCommandSerializer(version)) put(SelectLayerCommand::class.java, SelectLayerCommandSerializer(version)) put(LoadCommand::class.java, LoadCommandSerializer(version)) - /* put(TextToolCommand::class.java, TextToolCommandSerializer(version, activityContext)) - put(Array::class.java, DataStructuresSerializer.StringArraySerializer(version)) + put(TextToolCommand::class.java, TextToolCommandSerializer(version, activityContext)) + /* put(Array::class.java, DataStructuresSerializer.StringArraySerializer(version)) put(FillCommand::class.java, FillCommandSerializer(version)) put(FlipCommand::class.java, FlipCommandSerializer(version)) put(CropCommand::class.java, CropCommandSerializer(version)) diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/TextToolCommand.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/TextToolCommand.kt new file mode 100644 index 00000000..713420f8 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/TextToolCommand.kt @@ -0,0 +1,92 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.paintroid.command.implementation + +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.PointF +import org.catrobat.paintroid.command.Command +import org.catrobat.paintroid.command.serialization.SerializableTypeface +import org.catrobat.paintroid.common.ITALIC_FONT_BOX_ADJUSTMENT +import org.catrobat.paintroid.contract.LayerContracts + +class TextToolCommand( + multilineText: Array, + textPaint: Paint, + boxOffset: Float, + boxWidth: Float, + boxHeight: Float, + toolPosition: PointF, + rotationAngle: Float, + typeFaceInfo: SerializableTypeface +) : Command { + + var multilineText = multilineText.clone(); private set + var textPaint = textPaint; private set + var boxOffset = boxOffset; private set + var boxWidth = boxWidth; private set + var boxHeight = boxHeight; private set + var toolPosition = toolPosition; private set + var rotationAngle = rotationAngle; private set + var typeFaceInfo = typeFaceInfo; private set + + override fun run(canvas: Canvas, layerModel: LayerContracts.Model) { + val textAscent = textPaint.ascent() + val textDescent = textPaint.descent() + val textHeight = (textDescent - textAscent) * multilineText.size + val lineHeight = textHeight / multilineText.size + var maxTextWidth = multilineText.maxOf { line -> + textPaint.measureText(line) + } + + if (typeFaceInfo.italic) { + maxTextWidth *= ITALIC_FONT_BOX_ADJUSTMENT + } + + with(canvas) { + save() + translate(toolPosition.x, toolPosition.y) + rotate(rotationAngle) + + val widthScaling = (boxWidth - 2 * boxOffset) / maxTextWidth + val heightScaling = (boxHeight - 2 * boxOffset) / textHeight + canvas.scale(widthScaling, heightScaling) + + val scaledHeightOffset = boxOffset / heightScaling + val scaledWidthOffset = boxOffset / widthScaling + val scaledBoxWidth = boxWidth / widthScaling + val scaledBoxHeight = boxHeight / heightScaling + + multilineText.forEachIndexed { index, textLine -> + canvas.drawText( + textLine, + scaledWidthOffset - scaledBoxWidth / 2 / if (typeFaceInfo.italic) ITALIC_FONT_BOX_ADJUSTMENT else 1f, + -(scaledBoxHeight / 2) + scaledHeightOffset - textAscent + lineHeight * index, + textPaint + ) + } + restore() + } + } + + override fun freeResources() { + // No resources to free + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/TextToolCommandSerializer.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/TextToolCommandSerializer.kt new file mode 100644 index 00000000..466ca5b5 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/TextToolCommandSerializer.kt @@ -0,0 +1,87 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.serialization + +import android.content.Context +import android.graphics.Paint +import android.graphics.PointF +import android.graphics.Typeface +import android.util.Log +import androidx.core.content.res.ResourcesCompat +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import org.catrobat.paintroid.R +import org.catrobat.paintroid.command.implementation.TextToolCommand +import org.catrobat.paintroid.tools.FontType + +class TextToolCommandSerializer(version: Int, private val activityContext: Context) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, command: TextToolCommand) { + with(kryo) { + with(output) { + writeObject(output, command.multilineText) + writeObject(output, command.textPaint) + writeFloat(command.boxOffset) + writeFloat(command.boxWidth) + writeFloat(command.boxHeight) + writeObject(output, command.toolPosition) + writeFloat(command.rotationAngle) + writeObject(output, command.typeFaceInfo) + } + } + } + + override fun read(kryo: Kryo, input: Input, type: Class): TextToolCommand = + super.handleVersions(this, kryo, input, type) + + @Suppress("Detekt.TooGenericExceptionCaught") + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): TextToolCommand { + return with(input) { + val text = kryo.readObject(input, Array::class.java) + val paint = kryo.readObject(input, Paint::class.java) + val offset = readFloat() + val width = readFloat() + val height = readFloat() + val position = kryo.readObject(input, PointF::class.java) + val rotation = readFloat() + val typeFaceInfo = kryo.readObject(input, SerializableTypeface::class.java) + + paint.apply { + isFakeBoldText = typeFaceInfo.bold + isUnderlineText = typeFaceInfo.underline + textSize = typeFaceInfo.textSize + textSkewX = typeFaceInfo.textSkewX + val style = if (typeFaceInfo.italic) Typeface.ITALIC else Typeface.NORMAL + typeface = try { + when (typeFaceInfo.font) { + FontType.SANS_SERIF -> Typeface.create(Typeface.SANS_SERIF, style) + FontType.SERIF -> Typeface.create(Typeface.SERIF, style) + FontType.MONOSPACE -> Typeface.create(Typeface.MONOSPACE, style) + FontType.STC -> ResourcesCompat.getFont(activityContext, R.font.stc_regular) + FontType.DUBAI -> ResourcesCompat.getFont(activityContext, R.font.dubai) + } + } catch (e: Exception) { + Log.e("LoadImageAsync", "Typeface not supported on this mobile phone") + Typeface.create(Typeface.SANS_SERIF, style) + } + } + TextToolCommand(text, paint, offset, width, height, position, rotation, typeFaceInfo) + } + } +} diff --git a/android/app/src/main/res/font/dubai.ttf b/android/app/src/main/res/font/dubai.ttf new file mode 100644 index 0000000000000000000000000000000000000000..9d9cea18ffa0cdf7b659da4c2460d9e63c42e944 GIT binary patch literal 181484 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 bgr?W_T8_0*_p>eS2v`#L?R4r7I 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

>=NFf5QdJiJW|@=@Isp=|YXxs9_$!mV# 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 Date: Sat, 3 Aug 2024 11:42:34 +0200 Subject: [PATCH 15/25] PAINTROID-761 - FillCommand work --- .../org/catrobat/paintroid/FileReader.kt | 9 +- .../command/implementation/FillCommand.kt | 55 ++++++ .../paintroid/tools/helper/FillAlgorithm.kt | 34 ++++ .../tools/helper/FillAlgorithmFactory.kt | 23 +++ .../tools/helper/JavaFillAlgorithm.kt | 179 ++++++++++++++++++ .../tools/helper/JavaFillAlgorithmFactory.kt | 23 +++ 6 files changed, 320 insertions(+), 3 deletions(-) create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/FillCommand.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/helper/FillAlgorithm.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/helper/FillAlgorithmFactory.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/helper/JavaFillAlgorithm.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/helper/JavaFillAlgorithmFactory.kt diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt index 31eebdb9..22d4de1f 100644 --- a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt @@ -33,6 +33,9 @@ import org.catrobat.paintroid.command.serialization.LoadCommandSerializer import org.catrobat.paintroid.command.implementation.TextToolCommand import org.catrobat.paintroid.command.serialization.TextToolCommandSerializer +import org.catrobat.paintroid.command.implementation.FillCommand +import org.catrobat.paintroid.command.serialization.FillCommandSerializer + import android.graphics.Paint import android.graphics.Point import android.graphics.PointF @@ -77,10 +80,10 @@ class FileReader(private val context : Context) put(SelectLayerCommand::class.java, SelectLayerCommandSerializer(version)) put(LoadCommand::class.java, LoadCommandSerializer(version)) put(TextToolCommand::class.java, TextToolCommandSerializer(version, activityContext)) - /* put(Array::class.java, DataStructuresSerializer.StringArraySerializer(version)) + put(Array::class.java, DataStructuresSerializer.StringArraySerializer(version)) put(FillCommand::class.java, FillCommandSerializer(version)) - put(FlipCommand::class.java, FlipCommandSerializer(version)) - put(CropCommand::class.java, CropCommandSerializer(version)) + // put(FlipCommand::class.java, FlipCommandSerializer(version)) + /* put(CropCommand::class.java, CropCommandSerializer(version)) put(CutCommand::class.java, CutCommandSerializer(version)) put(ResizeCommand::class.java, ResizeCommandSerializer(version)) put(RotateCommand::class.java, RotateCommandSerializer(version)) diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/FillCommand.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/FillCommand.kt new file mode 100644 index 00000000..d3a9bc13 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/FillCommand.kt @@ -0,0 +1,55 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.paintroid.command.implementation + +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Point +import org.catrobat.paintroid.command.Command +import org.catrobat.paintroid.contract.LayerContracts +import org.catrobat.paintroid.tools.helper.FillAlgorithmFactory + +class FillCommand(private val fillAlgorithmFactory: FillAlgorithmFactory, clickedPixel: Point, paint: Paint, colorTolerance: Float) : Command { + + var clickedPixel = clickedPixel; private set + var paint = paint; private set + var colorTolerance = colorTolerance; private set + + override fun run(canvas: Canvas, layerModel: LayerContracts.Model) { + val currentLayer = layerModel.currentLayer + currentLayer ?: return + currentLayer.bitmap.let { bitmap -> + val colorToBeReplaced = bitmap.getPixel(clickedPixel.x, clickedPixel.y) + val fillAlgorithm = fillAlgorithmFactory.createFillAlgorithm() + fillAlgorithm.setParameters( + bitmap, + clickedPixel, + paint.color, + colorToBeReplaced, + colorTolerance + ) + fillAlgorithm.performFilling() + } + } + + override fun freeResources() { + // No resources to free + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/helper/FillAlgorithm.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/helper/FillAlgorithm.kt new file mode 100644 index 00000000..43f2ac16 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/helper/FillAlgorithm.kt @@ -0,0 +1,34 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools.helper + +import android.graphics.Bitmap +import android.graphics.Point + +interface FillAlgorithm { + fun setParameters( + bitmap: Bitmap, + clickedPixel: Point, + targetColor: Int, + replacementColor: Int, + colorToleranceThreshold: Float + ) + + fun performFilling() +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/helper/FillAlgorithmFactory.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/helper/FillAlgorithmFactory.kt new file mode 100644 index 00000000..809cadf9 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/helper/FillAlgorithmFactory.kt @@ -0,0 +1,23 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools.helper + +interface FillAlgorithmFactory { + fun createFillAlgorithm(): FillAlgorithm +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/helper/JavaFillAlgorithm.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/helper/JavaFillAlgorithm.kt new file mode 100644 index 00000000..9b461ba1 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/helper/JavaFillAlgorithm.kt @@ -0,0 +1,179 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools.helper + +import android.graphics.Bitmap +import android.graphics.Color +import android.graphics.Point +import androidx.annotation.VisibleForTesting +import java.util.LinkedList +import java.util.Queue + +private const val UP = true +private const val DOWN = false + +class JavaFillAlgorithm : FillAlgorithm { + @VisibleForTesting + lateinit var pixels: Array + + @VisibleForTesting + lateinit var clickedPixel: Point + + @VisibleForTesting + val ranges: Queue = LinkedList() + + @VisibleForTesting + var targetColor = 0 + + @VisibleForTesting + var colorToBeReplaced = 0 + + @VisibleForTesting + var colorToleranceThresholdSquared = 0 + + private lateinit var filledPixels: Array + private lateinit var bitmap: Bitmap + private var considerTolerance = false + private var width = 0 + private var height = 0 + + override fun setParameters( + bitmap: Bitmap, + clickedPixel: Point, + targetColor: Int, + replacementColor: Int, + colorToleranceThreshold: Float + ) { + this.bitmap = bitmap + this.width = bitmap.width + this.height = bitmap.height + pixels = Array(bitmap.height) { IntArray(bitmap.width) } + for (i in 0 until height) { + this.bitmap.getPixels(pixels[i], 0, width, 0, i, width, 1) + } + filledPixels = Array(bitmap.height) { BooleanArray(bitmap.width) } + this.clickedPixel = clickedPixel + this.targetColor = targetColor + this.colorToBeReplaced = replacementColor + colorToleranceThresholdSquared = square(colorToleranceThreshold.toInt()) + considerTolerance = colorToleranceThreshold > 0 + } + + override fun performFilling() { + var range = generateRangeAndReplaceColor( + clickedPixel.y, clickedPixel.x, UP + ) + ranges.add(range) + ranges.add(Range(range.line, range.start, range.end, DOWN)) + var row: Int + while (!ranges.isEmpty()) { + range = ranges.poll() ?: break + val direction = range.direction + val diff = if (direction == UP) -1 else 1 + row = range.line + diff + if (row in 0 until height) { + checkRangeAndGenerateNewRanges(range, row, direction) + } + } + } + + private fun square(x: Int) = x * x + + private fun shouldCellBeFilled(row: Int, col: Int): Boolean = + !filledPixels[row][col] && ( + pixels[row][col] == colorToBeReplaced || considerTolerance && isPixelWithinColorTolerance( + pixels[row][col], + colorToBeReplaced + ) + ) + + private fun validateAndAssign(row: Int, col: Int): Boolean = if (shouldCellBeFilled(row, col)) { + pixels[row][col] = targetColor + filledPixels[row][col] = true + true + } else false + + private fun getStartIndex(row: Int, col: Int): Int { + val start = (col downTo 0).find { !validateAndAssign(row, it) } ?: -1 + return start + 1 + } + + private fun getEndIndex(row: Int, col: Int): Int { + val end = (col until width).find { !validateAndAssign(row, it) } ?: width + return end - 1 + } + + private fun generateRangeAndReplaceColor(row: Int, col: Int, direction: Boolean): Range { + val range = Range() + pixels[row][col] = targetColor + filledPixels[row][col] = true + + val start = getStartIndex(row, col - 1) + val end = getEndIndex(row, col + 1) + range.apply { + line = row + this.end = end + this.start = start + this.direction = direction + } + bitmap.setPixels(pixels[row], start, width, start, row, end - start + 1, 1) + return range + } + + private fun checkRangeAndGenerateNewRanges(range: Range, row: Int, directionUp: Boolean) { + var newRange: Range + var col = range.start + while (col <= range.end) { + if (shouldCellBeFilled(row, col)) { + newRange = generateRangeAndReplaceColor(row, col, directionUp) + ranges.add(newRange) + if (newRange.start <= range.start - 2) { + ranges.add(Range(row, newRange.start, range.start - 2, !directionUp)) + } + if (newRange.end >= range.end + 2) { + ranges.add(Range(row, range.end + 2, newRange.end, !directionUp)) + } + col = if (newRange.end >= range.end - 1) { + break + } else { + newRange.end + 1 + } + } + col++ + } + } + + private fun isPixelWithinColorTolerance(pixel: Int, referenceColor: Int): Boolean { + val redDiff = Color.red(pixel) - Color.red(referenceColor) + val greenDiff = Color.green(pixel) - Color.green(referenceColor) + val blueDiff = Color.blue(pixel) - Color.blue(referenceColor) + val alphaDiff = Color.alpha(pixel) - Color.alpha(referenceColor) + return ( + square(redDiff) + square(greenDiff) + square(blueDiff) + square(alphaDiff) + <= colorToleranceThresholdSquared + ) + } + + data class Range( + var line: Int = 0, + var start: Int = 0, + var end: Int = 0, + var direction: Boolean = false + ) +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/helper/JavaFillAlgorithmFactory.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/helper/JavaFillAlgorithmFactory.kt new file mode 100644 index 00000000..8301aaa7 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/helper/JavaFillAlgorithmFactory.kt @@ -0,0 +1,23 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools.helper + +class JavaFillAlgorithmFactory : FillAlgorithmFactory { + override fun createFillAlgorithm(): FillAlgorithm = JavaFillAlgorithm() +} From 43bf27b995505ac7d50d1f51996fe64d87c6c671 Mon Sep 17 00:00:00 2001 From: Tim Celec Date: Sat, 3 Aug 2024 11:45:01 +0200 Subject: [PATCH 16/25] PAINTROID-761 - CropCommand work --- .../org/catrobat/paintroid/FileReader.kt | 12 +++- .../command/implementation/CropCommand.kt | 72 +++++++++++++++++++ .../serialization/CropCommandSerializer.kt | 50 +++++++++++++ .../serialization/FillCommandSerializer.kt | 49 +++++++++++++ .../serialization/FlipCommandSerializer.kt | 38 ++++++++++ 5 files changed, 218 insertions(+), 3 deletions(-) create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/CropCommand.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/CropCommandSerializer.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/FillCommandSerializer.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/FlipCommandSerializer.kt diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt index 22d4de1f..09d5815e 100644 --- a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt @@ -36,6 +36,12 @@ import org.catrobat.paintroid.command.serialization.TextToolCommandSerializer import org.catrobat.paintroid.command.implementation.FillCommand import org.catrobat.paintroid.command.serialization.FillCommandSerializer +import org.catrobat.paintroid.command.implementation.FlipCommand +import org.catrobat.paintroid.command.serialization.FlipCommandSerializer + +import org.catrobat.paintroid.command.implementation.CropCommand +import org.catrobat.paintroid.command.serialization.CropCommandSerializer + import android.graphics.Paint import android.graphics.Point import android.graphics.PointF @@ -82,9 +88,9 @@ class FileReader(private val context : Context) put(TextToolCommand::class.java, TextToolCommandSerializer(version, activityContext)) put(Array::class.java, DataStructuresSerializer.StringArraySerializer(version)) put(FillCommand::class.java, FillCommandSerializer(version)) - // put(FlipCommand::class.java, FlipCommandSerializer(version)) - /* put(CropCommand::class.java, CropCommandSerializer(version)) - put(CutCommand::class.java, CutCommandSerializer(version)) + put(FlipCommand::class.java, FlipCommandSerializer(version)) + put(CropCommand::class.java, CropCommandSerializer(version)) + /* put(CutCommand::class.java, CutCommandSerializer(version)) put(ResizeCommand::class.java, ResizeCommandSerializer(version)) put(RotateCommand::class.java, RotateCommandSerializer(version)) put(ResetCommand::class.java, ResetCommandSerializer(version)) diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/CropCommand.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/CropCommand.kt new file mode 100644 index 00000000..874d249a --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/CropCommand.kt @@ -0,0 +1,72 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.paintroid.command.implementation + +import android.graphics.Bitmap +import android.graphics.Canvas +import org.catrobat.paintroid.command.Command +import org.catrobat.paintroid.contract.LayerContracts + +class CropCommand( + resizeCoordinateXLeft: Int, + resizeCoordinateYTop: Int, + resizeCoordinateXRight: Int, + resizeCoordinateYBottom: Int, + maximumBitmapResolution: Int +) : Command { + + var resizeCoordinateXLeft = resizeCoordinateXLeft; private set + var resizeCoordinateYTop = resizeCoordinateYTop; private set + var resizeCoordinateXRight = resizeCoordinateXRight; private set + var resizeCoordinateYBottom = resizeCoordinateYBottom; private set + var maximumBitmapResolution = maximumBitmapResolution; private set + + override fun run(canvas: Canvas, layerModel: LayerContracts.Model) { + if (resizeCoordinateXRight < resizeCoordinateXLeft || resizeCoordinateYBottom < resizeCoordinateYTop) { + return + } + if (resizeCoordinateXLeft >= layerModel.width || resizeCoordinateXRight < 0 || resizeCoordinateYTop >= layerModel.height || resizeCoordinateYBottom < 0) { + return + } + if (resizeCoordinateXLeft == 0 && resizeCoordinateXRight == layerModel.width - 1 && resizeCoordinateYBottom == layerModel.height - 1 && resizeCoordinateYTop == 0) { + return + } + if ((resizeCoordinateXRight + 1 - resizeCoordinateXLeft) * (resizeCoordinateYBottom + 1 - resizeCoordinateYTop) > maximumBitmapResolution) { + return + } + val width = resizeCoordinateXRight + 1 - resizeCoordinateXLeft + val height = resizeCoordinateYBottom + 1 - resizeCoordinateYTop + val iterator = layerModel.listIterator(0) + while (iterator.hasNext()) { + val currentLayer = iterator.next() + val currentBitmap = currentLayer.bitmap ?: Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + val resizedBitmap = Bitmap.createBitmap(width, height, currentBitmap.config) + val resizedCanvas = Canvas(resizedBitmap) + resizedCanvas.drawBitmap(currentBitmap, -resizeCoordinateXLeft.toFloat(), -resizeCoordinateYTop.toFloat(), null) + currentLayer.bitmap = resizedBitmap + } + layerModel.height = height + layerModel.width = width + } + + override fun freeResources() { + // No resources to free + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/CropCommandSerializer.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/CropCommandSerializer.kt new file mode 100644 index 00000000..4ce7d19a --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/CropCommandSerializer.kt @@ -0,0 +1,50 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.serialization + +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import org.catrobat.paintroid.command.implementation.CropCommand + +class CropCommandSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, command: CropCommand) { + with(output) { + writeInt(command.resizeCoordinateXLeft) + writeInt(command.resizeCoordinateYTop) + writeInt(command.resizeCoordinateXRight) + writeInt(command.resizeCoordinateYBottom) + writeInt(command.maximumBitmapResolution) + } + } + + override fun read(kryo: Kryo, input: Input, type: Class): CropCommand = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): CropCommand { + return with(input) { + val coordinateXLeft = readInt() + val coordinateYTop = readInt() + val coordinateXRight = readInt() + val coordinateYBottom = readInt() + val maxResolution = readInt() + CropCommand(coordinateXLeft, coordinateYTop, coordinateXRight, coordinateYBottom, maxResolution) + } + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/FillCommandSerializer.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/FillCommandSerializer.kt new file mode 100644 index 00000000..42aa02b0 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/FillCommandSerializer.kt @@ -0,0 +1,49 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.serialization + +import android.graphics.Paint +import android.graphics.Point +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import org.catrobat.paintroid.command.implementation.FillCommand +import org.catrobat.paintroid.tools.helper.JavaFillAlgorithmFactory + +class FillCommandSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, command: FillCommand) { + output.writeFloat(command.colorTolerance) + with(kryo) { + writeObject(output, command.clickedPixel) + writeObject(output, command.paint) + } + } + + override fun read(kryo: Kryo, input: Input, type: Class): FillCommand = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): FillCommand { + val tolerance = input.readFloat() + return with(kryo) { + val pixel = readObject(input, Point::class.java) + val paint = readObject(input, Paint::class.java) + FillCommand(JavaFillAlgorithmFactory(), pixel, paint, tolerance) + } + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/FlipCommandSerializer.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/FlipCommandSerializer.kt new file mode 100644 index 00000000..475e7152 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/FlipCommandSerializer.kt @@ -0,0 +1,38 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.serialization + +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import org.catrobat.paintroid.command.implementation.FlipCommand + +class FlipCommandSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, command: FlipCommand) { + output.writeInt(command.flipDirection.ordinal) + } + + override fun read(kryo: Kryo, input: Input, type: Class): FlipCommand = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): FlipCommand { + val flipDirection = FlipCommand.FlipDirection.values()[input.readInt()] + return FlipCommand(flipDirection) + } +} From 917748422e7515065a3e319886d2706973000de3 Mon Sep 17 00:00:00 2001 From: Tim Celec Date: Sat, 3 Aug 2024 11:48:27 +0200 Subject: [PATCH 17/25] PAINTROID-761 - ReorderLayersCommand work --- .../org/catrobat/paintroid/FileReader.kt | 23 +++++++- .../command/implementation/CutCommand.kt | 53 ++++++++++++++++++ .../implementation/ReorderLayersCommand.kt | 54 +++++++++++++++++++ .../command/implementation/ResetCommand.kt | 35 ++++++++++++ .../command/implementation/ResizeCommand.kt | 47 ++++++++++++++++ .../serialization/CutCommandSerializer.kt | 49 +++++++++++++++++ .../ReorderLayersCommandSerializer.kt | 44 +++++++++++++++ .../serialization/ResetCommandSerializer.kt | 36 +++++++++++++ .../serialization/ResizeCommandSerializer.kt | 44 +++++++++++++++ .../serialization/RotateCommandSerializer.kt | 38 +++++++++++++ 10 files changed, 421 insertions(+), 2 deletions(-) create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/CutCommand.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/ReorderLayersCommand.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/ResetCommand.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/ResizeCommand.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/CutCommandSerializer.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/ReorderLayersCommandSerializer.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/ResetCommandSerializer.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/ResizeCommandSerializer.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/RotateCommandSerializer.kt diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt index 09d5815e..670680e0 100644 --- a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt @@ -42,6 +42,25 @@ import org.catrobat.paintroid.command.serialization.FlipCommandSerializer import org.catrobat.paintroid.command.implementation.CropCommand import org.catrobat.paintroid.command.serialization.CropCommandSerializer +import org.catrobat.paintroid.command.implementation.CutCommand +import org.catrobat.paintroid.command.serialization.CutCommandSerializer + + + +import org.catrobat.paintroid.command.implementation.ResizeCommand +import org.catrobat.paintroid.command.serialization.ResizeCommandSerializer + +import org.catrobat.paintroid.command.implementation.RotateCommand +import org.catrobat.paintroid.command.serialization.RotateCommandSerializer +import org.catrobat.paintroid.command.implementation.ResetCommand +import org.catrobat.paintroid.command.serialization.ResetCommandSerializer + + +import org.catrobat.paintroid.command.implementation.ReorderLayersCommand +import org.catrobat.paintroid.command.serialization.ReorderLayersCommandSerializer + + + import android.graphics.Paint import android.graphics.Point import android.graphics.PointF @@ -90,12 +109,12 @@ class FileReader(private val context : Context) put(FillCommand::class.java, FillCommandSerializer(version)) put(FlipCommand::class.java, FlipCommandSerializer(version)) put(CropCommand::class.java, CropCommandSerializer(version)) - /* put(CutCommand::class.java, CutCommandSerializer(version)) + put(CutCommand::class.java, CutCommandSerializer(version)) put(ResizeCommand::class.java, ResizeCommandSerializer(version)) put(RotateCommand::class.java, RotateCommandSerializer(version)) put(ResetCommand::class.java, ResetCommandSerializer(version)) put(ReorderLayersCommand::class.java, ReorderLayersCommandSerializer(version)) - put(RemoveLayerCommand::class.java, RemoveLayerCommandSerializer(version)) + /* put(RemoveLayerCommand::class.java, RemoveLayerCommandSerializer(version)) put(MergeLayersCommand::class.java, MergeLayersCommandSerializer(version)) put(PathCommand::class.java, PathCommandSerializer(version)) put(SerializablePath::class.java, SerializablePath.PathSerializer(version)) diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/CutCommand.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/CutCommand.kt new file mode 100644 index 00000000..e26fd408 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/CutCommand.kt @@ -0,0 +1,53 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.implementation + +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Point +import android.graphics.PorterDuff +import android.graphics.PorterDuffXfermode +import android.graphics.RectF +import org.catrobat.paintroid.command.Command +import org.catrobat.paintroid.contract.LayerContracts + +class CutCommand( + val toolPosition: Point, + val boxWidth: Float, + val boxHeight: Float, + val boxRotation: Float +) : Command { + private val boxRect = RectF(-boxWidth / 2f, -boxHeight / 2f, boxWidth / 2f, boxHeight / 2f) + private val paint: Paint = Paint().apply { + xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) + alpha = 0 + } + + override fun run(canvas: Canvas, layerModel: LayerContracts.Model) { + canvas.save() + canvas.translate(toolPosition.x.toFloat(), toolPosition.y.toFloat()) + canvas.rotate(boxRotation) + canvas.drawRect(boxRect, paint) + canvas.restore() + } + + override fun freeResources() { + // No resources to free + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/ReorderLayersCommand.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/ReorderLayersCommand.kt new file mode 100644 index 00000000..f3b89684 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/ReorderLayersCommand.kt @@ -0,0 +1,54 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.paintroid.command.implementation + +import android.graphics.Canvas +import android.util.Log +import org.catrobat.paintroid.command.Command +import org.catrobat.paintroid.contract.LayerContracts + +class ReorderLayersCommand(position: Int, destination: Int) : Command { + + var position = position; private set + var destination = destination; private set + + companion object { + private val TAG = ReorderLayersCommand::class.java.simpleName + } + + override fun run(canvas: Canvas, layerModel: LayerContracts.Model) { + layerModel.run { + var success = false + getLayerAt(position)?.let { layer -> + if (removeLayerAt(position)) { + success = addLayerAt(destination, layer) + } + } + + if (!success) { + Log.e(TAG, "Could not retrieve layer to reorder!") + } + } + } + + override fun freeResources() { + // No resources to free + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/ResetCommand.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/ResetCommand.kt new file mode 100644 index 00000000..b2632d68 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/ResetCommand.kt @@ -0,0 +1,35 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.paintroid.command.implementation + +import android.graphics.Canvas +import org.catrobat.paintroid.command.Command +import org.catrobat.paintroid.contract.LayerContracts + +class ResetCommand : Command { + + override fun run(canvas: Canvas, layerModel: LayerContracts.Model) { + layerModel.reset() + } + + override fun freeResources() { + // No resources to free + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/ResizeCommand.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/ResizeCommand.kt new file mode 100644 index 00000000..346b1c7e --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/ResizeCommand.kt @@ -0,0 +1,47 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2015 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.implementation + +import android.graphics.Bitmap +import android.graphics.Canvas +import org.catrobat.paintroid.command.Command +import org.catrobat.paintroid.contract.LayerContracts + +class ResizeCommand(newWidth: Int, newHeight: Int) : Command { + + var newWidth = newWidth; private set + var newHeight = newHeight; private set + + override fun run(canvas: Canvas, layerModel: LayerContracts.Model) { + val iterator = layerModel.listIterator(0) + while (iterator.hasNext()) { + val currentLayer = iterator.next() + currentLayer.bitmap?.let { currentBitmap -> + val resizedBitmap = Bitmap.createScaledBitmap(currentBitmap, newWidth, newHeight, true) + currentLayer.bitmap = resizedBitmap + } + } + layerModel.height = newHeight + layerModel.width = newWidth + } + + override fun freeResources() { + // No resources to free + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/CutCommandSerializer.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/CutCommandSerializer.kt new file mode 100644 index 00000000..1df7fea8 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/CutCommandSerializer.kt @@ -0,0 +1,49 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.serialization + +import android.graphics.Point +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import org.catrobat.paintroid.command.implementation.CutCommand + +class CutCommandSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, command: CutCommand) { + kryo.writeObject(output, command.toolPosition) + with(output) { + writeFloat(command.boxWidth) + writeFloat(command.boxHeight) + writeFloat(command.boxRotation) + } + } + + override fun read(kryo: Kryo, input: Input, type: Class): CutCommand = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): CutCommand { + val position = kryo.readObject(input, Point::class.java) + return with(input) { + val width = readFloat() + val height = readFloat() + val rotation = readFloat() + CutCommand(position, width, height, rotation) + } + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/ReorderLayersCommandSerializer.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/ReorderLayersCommandSerializer.kt new file mode 100644 index 00000000..64e0a719 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/ReorderLayersCommandSerializer.kt @@ -0,0 +1,44 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.serialization + +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import org.catrobat.paintroid.command.implementation.ReorderLayersCommand + +class ReorderLayersCommandSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, command: ReorderLayersCommand) { + with(output) { + writeInt(command.position) + writeInt(command.destination) + } + } + + override fun read(kryo: Kryo, input: Input, type: Class): ReorderLayersCommand = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): ReorderLayersCommand { + return with(input) { + val position = readInt() + val destination = readInt() + ReorderLayersCommand(position, destination) + } + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/ResetCommandSerializer.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/ResetCommandSerializer.kt new file mode 100644 index 00000000..86cc8e58 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/ResetCommandSerializer.kt @@ -0,0 +1,36 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.serialization + +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import org.catrobat.paintroid.command.implementation.ResetCommand + +class ResetCommandSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, command: ResetCommand) { + // Has no member variables to save + } + + override fun read(kryo: Kryo, input: Input, type: Class): ResetCommand = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): ResetCommand = + ResetCommand() +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/ResizeCommandSerializer.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/ResizeCommandSerializer.kt new file mode 100644 index 00000000..df7d04ba --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/ResizeCommandSerializer.kt @@ -0,0 +1,44 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.serialization + +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import org.catrobat.paintroid.command.implementation.ResizeCommand + +class ResizeCommandSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, command: ResizeCommand) { + with(output) { + writeInt(command.newWidth) + writeInt(command.newHeight) + } + } + + override fun read(kryo: Kryo, input: Input, type: Class): ResizeCommand = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): ResizeCommand { + return with(input) { + val width = readInt() + val height = readInt() + ResizeCommand(width, height) + } + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/RotateCommandSerializer.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/RotateCommandSerializer.kt new file mode 100644 index 00000000..8c80293b --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/RotateCommandSerializer.kt @@ -0,0 +1,38 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.serialization + +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import org.catrobat.paintroid.command.implementation.RotateCommand + +class RotateCommandSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, command: RotateCommand) { + output.writeInt(command.rotateDirection.ordinal) + } + + override fun read(kryo: Kryo, input: Input, type: Class): RotateCommand = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): RotateCommand { + val rotateDirection = RotateCommand.RotateDirection.values()[input.readInt()] + return RotateCommand(rotateDirection) + } +} From 79b5fc856ff1df5340296d2d8c9e5cdc85ca7a98 Mon Sep 17 00:00:00 2001 From: Tim Celec Date: Sat, 3 Aug 2024 11:52:06 +0200 Subject: [PATCH 18/25] PAINTROID-761 - RemoveLayerCommand work --- .../org/catrobat/paintroid/FileReader.kt | 7 ++-- .../implementation/RemoveLayerCommand.kt | 39 +++++++++++++++++ .../RemoveLayerCommandSerializer.kt | 42 +++++++++++++++++++ 3 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/RemoveLayerCommand.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/RemoveLayerCommandSerializer.kt diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt index 670680e0..7c80c719 100644 --- a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt @@ -59,7 +59,8 @@ import org.catrobat.paintroid.command.serialization.ResetCommandSerializer import org.catrobat.paintroid.command.implementation.ReorderLayersCommand import org.catrobat.paintroid.command.serialization.ReorderLayersCommandSerializer - +import org.catrobat.paintroid.command.implementation.RemoveLayerCommand +import org.catrobat.paintroid.command.serialization.RemoveLayerCommandSerializer import android.graphics.Paint import android.graphics.Point @@ -114,8 +115,8 @@ class FileReader(private val context : Context) put(RotateCommand::class.java, RotateCommandSerializer(version)) put(ResetCommand::class.java, ResetCommandSerializer(version)) put(ReorderLayersCommand::class.java, ReorderLayersCommandSerializer(version)) - /* put(RemoveLayerCommand::class.java, RemoveLayerCommandSerializer(version)) - put(MergeLayersCommand::class.java, MergeLayersCommandSerializer(version)) + put(RemoveLayerCommand::class.java, RemoveLayerCommandSerializer(version)) + /*put(MergeLayersCommand::class.java, MergeLayersCommandSerializer(version)) put(PathCommand::class.java, PathCommandSerializer(version)) put(SerializablePath::class.java, SerializablePath.PathSerializer(version)) put(SerializablePath.Move::class.java, SerializablePath.PathActionMoveSerializer(version)) diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/RemoveLayerCommand.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/RemoveLayerCommand.kt new file mode 100644 index 00000000..46c18ac3 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/RemoveLayerCommand.kt @@ -0,0 +1,39 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.paintroid.command.implementation + +import android.graphics.Canvas +import org.catrobat.paintroid.command.Command +import org.catrobat.paintroid.contract.LayerContracts + +class RemoveLayerCommand(position: Int) : Command { + + var position = position; private set + + override fun run(canvas: Canvas, layerModel: LayerContracts.Model) { + if (layerModel.removeLayerAt(position)) { + layerModel.getLayerAt(0)?.let { layerModel.currentLayer = it } + } + } + + override fun freeResources() { + // No resources to free + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/RemoveLayerCommandSerializer.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/RemoveLayerCommandSerializer.kt new file mode 100644 index 00000000..75ebaecd --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/RemoveLayerCommandSerializer.kt @@ -0,0 +1,42 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.serialization + +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import org.catrobat.paintroid.command.implementation.RemoveLayerCommand + +class RemoveLayerCommandSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, command: RemoveLayerCommand) { + with(output) { + writeInt(command.position) + } + } + + override fun read(kryo: Kryo, input: Input, type: Class): RemoveLayerCommand = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): RemoveLayerCommand { + return with(input) { + val position = readInt() + RemoveLayerCommand(position) + } + } +} From 963d6f8ca72cb369ea64acac43cf49a0b5727195 Mon Sep 17 00:00:00 2001 From: Tim Celec Date: Sat, 3 Aug 2024 11:54:46 +0200 Subject: [PATCH 19/25] PAINTROID-761 - PathCommand work --- .../org/catrobat/paintroid/FileReader.kt | 12 +++- .../implementation/MergeLayersCommand.kt | 63 +++++++++++++++++++ .../command/implementation/PathCommand.kt | 39 ++++++++++++ .../MergeLayersCommandSerializer.kt | 44 +++++++++++++ .../serialization/PathCommandSerializer.kt | 45 +++++++++++++ 5 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/MergeLayersCommand.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/PathCommand.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/MergeLayersCommandSerializer.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/PathCommandSerializer.kt diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt index 7c80c719..625bbe89 100644 --- a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt @@ -62,6 +62,14 @@ import org.catrobat.paintroid.command.serialization.ReorderLayersCommandSerializ import org.catrobat.paintroid.command.implementation.RemoveLayerCommand import org.catrobat.paintroid.command.serialization.RemoveLayerCommandSerializer + + +import org.catrobat.paintroid.command.implementation.MergeLayersCommand +import org.catrobat.paintroid.command.serialization.MergeLayersCommandSerializer + +import org.catrobat.paintroid.command.implementation.PathCommand +import org.catrobat.paintroid.command.serialization.PathCommandSerializer + import android.graphics.Paint import android.graphics.Point import android.graphics.PointF @@ -116,9 +124,9 @@ class FileReader(private val context : Context) put(ResetCommand::class.java, ResetCommandSerializer(version)) put(ReorderLayersCommand::class.java, ReorderLayersCommandSerializer(version)) put(RemoveLayerCommand::class.java, RemoveLayerCommandSerializer(version)) - /*put(MergeLayersCommand::class.java, MergeLayersCommandSerializer(version)) + put(MergeLayersCommand::class.java, MergeLayersCommandSerializer(version)) put(PathCommand::class.java, PathCommandSerializer(version)) - put(SerializablePath::class.java, SerializablePath.PathSerializer(version)) + /*put(SerializablePath::class.java, SerializablePath.PathSerializer(version)) put(SerializablePath.Move::class.java, SerializablePath.PathActionMoveSerializer(version)) put(SerializablePath.Line::class.java, SerializablePath.PathActionLineSerializer(version)) put(SerializablePath.Quad::class.java, SerializablePath.PathActionQuadSerializer(version)) diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/MergeLayersCommand.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/MergeLayersCommand.kt new file mode 100644 index 00000000..c4b43491 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/MergeLayersCommand.kt @@ -0,0 +1,63 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.paintroid.command.implementation + +import android.graphics.Canvas +import android.util.Log +import org.catrobat.paintroid.command.Command +import org.catrobat.paintroid.contract.LayerContracts + +class MergeLayersCommand(position: Int, mergeWith: Int) : Command { + + var position = position; private set + var mergeWith = mergeWith; private set + + companion object { + private val TAG = MergeLayersCommand::class.java.simpleName + } + + override fun run(canvas: Canvas, layerModel: LayerContracts.Model) { + val sourceLayer = layerModel.getLayerAt(position) + val destinationLayer = layerModel.getLayerAt(mergeWith) + var success = false + if (sourceLayer != null && destinationLayer != null) { + val destinationBitmap = destinationLayer.bitmap + destinationBitmap ?: return + val copyBitmap = destinationBitmap.copy(destinationBitmap.config, true) + val copyCanvas = Canvas(copyBitmap) + copyCanvas.drawBitmap(sourceLayer.bitmap ?: return, 0f, 0f, null) + if (layerModel.removeLayerAt(position)) { + destinationLayer.bitmap = copyBitmap + if (sourceLayer == layerModel.currentLayer) { + layerModel.currentLayer = destinationLayer + } + success = true + } + } + + if (!success) { + Log.e(TAG, "Could not merge layers!") + } + } + + override fun freeResources() { + // No resources to free + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/PathCommand.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/PathCommand.kt new file mode 100644 index 00000000..f0d84776 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/PathCommand.kt @@ -0,0 +1,39 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.paintroid.command.implementation + +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Path +import org.catrobat.paintroid.command.Command +import org.catrobat.paintroid.contract.LayerContracts + +class PathCommand(val paint: Paint, path: Path) : Command { + + var path = path; private set + + override fun run(canvas: Canvas, layerModel: LayerContracts.Model) { + canvas.drawPath(path, paint) + } + + override fun freeResources() { + // No resources to free + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/MergeLayersCommandSerializer.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/MergeLayersCommandSerializer.kt new file mode 100644 index 00000000..66b2514d --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/MergeLayersCommandSerializer.kt @@ -0,0 +1,44 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.serialization + +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import org.catrobat.paintroid.command.implementation.MergeLayersCommand + +class MergeLayersCommandSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, command: MergeLayersCommand) { + with(output) { + writeInt(command.position) + writeInt(command.mergeWith) + } + } + + override fun read(kryo: Kryo, input: Input, type: Class): MergeLayersCommand = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): MergeLayersCommand { + return with(input) { + val position = readInt() + val mergeWith = readInt() + MergeLayersCommand(position, mergeWith) + } + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/PathCommandSerializer.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/PathCommandSerializer.kt new file mode 100644 index 00000000..9a5f8194 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/PathCommandSerializer.kt @@ -0,0 +1,45 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.serialization + +import android.graphics.Paint +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import org.catrobat.paintroid.command.implementation.PathCommand + +class PathCommandSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, command: PathCommand) { + with(kryo) { + writeObject(output, command.paint) + writeObject(output, command.path as SerializablePath) + } + } + + override fun read(kryo: Kryo, input: Input, type: Class): PathCommand = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): PathCommand { + return with(kryo) { + val paint = readObject(input, Paint::class.java) + val path = readObject(input, SerializablePath::class.java) + PathCommand(Paint(paint), path) + } + } +} From 0ae22cf45b0bb088582f8a63ebe86463c8c100a2 Mon Sep 17 00:00:00 2001 From: Tim Celec Date: Sat, 3 Aug 2024 12:02:11 +0200 Subject: [PATCH 20/25] PAINTROID-761 - ShapeDrawable work --- .../org/catrobat/paintroid/FileReader.kt | 23 +++- .../implementation/GeometricFillCommand.kt | 58 +++++++++ .../implementation/LoadLayerListCommand.kt | 44 +++++++ .../GeometricFillCommandSerializer.kt | 111 ++++++++++++++++++ .../LoadLayerListCommandSerializer.kt | 69 +++++++++++ .../paintroid/tools/drawable/HeartDrawable.kt | 66 +++++++++++ .../paintroid/tools/drawable/OvalDrawable.kt | 29 +++++ .../tools/drawable/RectangleDrawable.kt | 29 +++++ .../paintroid/tools/drawable/StarDrawable.kt | 58 +++++++++ 9 files changed, 485 insertions(+), 2 deletions(-) create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/GeometricFillCommand.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/LoadLayerListCommand.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/GeometricFillCommandSerializer.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/LoadLayerListCommandSerializer.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/drawable/HeartDrawable.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/drawable/OvalDrawable.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/drawable/RectangleDrawable.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/drawable/StarDrawable.kt diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt index 625bbe89..3a242e42 100644 --- a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt @@ -70,6 +70,17 @@ import org.catrobat.paintroid.command.serialization.MergeLayersCommandSerializer import org.catrobat.paintroid.command.implementation.PathCommand import org.catrobat.paintroid.command.serialization.PathCommandSerializer + +import org.catrobat.paintroid.command.serialization.SerializablePath + +import org.catrobat.paintroid.command.implementation.LoadLayerListCommand +import org.catrobat.paintroid.command.serialization.LoadLayerListCommandSerializer + + +import org.catrobat.paintroid.command.implementation.GeometricFillCommand +import org.catrobat.paintroid.command.serialization.GeometricFillCommandSerializer + + import android.graphics.Paint import android.graphics.Point import android.graphics.PointF @@ -80,6 +91,14 @@ import android.content.ContentResolver import android.content.ContentUris import android.content.ContentValues + +import org.catrobat.paintroid.tools.drawable.HeartDrawable +import org.catrobat.paintroid.tools.drawable.OvalDrawable +import org.catrobat.paintroid.tools.drawable.RectangleDrawable +import org.catrobat.paintroid.tools.drawable.ShapeDrawable +import org.catrobat.paintroid.tools.drawable.StarDrawable + + class FileReader(private val context : Context) { private lateinit var activityContext: Context // MAYBE CAUSE A CRASH @@ -126,7 +145,7 @@ class FileReader(private val context : Context) put(RemoveLayerCommand::class.java, RemoveLayerCommandSerializer(version)) put(MergeLayersCommand::class.java, MergeLayersCommandSerializer(version)) put(PathCommand::class.java, PathCommandSerializer(version)) - /*put(SerializablePath::class.java, SerializablePath.PathSerializer(version)) + put(SerializablePath::class.java, SerializablePath.PathSerializer(version)) put(SerializablePath.Move::class.java, SerializablePath.PathActionMoveSerializer(version)) put(SerializablePath.Line::class.java, SerializablePath.PathActionLineSerializer(version)) put(SerializablePath.Quad::class.java, SerializablePath.PathActionQuadSerializer(version)) @@ -138,7 +157,7 @@ class FileReader(private val context : Context) put(RectangleDrawable::class.java, GeometricFillCommandSerializer.RectangleDrawableSerializer(version)) put(StarDrawable::class.java, GeometricFillCommandSerializer.StarDrawableSerializer(version)) put(ShapeDrawable::class.java, null) - put(RectF::class.java, DataStructuresSerializer.RectFSerializer(version)) + /* put(RectF::class.java, DataStructuresSerializer.RectFSerializer(version)) put(ClipboardCommand::class.java, ClipboardCommandSerializer(version)) put(SerializableTypeface::class.java, SerializableTypeface.TypefaceSerializer(version)) put(PointCommand::class.java, PointCommandSerializer(version)) diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/GeometricFillCommand.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/GeometricFillCommand.kt new file mode 100644 index 00000000..65d9c200 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/GeometricFillCommand.kt @@ -0,0 +1,58 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.paintroid.command.implementation + +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.RectF +import org.catrobat.paintroid.command.Command +import org.catrobat.paintroid.contract.LayerContracts +import org.catrobat.paintroid.tools.drawable.ShapeDrawable + +class GeometricFillCommand( + shapeDrawable: ShapeDrawable, + pointX: Int, + pointY: Int, + boxRect: RectF, + boxRotation: Float, + paint: Paint +) : Command { + + var shapeDrawable = shapeDrawable; private set + var pointX = pointX; private set + var pointY = pointY; private set + var boxRect = boxRect; private set + var boxRotation = boxRotation; private set + var paint = paint; private set + + override fun run(canvas: Canvas, layerModel: LayerContracts.Model) { + with(canvas) { + save() + translate(pointX.toFloat(), pointY.toFloat()) + rotate(boxRotation) + shapeDrawable.draw(this, boxRect, paint) + restore() + } + } + + override fun freeResources() { + // No resources to free + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/LoadLayerListCommand.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/LoadLayerListCommand.kt new file mode 100644 index 00000000..abdf969d --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/LoadLayerListCommand.kt @@ -0,0 +1,44 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.paintroid.command.implementation + +import android.graphics.Bitmap +import android.graphics.Canvas +import org.catrobat.paintroid.command.Command +import org.catrobat.paintroid.contract.LayerContracts +import org.catrobat.paintroid.model.Layer + +class LoadLayerListCommand(loadedLayers: List) : Command { + + var loadedLayers = loadedLayers; private set + + override fun run(canvas: Canvas, layerModel: LayerContracts.Model) { + loadedLayers.forEachIndexed { index, layer -> + val currentLayer = Layer(layer.bitmap.copy(Bitmap.Config.ARGB_8888, true)) + currentLayer.opacityPercentage = layer.opacityPercentage + layerModel.addLayerAt(index, currentLayer) + } + layerModel.currentLayer = layerModel.getLayerAt(0) + } + + override fun freeResources() { + // No resources to free + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/GeometricFillCommandSerializer.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/GeometricFillCommandSerializer.kt new file mode 100644 index 00000000..2f3334a5 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/GeometricFillCommandSerializer.kt @@ -0,0 +1,111 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.serialization + +import android.graphics.Paint +import android.graphics.RectF +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import org.catrobat.paintroid.command.implementation.GeometricFillCommand +import org.catrobat.paintroid.tools.drawable.HeartDrawable +import org.catrobat.paintroid.tools.drawable.OvalDrawable +import org.catrobat.paintroid.tools.drawable.RectangleDrawable +import org.catrobat.paintroid.tools.drawable.ShapeDrawable +import org.catrobat.paintroid.tools.drawable.StarDrawable + +class GeometricFillCommandSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, command: GeometricFillCommand) { + with(kryo) { + with(output) { + writeClassAndObject(output, command.shapeDrawable) + writeInt(command.pointX) + writeInt(command.pointY) + writeObject(output, command.boxRect) + writeFloat(command.boxRotation) + writeObject(output, command.paint) + } + } + } + + override fun read(kryo: Kryo, input: Input, type: Class): GeometricFillCommand = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): GeometricFillCommand { + return with(kryo) { + with(input) { + val shape = readClassAndObject(input) as ShapeDrawable + val pointX = readInt() + val pointY = readInt() + val rect = readObject(input, RectF::class.java) + val rotation = readFloat() + val paint = readObject(input, Paint::class.java) + GeometricFillCommand(shape, pointX, pointY, rect, rotation, paint) + } + } + } + + class HeartDrawableSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, command: HeartDrawable) { + // Has no member variables to save + } + + override fun read(kryo: Kryo, input: Input, type: Class): HeartDrawable = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): HeartDrawable = + HeartDrawable() + } + + class OvalDrawableSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, command: OvalDrawable) { + // Has no member variables to save + } + + override fun read(kryo: Kryo, input: Input, type: Class): OvalDrawable = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): OvalDrawable = + OvalDrawable() + } + + class RectangleDrawableSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, command: RectangleDrawable) { + // Has no member variables to save + } + + override fun read(kryo: Kryo, input: Input, type: Class): RectangleDrawable = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): RectangleDrawable = + RectangleDrawable() + } + + class StarDrawableSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, command: StarDrawable) { + // Has no member variables to save + } + + override fun read(kryo: Kryo, input: Input, type: Class): StarDrawable = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): StarDrawable = + StarDrawable() + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/LoadLayerListCommandSerializer.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/LoadLayerListCommandSerializer.kt new file mode 100644 index 00000000..5ce5be14 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/LoadLayerListCommandSerializer.kt @@ -0,0 +1,69 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.serialization + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import org.catrobat.paintroid.command.implementation.LoadLayerListCommand +import org.catrobat.paintroid.model.Layer + +class LoadLayerListCommandSerializer(version: Int) : + VersionSerializer(version) { + + companion object { + private const val COMPRESSION_QUALITY = 100 + } + + override fun write(kryo: Kryo, output: Output, command: LoadLayerListCommand) { + output.writeInt(command.loadedLayers.size) + command.loadedLayers.forEach { layer -> + layer.bitmap.compress(Bitmap.CompressFormat.PNG, COMPRESSION_QUALITY, output) + output.writeInt(layer.opacityPercentage) + } + } + + override fun read( + kryo: Kryo, + input: Input, + type: Class + ): LoadLayerListCommand = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion( + kryo: Kryo, + input: Input, + type: Class + ): LoadLayerListCommand { + val size = input.readInt() + val layerList = ArrayList() + repeat(size) { + val bitmap = BitmapFactory.decodeStream(input) + val alpha = input.readInt() + val layer = Layer(bitmap).apply { + opacityPercentage = alpha + } + layerList.add(layer) + } + + return LoadLayerListCommand(layerList) + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/drawable/HeartDrawable.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/drawable/HeartDrawable.kt new file mode 100644 index 00000000..0bc6a4ab --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/drawable/HeartDrawable.kt @@ -0,0 +1,66 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools.drawable + +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Path +import android.graphics.RectF + +private const val CONSTANT_1 = 8f +private const val CONSTANT_2 = 0.2f +private const val CONSTANT_3 = 0.8f +private const val CONSTANT_4 = 1.2f +private const val CONSTANT_5 = 1.5f +private const val CONSTANT_6 = 4.5f +private const val CONSTANT_7 = 7.2f + +class HeartDrawable : ShapeDrawable { + private val path = Path() + private val paint = Paint() + override fun draw(canvas: Canvas, shapeRect: RectF, drawPaint: Paint) { + paint.set(drawPaint) + val midWidth = shapeRect.width() / 2 + val height = shapeRect.height() + val width = shapeRect.width() + path.run { + reset() + moveTo(midWidth, height) + var x1 = -CONSTANT_2 * width + var x2 = CONSTANT_3 * width / CONSTANT_1 + val y1 = CONSTANT_6 * height / CONSTANT_1 + val y2 = -CONSTANT_5 * height / CONSTANT_1 + cubicTo( + x1, y1, + x2, y2, + midWidth, -y2 + ) + x1 = CONSTANT_7 * width / CONSTANT_1 + x2 = CONSTANT_4 * width + cubicTo( + x1, y2, + x2, y1, + midWidth, height + ) + close() + offset(shapeRect.left, shapeRect.top) + } + canvas.drawPath(path, paint) + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/drawable/OvalDrawable.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/drawable/OvalDrawable.kt new file mode 100644 index 00000000..b24dded6 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/drawable/OvalDrawable.kt @@ -0,0 +1,29 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools.drawable + +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.RectF + +class OvalDrawable : ShapeDrawable { + override fun draw(canvas: Canvas, shapeRect: RectF, drawPaint: Paint) { + canvas.drawOval(shapeRect, drawPaint) + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/drawable/RectangleDrawable.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/drawable/RectangleDrawable.kt new file mode 100644 index 00000000..789b5e34 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/drawable/RectangleDrawable.kt @@ -0,0 +1,29 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools.drawable + +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.RectF + +class RectangleDrawable : ShapeDrawable { + override fun draw(canvas: Canvas, shapeRect: RectF, drawPaint: Paint) { + canvas.drawRect(shapeRect, drawPaint) + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/drawable/StarDrawable.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/drawable/StarDrawable.kt new file mode 100644 index 00000000..554888d1 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/drawable/StarDrawable.kt @@ -0,0 +1,58 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools.drawable + +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Path +import android.graphics.RectF + +private const val CONSTANT_1 = 8f +private const val CONSTANT_2 = 1.8f +private const val CONSTANT_3 = 3f +private const val CONSTANT_4 = 2f + +class StarDrawable : ShapeDrawable { + private val path = Path() + private val paint = Paint() + override fun draw(canvas: Canvas, shapeRect: RectF, drawPaint: Paint) { + paint.set(drawPaint) + val midWidth = shapeRect.width() / 2 + val midHeight = shapeRect.height() / 2 + val height = shapeRect.height() + val width = shapeRect.width() + path.run { + reset() + moveTo(midWidth, 0f) + lineTo(midWidth + width / CONSTANT_1, midHeight - height / CONSTANT_1) + lineTo(width, midHeight - height / CONSTANT_1) + lineTo(midWidth + CONSTANT_2 * width / CONSTANT_1, midHeight + height / CONSTANT_1) + lineTo(midWidth + CONSTANT_3 * width / CONSTANT_1, height) + lineTo(midWidth, midHeight + CONSTANT_4 * height / CONSTANT_1) + lineTo(midWidth - CONSTANT_3 * width / CONSTANT_1, height) + lineTo(midWidth - CONSTANT_2 * width / CONSTANT_1, midHeight + height / CONSTANT_1) + lineTo(0f, midHeight - height / CONSTANT_1) + lineTo(midWidth - width / CONSTANT_1, midHeight - height / CONSTANT_1) + lineTo(midWidth, 0f) + close() + offset(shapeRect.left, shapeRect.top) + } + canvas.drawPath(path, paint) + } +} From 9b793ecc4330f7512956c9bfba05f3bffa253cf7 Mon Sep 17 00:00:00 2001 From: Tim Celec Date: Sat, 3 Aug 2024 12:29:01 +0200 Subject: [PATCH 21/25] PAINTROID-761 - ClipboardCommand work --- .../org/catrobat/paintroid/FileReader.kt | 7 +- .../implementation/ClipboardCommand.kt | 112 ++++++++++++++++++ .../ClipboardCommandSerializer.kt | 55 +++++++++ 3 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/ClipboardCommand.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/ClipboardCommandSerializer.kt diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt index 3a242e42..6819d8f3 100644 --- a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt @@ -81,6 +81,9 @@ import org.catrobat.paintroid.command.implementation.GeometricFillCommand import org.catrobat.paintroid.command.serialization.GeometricFillCommandSerializer +import org.catrobat.paintroid.command.implementation.ClipboardCommand +import org.catrobat.paintroid.command.serialization.ClipboardCommandSerializer + import android.graphics.Paint import android.graphics.Point import android.graphics.PointF @@ -157,9 +160,9 @@ class FileReader(private val context : Context) put(RectangleDrawable::class.java, GeometricFillCommandSerializer.RectangleDrawableSerializer(version)) put(StarDrawable::class.java, GeometricFillCommandSerializer.StarDrawableSerializer(version)) put(ShapeDrawable::class.java, null) - /* put(RectF::class.java, DataStructuresSerializer.RectFSerializer(version)) + put(RectF::class.java, DataStructuresSerializer.RectFSerializer(version)) put(ClipboardCommand::class.java, ClipboardCommandSerializer(version)) - put(SerializableTypeface::class.java, SerializableTypeface.TypefaceSerializer(version)) + /* put(SerializableTypeface::class.java, SerializableTypeface.TypefaceSerializer(version)) put(PointCommand::class.java, PointCommandSerializer(version)) put(SerializablePath.Cube::class.java, SerializablePath.PathActionCubeSerializer(version)) put(Bitmap::class.java, BitmapSerializer(version)) diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/ClipboardCommand.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/ClipboardCommand.kt new file mode 100644 index 00000000..11ceb490 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/ClipboardCommand.kt @@ -0,0 +1,112 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.paintroid.command.implementation + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Point +import android.graphics.RectF +import androidx.annotation.VisibleForTesting + +import org.catrobat.paintroid.command.Command +import org.catrobat.paintroid.contract.LayerContracts +import java.io.File +import java.io.FileOutputStream +import java.util.Random + +class ClipboardCommand(bitmap: Bitmap, position: Point, width: Float, height: Float, rotation: Float) : + Command { + + var bitmap: Bitmap? = bitmap.copy(Bitmap.Config.ARGB_8888, false); private set + var coordinates = position; private set + var boxRotation = rotation; private set + var boxWidth = width; private set + var boxHeight = height; private set + var fileToStoredBitmap: File? = null + + companion object { + private const val COMPRESSION_QUALITY = 100 + } + + override fun run(canvas: Canvas, layerModel: LayerContracts.Model) { + var bitmapToDraw = bitmap + if (fileToStoredBitmap != null) { + // bitmapToDraw = FileIO.getBitmapFromFile(fileToStoredBitmap) + } + bitmapToDraw ?: return + + val rect = RectF(-boxWidth / 2f, -boxHeight / 2f, boxWidth / 2f, boxHeight / 2f) + with(canvas) { + save() + translate(coordinates.x.toFloat(), coordinates.y.toFloat()) + rotate(boxRotation) + drawBitmap(bitmapToDraw, null, rect, null) + restore() + } + + if (fileToStoredBitmap == null) { + storeBitmap(bitmapToDraw, boxWidth, boxHeight) + } + bitmap = recycleBitmap(bitmapToDraw) + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + fun storeBitmap(bitmapToStore: Bitmap, boxWidth: Float, boxHeight: Float) { + // val random = Random() + // random.setSeed(System.currentTimeMillis()) + /* fileToStoredBitmap = + File(PaintroidApplication.cacheDir?.absolutePath, random.nextLong().toString()) + val resizedBitmap = resizeBitmap(bitmapToStore, boxWidth, boxHeight) + FileOutputStream(fileToStoredBitmap).use { stream -> + resizedBitmap.compress(Bitmap.CompressFormat.PNG, COMPRESSION_QUALITY, stream) + } + + */ + } + + private fun resizeBitmap(bitmapToResize: Bitmap, boxWidth: Float, boxHeight: Float): Bitmap { + val newWidth = + if (boxWidth < bitmapToResize.width) boxWidth.toInt() else bitmapToResize.width + val newHeight = + if (boxHeight < bitmapToResize.height) boxHeight.toInt() else bitmapToResize.height + if (newWidth == bitmapToResize.width && newHeight == bitmapToResize.height) { + return bitmapToResize + } + return Bitmap.createScaledBitmap(bitmapToResize, newWidth, newHeight, false) + } + + private fun recycleBitmap(bitmapToRecycle: Bitmap?): Bitmap? { + return bitmapToRecycle?.let { bitmap -> + if (!bitmap.isRecycled) { + bitmap.recycle() + } + null + } + } + + override fun freeResources() { + bitmap = recycleBitmap(bitmap) + fileToStoredBitmap?.let { file -> + if (file.exists()) { + file.delete() + } + } + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/ClipboardCommandSerializer.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/ClipboardCommandSerializer.kt new file mode 100644 index 00000000..59711007 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/ClipboardCommandSerializer.kt @@ -0,0 +1,55 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.serialization + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Point +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.KryoException +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import org.catrobat.paintroid.command.implementation.ClipboardCommand + +class ClipboardCommandSerializer(version: Int) : VersionSerializer(version) { + + companion object { + private const val COMPRESSION_QUALITY = 100 + } + + override fun write(kryo: Kryo, output: Output, command: ClipboardCommand) { + + } + + override fun read(kryo: Kryo, input: Input, type: Class): ClipboardCommand = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): ClipboardCommand { + return with(kryo) { + with(input) { + val bitmap = BitmapFactory.decodeStream(input) + val coordinates = readObject(input, Point::class.java) + val width = readFloat() + val height = readFloat() + val rotation = readFloat() + ClipboardCommand(bitmap, coordinates, width, height, rotation) + } + } + } +} From 4526c1c74b44f63f007701b5eaf496173ca62bd2 Mon Sep 17 00:00:00 2001 From: Tim Celec Date: Sat, 3 Aug 2024 12:31:54 +0200 Subject: [PATCH 22/25] PAINTROID-761 - PointCommand work --- .../org/catrobat/paintroid/FileReader.kt | 11 ++++- .../command/implementation/PointCommand.kt | 39 ++++++++++++++++ .../serialization/PointCommandSerializer.kt | 46 +++++++++++++++++++ 3 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/PointCommand.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/PointCommandSerializer.kt diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt index 6819d8f3..2a2da102 100644 --- a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt @@ -84,6 +84,13 @@ import org.catrobat.paintroid.command.serialization.GeometricFillCommandSerializ import org.catrobat.paintroid.command.implementation.ClipboardCommand import org.catrobat.paintroid.command.serialization.ClipboardCommandSerializer + +import org.catrobat.paintroid.command.serialization.SerializableTypeface + + +import org.catrobat.paintroid.command.implementation.PointCommand +import org.catrobat.paintroid.command.serialization.PointCommandSerializer + import android.graphics.Paint import android.graphics.Point import android.graphics.PointF @@ -162,9 +169,9 @@ class FileReader(private val context : Context) put(ShapeDrawable::class.java, null) put(RectF::class.java, DataStructuresSerializer.RectFSerializer(version)) put(ClipboardCommand::class.java, ClipboardCommandSerializer(version)) - /* put(SerializableTypeface::class.java, SerializableTypeface.TypefaceSerializer(version)) + put(SerializableTypeface::class.java, SerializableTypeface.TypefaceSerializer(version)) put(PointCommand::class.java, PointCommandSerializer(version)) - put(SerializablePath.Cube::class.java, SerializablePath.PathActionCubeSerializer(version)) + /*put(SerializablePath.Cube::class.java, SerializablePath.PathActionCubeSerializer(version)) put(Bitmap::class.java, BitmapSerializer(version)) put(SmudgePathCommand::class.java, SmudgePathCommandSerializer(version)) put(ColorHistory::class.java, ColorHistorySerializer(version)) diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/PointCommand.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/PointCommand.kt new file mode 100644 index 00000000..e072214b --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/PointCommand.kt @@ -0,0 +1,39 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.paintroid.command.implementation + +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.PointF +import org.catrobat.paintroid.command.Command +import org.catrobat.paintroid.contract.LayerContracts + +class PointCommand(var paint: Paint, point: PointF) : Command { + + var point = point; private set + + override fun run(canvas: Canvas, layerModel: LayerContracts.Model) { + canvas.drawPoint(point.x, point.y, paint) + } + + override fun freeResources() { + // No resources to free + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/PointCommandSerializer.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/PointCommandSerializer.kt new file mode 100644 index 00000000..3b06b23d --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/PointCommandSerializer.kt @@ -0,0 +1,46 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.serialization + +import android.graphics.Paint +import android.graphics.PointF +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import org.catrobat.paintroid.command.implementation.PointCommand + +class PointCommandSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, command: PointCommand) { + with(kryo) { + writeObject(output, command.point) + writeObject(output, command.paint) + } + } + + override fun read(kryo: Kryo, input: Input, type: Class): PointCommand = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): PointCommand { + return with(kryo) { + val point = readObject(input, PointF::class.java) + val paint = readObject(input, Paint::class.java) + PointCommand(paint, point) + } + } +} From 062ef75220e957c3cd5acb2d48f3efab2fdc35e1 Mon Sep 17 00:00:00 2001 From: Tim Celec Date: Sat, 3 Aug 2024 12:44:49 +0200 Subject: [PATCH 23/25] PAINTROID-761 - SmudgePathCommand work --- .../org/catrobat/paintroid/FileReader.kt | 13 +- .../implementation/SmudgePathCommand.kt | 79 +++++ .../command/serialization/BitmapSerializer.kt | 39 +++ .../SmudgePathCommandSerializer.kt | 69 ++++ .../tools/implementation/SmudgeTool.kt | 317 ++++++++++++++++++ 5 files changed, 514 insertions(+), 3 deletions(-) create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/SmudgePathCommand.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/BitmapSerializer.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/SmudgePathCommandSerializer.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/tools/implementation/SmudgeTool.kt diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt index 2a2da102..95036c55 100644 --- a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt @@ -91,6 +91,13 @@ import org.catrobat.paintroid.command.serialization.SerializableTypeface import org.catrobat.paintroid.command.implementation.PointCommand import org.catrobat.paintroid.command.serialization.PointCommandSerializer + +import org.catrobat.paintroid.command.serialization.BitmapSerializer + + +import org.catrobat.paintroid.command.implementation.SmudgePathCommand +import org.catrobat.paintroid.command.serialization.SmudgePathCommandSerializer + import android.graphics.Paint import android.graphics.Point import android.graphics.PointF @@ -171,13 +178,13 @@ class FileReader(private val context : Context) put(ClipboardCommand::class.java, ClipboardCommandSerializer(version)) put(SerializableTypeface::class.java, SerializableTypeface.TypefaceSerializer(version)) put(PointCommand::class.java, PointCommandSerializer(version)) - /*put(SerializablePath.Cube::class.java, SerializablePath.PathActionCubeSerializer(version)) + put(SerializablePath.Cube::class.java, SerializablePath.PathActionCubeSerializer(version)) put(Bitmap::class.java, BitmapSerializer(version)) put(SmudgePathCommand::class.java, SmudgePathCommandSerializer(version)) put(ColorHistory::class.java, ColorHistorySerializer(version)) - put(ClippingCommand::class.java, ClippingCommandSerializer(version)) + /* put(ClippingCommand::class.java, ClippingCommandSerializer(version)) put(LayerOpacityCommand::class.java, LayerOpacityCommandSerializer(version)) - */ } + */} } diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/SmudgePathCommand.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/SmudgePathCommand.kt new file mode 100644 index 00000000..0f96452c --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/SmudgePathCommand.kt @@ -0,0 +1,79 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2015 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.paintroid.command.implementation + +import android.graphics.Bitmap +import android.graphics.PointF +import android.graphics.Canvas +import android.graphics.ColorMatrix +import android.graphics.Paint +import android.graphics.ColorMatrixColorFilter +import android.graphics.RectF +import org.catrobat.paintroid.command.Command +import org.catrobat.paintroid.contract.LayerContracts +import org.catrobat.paintroid.tools.implementation.PRESSURE_UPDATE_STEP + +class SmudgePathCommand(bitmap: Bitmap, pointPath: MutableList, maxPressure: Float, maxSize: Float, minSize: Float) : Command { + + var originalBitmap = bitmap; private set + var pointPath = pointPath; private set + var maxPressure = maxPressure; private set + var maxSize = maxSize; private set + var minSize = minSize; private set + + override fun run(canvas: Canvas, layerModel: LayerContracts.Model) { + val step = (maxSize - minSize) / pointPath.size + var size = maxSize + var pressure = maxPressure + val colorMatrix = ColorMatrix() + val paint = Paint() + var bitmap = originalBitmap.copy(Bitmap.Config.ARGB_8888, false) + + pointPath.forEach { + colorMatrix.setScale(1f, 1f, 1f, pressure) + paint.colorFilter = ColorMatrixColorFilter(colorMatrix) + + val newBitmap = Bitmap.createBitmap(maxSize.toInt(), maxSize.toInt(), Bitmap.Config.ARGB_8888) + + newBitmap.let { + Canvas(it).apply { + drawBitmap(bitmap, 0f, 0f, paint) + } + } + + bitmap.recycle() + bitmap = newBitmap + + val rect = RectF(-size / 2f, -size / 2f, size / 2f, size / 2f) + with(canvas) { + save() + translate(it.x, it.y) + drawBitmap(bitmap, null, rect, Paint(Paint.DITHER_FLAG)) + restore() + } + size -= step + pressure -= PRESSURE_UPDATE_STEP + } + } + + override fun freeResources() { + originalBitmap.recycle() + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/BitmapSerializer.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/BitmapSerializer.kt new file mode 100644 index 00000000..750f6bd6 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/BitmapSerializer.kt @@ -0,0 +1,39 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.serialization + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output + +const val BITMAP_SERIALIZATION_COMPRESSION_QUALITY = 100 + +class BitmapSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, bitmap: Bitmap) { + bitmap.compress(Bitmap.CompressFormat.PNG, BITMAP_SERIALIZATION_COMPRESSION_QUALITY, output) + } + + override fun read(kryo: Kryo, input: Input, type: Class): Bitmap = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): Bitmap = + BitmapFactory.decodeStream(input) +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/SmudgePathCommandSerializer.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/SmudgePathCommandSerializer.kt new file mode 100644 index 00000000..59cdb7d8 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/SmudgePathCommandSerializer.kt @@ -0,0 +1,69 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.serialization + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.PointF +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import org.catrobat.paintroid.command.implementation.SmudgePathCommand + +class SmudgePathCommandSerializer(version: Int) : VersionSerializer(version) { + + companion object { + private const val COMPRESSION_QUALITY = 100 + } + + override fun write(kryo: Kryo, output: Output, command: SmudgePathCommand) { + with(kryo) { + with(output) { + command.originalBitmap.compress(Bitmap.CompressFormat.PNG, COMPRESSION_QUALITY, output) + writeInt(command.pointPath.size) + command.pointPath.forEach { + writeObject(output, it) + } + writeFloat(command.maxPressure) + writeFloat(command.maxSize) + writeFloat(command.minSize) + } + } + } + + override fun read(kryo: Kryo, input: Input, type: Class): SmudgePathCommand = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): SmudgePathCommand { + return with(kryo) { + with(input) { + val originalBitmap = BitmapFactory.decodeStream(input) + val pointPath = mutableListOf() + val size = readInt() + repeat(size) { + pointPath.add(readObject(input, PointF::class.java)) + } + val maxPressure = readFloat() + val maxSize = readFloat() + val minSize = readFloat() + SmudgePathCommand(originalBitmap, pointPath, maxPressure, maxSize, minSize) + } + } + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/tools/implementation/SmudgeTool.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/implementation/SmudgeTool.kt new file mode 100644 index 00000000..1603447d --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/tools/implementation/SmudgeTool.kt @@ -0,0 +1,317 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.tools.implementation + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.ColorMatrix +import android.graphics.ColorMatrixColorFilter +import android.graphics.Paint +import android.graphics.Path +import android.graphics.PointF +import android.graphics.RectF +import androidx.annotation.VisibleForTesting +import androidx.test.espresso.idling.CountingIdlingResource +import org.catrobat.paintroid.command.CommandManager +import org.catrobat.paintroid.tools.ContextCallback +import org.catrobat.paintroid.tools.ToolPaint +import org.catrobat.paintroid.tools.ToolType +import org.catrobat.paintroid.tools.Workspace +import org.catrobat.paintroid.tools.common.CommonBrushChangedListener +import org.catrobat.paintroid.tools.common.CommonBrushPreviewListener +import org.catrobat.paintroid.tools.options.SmudgeToolOptionsView +import org.catrobat.paintroid.tools.options.ToolOptionsViewController +import kotlin.math.sqrt + +const val PERCENT_100 = 100f +const val BITMAP_ROTATION_FACTOR = -0.0f +const val DEFAULT_PRESSURE_IN_PERCENT = 50 +const val MAX_PRESSURE = 1f +const val MIN_PRESSURE = 0.85f +const val DEFAULT_DRAG_IN_PERCENT = 50 +const val DISTANCE_SMOOTHING = 3f +const val DRAW_THRESHOLD = 0.8f +const val PRESSURE_UPDATE_STEP = 0.004f + +class SmudgeTool( + smudgeToolOptionsView: SmudgeToolOptionsView, + contextCallback: ContextCallback, + toolOptionsViewController: ToolOptionsViewController, + toolPaint: ToolPaint, + workspace: Workspace, + idlingResource: CountingIdlingResource, + commandManager: CommandManager +) : BaseTool(contextCallback, toolOptionsViewController, toolPaint, workspace, idlingResource, commandManager) { + + override var drawTime: Long = 0 + override fun handleUpAnimations(coordinate: PointF?) { + super.handleUp(coordinate) + } + + override fun handleDownAnimations(coordinate: PointF?) { + super.handleDown(coordinate) + } + + private var currentBitmap: Bitmap? = null + private var prevPoint: PointF? = null + private var numOfPointsOnPath = -1 + + @VisibleForTesting + var maxPressure = 0f + + @VisibleForTesting + var pressure = maxPressure + + @VisibleForTesting + var maxSmudgeSize = toolPaint.strokeWidth + + @VisibleForTesting + var minSmudgeSize = 0f + + @VisibleForTesting + val pointArray = mutableListOf() + + override val toolType: ToolType + get() = ToolType.SMUDGE + + init { + smudgeToolOptionsView.setBrushChangedListener(CommonBrushChangedListener(this)) + smudgeToolOptionsView.setBrushPreviewListener( + CommonBrushPreviewListener( + toolPaint, + toolType + ) + ) + smudgeToolOptionsView.setCurrentPaint(toolPaint.paint) + smudgeToolOptionsView.setStrokeCapButtonChecked(toolPaint.strokeCap) + smudgeToolOptionsView.setCallback(object : SmudgeToolOptionsView.Callback { + override fun onPressureChanged(pressure: Int) { + updatePressure(pressure) + } + + override fun onDragChanged(drag: Int) { + updateDrag(drag) + } + }) + + updatePressure(DEFAULT_PRESSURE_IN_PERCENT) + updateDrag(DEFAULT_DRAG_IN_PERCENT) + } + + fun updatePressure(pressureInPercent: Int) { + val onePercent = (MAX_PRESSURE - MIN_PRESSURE) / PERCENT_100 + maxPressure = MIN_PRESSURE + onePercent * pressureInPercent + pressure = maxPressure + } + + fun updateDrag(dragInPercent: Int) { + val onePercent = maxSmudgeSize / PERCENT_100 + minSmudgeSize = onePercent * dragInPercent + } + + override fun handleDown(coordinate: PointF?): Boolean { + coordinate ?: return false + + if (maxSmudgeSize != toolPaint.strokeWidth) { + val ratio = minSmudgeSize / maxSmudgeSize + maxSmudgeSize = toolPaint.strokeWidth + minSmudgeSize = maxSmudgeSize * ratio + } + + val layerBitmap = workspace.bitmapOfCurrentLayer + currentBitmap = Bitmap.createBitmap( + maxSmudgeSize.toInt(), + maxSmudgeSize.toInt(), + Bitmap.Config.ARGB_8888 + ) + currentBitmap?.let { + Canvas(it).apply { + translate(-coordinate.x + maxSmudgeSize / 2f, -coordinate.y + maxSmudgeSize / 2f) + rotate(BITMAP_ROTATION_FACTOR, coordinate.x, coordinate.y) + layerBitmap?.let { bitmap -> + drawBitmap(bitmap, 0f, 0f, null) + } + } + + if (toolPaint.strokeCap == Paint.Cap.ROUND) { + currentBitmap = getBitmapClippedCircle(it) + } + } + + if (!currentBitmapHasColor()) { + currentBitmap?.recycle() + currentBitmap = null + return false + } + + prevPoint = PointF(coordinate.x, coordinate.y) + prevPoint?.apply { + pointArray.add(PointF(x, y)) + } + + return true + } + + override fun handleMove(coordinate: PointF?, shouldAnimate: Boolean): Boolean { + coordinate ?: return false + + if (currentBitmap != null) { + if (pressure < DRAW_THRESHOLD) { // Needed to stop drawing preview when bitmap becomes too transparent. Has no effect on final drawing. + return false + } + + prevPoint?.apply { + val x1 = coordinate.x - x + val y1 = coordinate.y - y + + val distance = (sqrt(x1 * x1 + y1 * y1) / DISTANCE_SMOOTHING).toInt() + val xInterval = x1 / distance + val yInterval = y1 / distance + + repeat(distance) { + x += xInterval + y += yInterval + + pressure -= PRESSURE_UPDATE_STEP + + pointArray.add(PointF(x, y)) + } + } + return true + } else { + return false + } + } + + private fun getBitmapClippedCircle(bitmap: Bitmap): Bitmap { + val width = bitmap.width + val height = bitmap.height + val outputBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + val path = Path() + path.addCircle( + (width / 2).toFloat(), + (height / 2).toFloat(), + kotlin.math.min(width, height / 2).toFloat(), + Path.Direction.CCW + ) + val canvas = Canvas(outputBitmap) + canvas.clipPath(path) + canvas.drawBitmap(bitmap, 0f, 0f, null) + return outputBitmap + } + + private fun currentBitmapHasColor(): Boolean { + currentBitmap?.apply { + for (x in 0 until width) { + for (y in 0 until height) { + if (getPixel(x, y) != 0) { + return true + } + } + } + } + return false + } + + override fun handleUp(coordinate: PointF?): Boolean { + coordinate ?: return false + + if (pointArray.isNotEmpty() && currentBitmap != null) { + currentBitmap?.let { + val command = commandFactory.createSmudgePathCommand( + it, + pointArray, + maxPressure, + maxSmudgeSize, + minSmudgeSize + ) + commandManager.addCommand(command) + } + + numOfPointsOnPath = if (numOfPointsOnPath < 0) { + pointArray.size + } else { + (numOfPointsOnPath + pointArray.size) / 2 + } + + pressure = maxPressure + pointArray.clear() + currentBitmap?.recycle() + currentBitmap = null + return true + } else { + return false + } + } + + override fun toolPositionCoordinates(coordinate: PointF): PointF = coordinate + + override fun draw(canvas: Canvas) { + if (pointArray.isNotEmpty()) { + val pointPath = pointArray.toMutableList() + + val step = if (numOfPointsOnPath < 0) { + (maxSmudgeSize - minSmudgeSize) / pointPath.size + } else { + (maxSmudgeSize - minSmudgeSize) / numOfPointsOnPath + } + + var size = maxSmudgeSize + var pressure = maxPressure + val colorMatrix = ColorMatrix() + val paint = Paint() + var bitmap = currentBitmap?.copy(Bitmap.Config.ARGB_8888, false) + + pointPath.forEach { + colorMatrix.setScale(1f, 1f, 1f, pressure) + paint.colorFilter = ColorMatrixColorFilter(colorMatrix) + + val newBitmap = Bitmap.createBitmap( + maxSmudgeSize.toInt(), + maxSmudgeSize.toInt(), + Bitmap.Config.ARGB_8888 + ) + + Canvas(newBitmap).apply { + bitmap?.let { currentBitmap -> + drawBitmap(currentBitmap, 0f, 0f, paint) + } + } + + bitmap?.recycle() + bitmap = newBitmap + + val rect = RectF(-size / 2f, -size / 2f, size / 2f, size / 2f) + with(canvas) { + save() + clipRect(0, 0, workspace.width, workspace.height) + translate(it.x, it.y) + bitmap?.let { currentBitmap -> + drawBitmap(currentBitmap, null, rect, Paint(Paint.DITHER_FLAG)) + } + restore() + } + size -= step + pressure -= PRESSURE_UPDATE_STEP + } + + bitmap?.recycle() + } + } +} From 359e5ffc285d54c87fbd660971980a95ae5a3c3c Mon Sep 17 00:00:00 2001 From: Tim Celec Date: Sat, 3 Aug 2024 15:47:20 +0200 Subject: [PATCH 24/25] PAINTROID-761 - LayerOpacityCommand work --- .../org/catrobat/colorpicker/ColorHistory.kt | 22 ++++++++ .../org/catrobat/paintroid/FileReader.kt | 18 ++++++- .../command/implementation/ClippingCommand.kt | 36 +++++++++++++ .../implementation/LayerOpacityCommand.kt | 38 ++++++++++++++ .../ClippingCommandSerializer.kt | 27 ++++++++++ .../serialization/ColorHistorySerializer.kt | 50 +++++++++++++++++++ .../LayerOpacityCommandSerializer.kt | 44 ++++++++++++++++ 7 files changed, 233 insertions(+), 2 deletions(-) create mode 100644 android/app/src/main/kotlin/org/catrobat/colorpicker/ColorHistory.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/ClippingCommand.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/LayerOpacityCommand.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/ClippingCommandSerializer.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/ColorHistorySerializer.kt create mode 100644 android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/LayerOpacityCommandSerializer.kt diff --git a/android/app/src/main/kotlin/org/catrobat/colorpicker/ColorHistory.kt b/android/app/src/main/kotlin/org/catrobat/colorpicker/ColorHistory.kt new file mode 100644 index 00000000..d7202362 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/colorpicker/ColorHistory.kt @@ -0,0 +1,22 @@ +package org.catrobat.paintroid.colorpicker + +import java.io.Serializable + +const val COLOR_HISTORY_SIZE = 4 + +class ColorHistory : Serializable { + private val colorHistory: ArrayList = arrayListOf() + + val colors: ArrayList + get() = colorHistory + + fun addColor(color: Int) { + if (colorHistory.lastOrNull() != color) { + colorHistory.add(color) + } + + if (colorHistory.size > COLOR_HISTORY_SIZE) { + colorHistory.removeFirst() + } + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt index 95036c55..65bad6cf 100644 --- a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt @@ -98,6 +98,20 @@ import org.catrobat.paintroid.command.serialization.BitmapSerializer import org.catrobat.paintroid.command.implementation.SmudgePathCommand import org.catrobat.paintroid.command.serialization.SmudgePathCommandSerializer + +//import org.catrobat.paintroid.command.implementation.SmudgePathCommand +import org.catrobat.paintroid.colorpicker.ColorHistory +import org.catrobat.paintroid.command.serialization.ColorHistorySerializer + + + +import org.catrobat.paintroid.command.implementation.ClippingCommand +import org.catrobat.paintroid.command.serialization.ClippingCommandSerializer + + +import org.catrobat.paintroid.command.implementation.LayerOpacityCommand +import org.catrobat.paintroid.command.serialization.LayerOpacityCommandSerializer + import android.graphics.Paint import android.graphics.Point import android.graphics.PointF @@ -182,9 +196,9 @@ class FileReader(private val context : Context) put(Bitmap::class.java, BitmapSerializer(version)) put(SmudgePathCommand::class.java, SmudgePathCommandSerializer(version)) put(ColorHistory::class.java, ColorHistorySerializer(version)) - /* put(ClippingCommand::class.java, ClippingCommandSerializer(version)) + put(ClippingCommand::class.java, ClippingCommandSerializer(version)) put(LayerOpacityCommand::class.java, LayerOpacityCommandSerializer(version)) - */} + } } diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/ClippingCommand.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/ClippingCommand.kt new file mode 100644 index 00000000..2ebf7f70 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/ClippingCommand.kt @@ -0,0 +1,36 @@ +package org.catrobat.paintroid.command.implementation + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.PorterDuff +import android.graphics.PorterDuffXfermode +import android.graphics.Rect +import org.catrobat.paintroid.command.Command +import org.catrobat.paintroid.contract.LayerContracts + +class ClippingCommand(bitmap: Bitmap, pathBitmap: Bitmap) : Command { + + var bitmap: Bitmap? = bitmap.copy(bitmap.config, true); private set + var pathBitmap: Bitmap? = pathBitmap.copy(pathBitmap.config, true); private set + + override fun run(canvas: Canvas, layerModel: LayerContracts.Model) { + val bitmapToDraw = bitmap + bitmapToDraw ?: return + val wholeRect = Rect(0, 0, bitmapToDraw.width, bitmapToDraw.height) + layerModel.currentLayer?.bitmap?.eraseColor(Color.TRANSPARENT) + val paint = Paint() + with(canvas) { + save() + pathBitmap?.let { drawBitmap(it, null, wholeRect, null) } + paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN) + bitmap?.let { drawBitmap(it, null, wholeRect, paint) } + restore() + } + } + override fun freeResources() { + bitmap?.recycle() + pathBitmap?.recycle() + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/LayerOpacityCommand.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/LayerOpacityCommand.kt new file mode 100644 index 00000000..ba1cc68b --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/implementation/LayerOpacityCommand.kt @@ -0,0 +1,38 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.implementation + +import android.graphics.Canvas +import org.catrobat.paintroid.command.Command +import org.catrobat.paintroid.contract.LayerContracts + +class LayerOpacityCommand( + val position: Int, + val opacityPercentage: Int +) : Command { + + override fun run(canvas: Canvas, layerModel: LayerContracts.Model) { + val layer = layerModel.getLayerAt(position) + layer?.opacityPercentage = opacityPercentage + } + + override fun freeResources() { + // No resources to free + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/ClippingCommandSerializer.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/ClippingCommandSerializer.kt new file mode 100644 index 00000000..e6f4ab26 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/ClippingCommandSerializer.kt @@ -0,0 +1,27 @@ +package org.catrobat.paintroid.command.serialization + +import android.graphics.Bitmap +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import org.catrobat.paintroid.command.implementation.ClippingCommand + +class ClippingCommandSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, command: ClippingCommand) { + with(kryo) { + writeObject(output, command.bitmap) + writeObject(output, command.pathBitmap) + } + } + + override fun read(kryo: Kryo, input: Input, type: Class): ClippingCommand = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): ClippingCommand { + return with(kryo) { + val bitmap = readObject(input, Bitmap::class.java) + val pathBitmap = readObject(input, Bitmap::class.java) + ClippingCommand(bitmap, pathBitmap) + } + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/ColorHistorySerializer.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/ColorHistorySerializer.kt new file mode 100644 index 00000000..d6d81e37 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/ColorHistorySerializer.kt @@ -0,0 +1,50 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.serialization + +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import org.catrobat.paintroid.colorpicker.ColorHistory + +class ColorHistorySerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, colorHistory: ColorHistory) { + with(output) { + val colors = colorHistory.colors + writeInt(colors.size) + colors.forEach { intValue -> + writeInt(intValue) + } + } + } + + override fun read(kryo: Kryo, input: Input, type: Class): ColorHistory = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): ColorHistory { + return with(input) { + val size = readInt() + val colorHistory = ColorHistory() + repeat(size) { + colorHistory.addColor(readInt()) + } + colorHistory + } + } +} diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/LayerOpacityCommandSerializer.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/LayerOpacityCommandSerializer.kt new file mode 100644 index 00000000..d28f4eb7 --- /dev/null +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/command/serialization/LayerOpacityCommandSerializer.kt @@ -0,0 +1,44 @@ +/* + * Paintroid: An image manipulation application for Android. + * Copyright (C) 2010-2022 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.catrobat.paintroid.command.serialization + +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import org.catrobat.paintroid.command.implementation.LayerOpacityCommand + +class LayerOpacityCommandSerializer(version: Int) : VersionSerializer(version) { + override fun write(kryo: Kryo, output: Output, command: LayerOpacityCommand) { + with(output) { + writeInt(command.position) + writeInt(command.opacityPercentage) + } + } + + override fun read(kryo: Kryo, input: Input, type: Class): LayerOpacityCommand = + super.handleVersions(this, kryo, input, type) + + override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class): LayerOpacityCommand { + return with(input) { + val position = readInt() + val opacityPercentage = readInt() + LayerOpacityCommand(position, opacityPercentage) + } + } +} From f0ced4ce59cef25e18778962c11662bce236c93d Mon Sep 17 00:00:00 2001 From: Tim Celec Date: Sat, 3 Aug 2024 18:06:48 +0200 Subject: [PATCH 25/25] PAINTROID-761 - registerClasses --- .../main/kotlin/org/catrobat/paintroid/FileReader.kt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt index 65bad6cf..9740ec09 100644 --- a/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt +++ b/android/app/src/main/kotlin/org/catrobat/paintroid/FileReader.kt @@ -141,7 +141,7 @@ class FileReader(private val context : Context) } init { setRegisterMapVersion(CURRENT_IMAGE_VERSION) - // registerClasses() + registerClasses() } /* fun readFromFile(uri: String): CatrobatFileContent{ var commandModel: CommandManagerModel @@ -200,7 +200,14 @@ class FileReader(private val context : Context) put(LayerOpacityCommand::class.java, LayerOpacityCommandSerializer(version)) } } - + private fun registerClasses() { + registerMap.forEach { (classRegister, serializer) -> + val registration = kryo.register(classRegister) + serializer?.let { + registration.serializer = serializer + } + } + } } \ No newline at end of file