diff --git a/CHANGELOG.md b/CHANGELOG.md index b55e030..21d69b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 0.18.0 +- Add Memento Editor. + ## 0.17.16 - refactoring - Simplifying the ternary construction. - Remove multiline comment from main README. diff --git a/README.md b/README.md index 87b9ef5..84db0c3 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ It contains **Dart** examples for all classic **GoF** design patterns. - [ ] Interpreter - [ ] **Iterator** - [ ] **Mediator** - - [x] **Memento** - [[Conceptual](https://github.com/RefactoringGuru/design-patterns-dart/tree/master/patterns/memento/conceptual)] + - [x] **Memento** - [[Conceptual](https://github.com/RefactoringGuru/design-patterns-dart/tree/master/patterns/memento/conceptual)] [[Memento Editor](https://github.com/RefactoringGuru/design-patterns-dart/tree/master/patterns/memento/memento_editor)] - [x] **Observer** - [[Open-Close Editor Events](https://github.com/RefactoringGuru/design-patterns-dart/tree/master/patterns/observer/open_close_editor_events)] [[AppObserver](https://github.com/RefactoringGuru/design-patterns-dart/tree/master/patterns/observer/app_observer)] [[Subscriber Flutter Widget](https://github.com/RefactoringGuru/design-patterns-dart/tree/master/patterns/observer/subscriber_flutter_widget)] - [ ] **State** - [ ] **Template Method** @@ -33,7 +33,7 @@ It contains **Dart** examples for all classic **GoF** design patterns. ## Requirements The examples were written in **Dart 2.15**. -Some complex examples require **Flutter 2.15.0**. +Some complex examples require **Flutter 2.12**. ## Contributor's Guide diff --git a/bin/deploy_flutter_demos.dart b/bin/deploy_flutter_demos.dart index b6e0509..387cd87 100644 --- a/bin/deploy_flutter_demos.dart +++ b/bin/deploy_flutter_demos.dart @@ -119,7 +119,7 @@ Future repositoryOriginUrl(Directory workingDir) async { Future lastProjectCommit() async { final rawCommit = await cmd('git log -1 --pretty=%B', workingDirectory: projectDir); - final formatCommit = rawCommit.replaceAll(' ', '_'); + final formatCommit = rawCommit.replaceAll(' ', '_').replaceAll('&', ''); return 'auto_commit:_$formatCommit'; } diff --git a/bin/main.dart b/bin/main.dart index 2a23b38..935d592 100644 --- a/bin/main.dart +++ b/bin/main.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../patterns/observer/subscriber_flutter_widget/main.dart'; import '../patterns/adapter/flutter_adapter/main.dart'; +import '../patterns/memento/memento_editor/main.dart'; void main() { runApp(MyApp()); @@ -12,10 +13,11 @@ class MyApp extends StatelessWidget { return MaterialApp( title: 'Refactoring Guru: Flutter launcher', theme: ThemeData(primarySwatch: Colors.pink), - initialRoute: '/adapter/flutter_adapter', + initialRoute: '/memento/flutter_memento_editor', routes: { '/observer/subscriber_flutter_widget': (_) => SubscriberFlutterApp(), '/adapter/flutter_adapter': (_) => FlutterAdapterApp(), + '/memento/flutter_memento_editor': (_) => FlutterMementoEditorApp(), }, ); } diff --git a/patterns/adapter/flutter_adapter/adapter/classic_app_render_object.dart b/patterns/adapter/flutter_adapter/adapter/classic_app_render_object.dart index 4470082..0811cda 100644 --- a/patterns/adapter/flutter_adapter/adapter/classic_app_render_object.dart +++ b/patterns/adapter/flutter_adapter/adapter/classic_app_render_object.dart @@ -63,13 +63,16 @@ class ClassicAppRenderObject extends RenderBox { @override void handleEvent(PointerEvent event, covariant BoxHitTestEntry entry) { if (event is PointerHoverEvent || event is PointerMoveEvent) { - } else if (event is PointerDownEvent) { - if (event.buttons == kPrimaryMouseButton) { - _classicApp.onMouseDown(); - } else if (event.buttons == kSecondaryMouseButton) {} - else if (event.buttons == kMiddleMouseButton) {} + _classicApp.onMouseMove(event.position.dx, event.position.dy); } else if (event is PointerScrollEvent) { _classicApp.onPointerWheel(event.scrollDelta.dx, event.scrollDelta.dy); + } else if (event is PointerDownEvent) { + if (event.buttons == kPrimaryMouseButton) { + _classicApp.onMouseDown(event.position.dx, event.position.dy); + } else if (event.buttons == kSecondaryMouseButton) { + } else if (event.buttons == kMiddleMouseButton) {} + } else if (event is PointerUpEvent) { + _classicApp.onMouseUp(); } } diff --git a/patterns/adapter/flutter_adapter/classic_app/classic_app.dart b/patterns/adapter/flutter_adapter/classic_app/classic_app.dart index f19a5c2..7e56111 100644 --- a/patterns/adapter/flutter_adapter/classic_app/classic_app.dart +++ b/patterns/adapter/flutter_adapter/classic_app/classic_app.dart @@ -7,7 +7,11 @@ import 'repaint_compatible.dart'; abstract class ClassicApp implements RepaintCompatible { final events = AppObserver(); - void onMouseDown() {} + void onMouseDown(double x, double y) {} + + void onMouseUp() {} + + void onMouseMove(double x, double y) {} void onPointerWheel(double deltaX, double deltaY) {} diff --git a/patterns/adapter/flutter_adapter/client_app/app.dart b/patterns/adapter/flutter_adapter/client_app/app.dart index b648c75..7a4f5a7 100644 --- a/patterns/adapter/flutter_adapter/client_app/app.dart +++ b/patterns/adapter/flutter_adapter/client_app/app.dart @@ -19,7 +19,7 @@ class App extends ClassicApp { } @override - void onMouseDown() { + void onMouseDown(_, __) { textColoring.color = colorRules.nextColor(textColoring.color); } diff --git a/patterns/memento/memento_editor/README.md b/patterns/memento/memento_editor/README.md new file mode 100644 index 0000000..622c72f --- /dev/null +++ b/patterns/memento/memento_editor/README.md @@ -0,0 +1,48 @@ +# Memento pattern +Memento is a behavioral design pattern that lets you save and restore the previous state of an +object without revealing the details of its implementation. + +Tutorial: [here](https://refactoring.guru/design-patterns/memento). + +### Online demo: +Click on the picture to see a [demo](https://RefactoringGuru.github.io/design-patterns-dart/#/memento/flutter_memento_editor). + +[![image](https://user-images.githubusercontent.com/8049534/165401175-88bc4593-4624-45b4-8c03-6f1390ed771a.png)](https://refactoringguru.github.io/design-patterns-dart/#/memento/flutter_memento_editor) + + +### Dependency Patterns +This complex example includes these implementations: +- [[AppObserver](https://github.com/RefactoringGuru/design-patterns-dart/tree/master/patterns/observer/app_observer)] +- [[SubscriberWidget](https://github.com/RefactoringGuru/design-patterns-dart/tree/master/patterns/observer/subscriber_flutter_widget)] + +### Diagram: +![image](https://user-images.githubusercontent.com/8049534/165399085-06835617-8ef1-4e2f-930f-03d730433afb.png) + +### Client code: +```dart +class MementoEditorApplication { + final editor = Editor(); + final caretaker = Caretaker(); + + void createDefaultShapes() {/*...*/} + + void saveState() { + final snapshot = editor.backup(); + + if (caretaker.isSnapshotExists(snapshot)) { + return; + } + + final memento = Memento(DateTime.now(), snapshot); + caretaker.addMemento(memento); + editor.events.notify(MementoCreateEvent()); + } + + void restoreState(Memento memento) { + editor + ..unSelect() + ..restore(memento.snapshot) + ..repaint(); + } +} +``` diff --git a/patterns/memento/memento_editor/application.dart b/patterns/memento/memento_editor/application.dart new file mode 100644 index 0000000..a98d3cc --- /dev/null +++ b/patterns/memento/memento_editor/application.dart @@ -0,0 +1,44 @@ +import 'dart:math'; + +import 'editor/memento_create_event.dart'; +import 'editor/editor.dart'; +import 'memento_pattern/caretaker.dart'; +import 'memento_pattern/memento.dart'; +import 'shapes/shape.dart'; + +class MementoEditorApplication { + final editor = Editor(); + final caretaker = Caretaker(); + + MementoEditorApplication() { + createDefaultShapes(); + } + + void createDefaultShapes() { + const radius = 300.0; + for (var i = 0; i < 7; i++) { + final x = 60 + radius + cos(i / 1.15) * radius; + final y = 60 + radius + sin(i / 1.15) * radius; + editor.shapes.add(Shape(x, y)); + } + } + + void saveState() { + final snapshot = editor.backup(); + + if (caretaker.isSnapshotExists(snapshot)) { + return; + } + + final memento = Memento(DateTime.now(), snapshot); + caretaker.addMemento(memento); + editor.events.notify(MementoCreateEvent()); + } + + void restoreState(Memento memento) { + editor + ..unSelect() + ..restore(memento.snapshot) + ..repaint(); + } +} diff --git a/patterns/memento/memento_editor/editor/editor.dart b/patterns/memento/memento_editor/editor/editor.dart new file mode 100644 index 0000000..8ab97ac --- /dev/null +++ b/patterns/memento/memento_editor/editor/editor.dart @@ -0,0 +1,22 @@ +import 'dart:ui'; + +import '../../../adapter/flutter_adapter/classic_app/classic_app.dart'; +import '../memento_pattern/originator.dart'; +import '../shapes/shapes.dart'; +import 'manipulator.dart'; + +class Editor extends ClassicApp with Manipulator, Shapes, Originator { + @override + void onPaint(Canvas canvas, Size canvasSize) { + _paintBackground(canvas, canvasSize); + paintShapes(canvas); + activeShape?.paintSelectionBox(canvas); + } + + void _paintBackground(Canvas canvas, Size canvasSize) { + canvas.drawRect( + Offset.zero & canvasSize, + Paint()..color = Color(0xff404040), + ); + } +} diff --git a/patterns/memento/memento_editor/editor/manipulator.dart b/patterns/memento/memento_editor/editor/manipulator.dart new file mode 100644 index 0000000..c883e97 --- /dev/null +++ b/patterns/memento/memento_editor/editor/manipulator.dart @@ -0,0 +1,42 @@ +import '../../../adapter/flutter_adapter/classic_app/classic_app.dart'; +import '../shapes/shapes.dart'; + +mixin Manipulator implements ClassicApp, Shapes { + var _isMouseDown = false; + + @override + void onMouseDown(double x, double y) { + _isMouseDown = true; + final currSelection = activeShape; + + select(x, y); + + if (currSelection == activeShape) { + return; + } + + if (activeShape == null) { + unSelect(); + } + + repaint(); + } + + @override + void onMouseMove(double x, double y) { + if (_isMouseDown) { + activeShape?.dragTo(x, y); + repaint(); + } + } + + @override + void onPointerWheel(double deltaX, double deltaY) { + activeShape?.changeSize(deltaY / 5); + } + + @override + void onMouseUp() { + _isMouseDown = false; + } +} diff --git a/patterns/memento/memento_editor/editor/memento_create_event.dart b/patterns/memento/memento_editor/editor/memento_create_event.dart new file mode 100644 index 0000000..67e459a --- /dev/null +++ b/patterns/memento/memento_editor/editor/memento_create_event.dart @@ -0,0 +1,3 @@ +import '../../../observer/app_observer/observer/event.dart'; + +class MementoCreateEvent extends Event {} diff --git a/patterns/memento/memento_editor/main.dart b/patterns/memento/memento_editor/main.dart new file mode 100644 index 0000000..dc13777 --- /dev/null +++ b/patterns/memento/memento_editor/main.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +import '../../adapter/flutter_adapter/adapter/classic_app_adapter_widget.dart' + as adapter; +import 'application.dart'; +import 'widgets/right_panel_widget.dart'; + + +class FlutterMementoEditorApp extends StatefulWidget { + @override + State createState() => + _FlutterMementoEditorAppState(); +} + +class _FlutterMementoEditorAppState extends State { + final app = MementoEditorApplication(); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Row( + children: [ + Expanded( + child: adapter.ClassicAppAdapterWidget( + classicApp: app.editor, + ), + ), + RightPanelWidget(app: app), + ], + ), + ); + } +} diff --git a/patterns/memento/memento_editor/memento_pattern/caretaker.dart b/patterns/memento/memento_editor/memento_pattern/caretaker.dart new file mode 100644 index 0000000..e736f00 --- /dev/null +++ b/patterns/memento/memento_editor/memento_pattern/caretaker.dart @@ -0,0 +1,18 @@ +import 'memento.dart'; +import 'snapshot.dart'; + +class Caretaker { + final _mementoList = []; + + List get list => List.unmodifiable(_mementoList); + + void addMemento(Memento memento) { + _mementoList.add(memento); + } + + bool isSnapshotExists(Snapshot snapshot) { + return list.any( + (e) => e.snapshot == snapshot, + ); + } +} diff --git a/patterns/memento/memento_editor/memento_pattern/memento.dart b/patterns/memento/memento_editor/memento_pattern/memento.dart new file mode 100644 index 0000000..2bfb0b7 --- /dev/null +++ b/patterns/memento/memento_editor/memento_pattern/memento.dart @@ -0,0 +1,9 @@ + +import 'snapshot.dart'; + +class Memento { + final DateTime time; + final Snapshot snapshot; + + Memento(this.time, this.snapshot); +} diff --git a/patterns/memento/memento_editor/memento_pattern/originator.dart b/patterns/memento/memento_editor/memento_pattern/originator.dart new file mode 100644 index 0000000..8db1b44 --- /dev/null +++ b/patterns/memento/memento_editor/memento_pattern/originator.dart @@ -0,0 +1,101 @@ +import 'dart:convert'; +import 'dart:typed_data'; +import 'dart:ui'; + +import '../../../adapter/flutter_adapter/classic_app/classic_app.dart'; +import '../shapes/shapes.dart'; +import '../shapes/shape.dart'; +import 'snapshot.dart'; + +mixin Originator implements Shapes, ClassicApp { + Snapshot backup() { + final data = _allocateBuffer(); + _writeShapes(data); + _writeSelectedIndex(data); + return _toSnapshot(data); + } + + void restore(Snapshot snapshot) { + final byteData = _fromSnapshotToByteData(snapshot); + final newShapes = _readShapes(byteData); + final selectedIndex = _readSelectedIndex(byteData); + shapes.clear(); + shapes.addAll(newShapes); + selectByIndex(selectedIndex); + } + + static const _shapeByteSize = 16; + static const _selectedIndexByteSize = 4; + + ByteData _allocateBuffer() { + final byteSize = shapes.length * _shapeByteSize + _selectedIndexByteSize; + return ByteData(byteSize); + } + + ByteData _fromSnapshotToByteData(Snapshot snapshot) { + final unBase = Base64Decoder().convert(snapshot); + final byteData = ByteData.sublistView(unBase); + return byteData; + } + + void _writeSelectedIndex(ByteData data) { + late final int selectedIndex; + + if (activeShape == null) { + selectedIndex = -1; + } else { + selectedIndex = shapes.indexOf(activeShape!.shape); + } + + final byteOffset = data.lengthInBytes - _selectedIndexByteSize; + data.setInt32(byteOffset, selectedIndex); + } + + int _writeShapes(ByteData data) { + var byteOffset = 0; + + for (final shape in shapes) { + data + ..setFloat32(byteOffset, shape.x) + ..setFloat32(byteOffset + 4, shape.y) + ..setInt32(byteOffset + 8, shape.color.value) + ..setFloat32(byteOffset + 12, shape.size); + byteOffset += 16; + } + + return byteOffset; + } + + int _getNumberOfShapes(ByteData byteData) { + return (byteData.lengthInBytes - _selectedIndexByteSize) ~/ _shapeByteSize; + } + + List _readShapes(ByteData byteData) { + final shapeCount = _getNumberOfShapes(byteData); + var byteOffset = 0; + final shapes = []; + + for (var i = 0; i < shapeCount; i++) { + final shape = Shape( + byteData.getFloat32(byteOffset), + byteData.getFloat32(byteOffset + 4), + Color(byteData.getInt32(byteOffset + 8)), + byteData.getFloat32(byteOffset + 12), + ); + shapes.add(shape); + byteOffset += 16; + } + + return shapes; + } + + int _readSelectedIndex(ByteData byteData) { + return byteData.getInt32(byteData.lengthInBytes - _selectedIndexByteSize); + } + + Snapshot _toSnapshot(ByteData data) { + return Base64Encoder().convert( + data.buffer.asUint8List(), + ); + } +} diff --git a/patterns/memento/memento_editor/memento_pattern/snapshot.dart b/patterns/memento/memento_editor/memento_pattern/snapshot.dart new file mode 100644 index 0000000..e71d8a3 --- /dev/null +++ b/patterns/memento/memento_editor/memento_pattern/snapshot.dart @@ -0,0 +1 @@ +typedef Snapshot = String; diff --git a/patterns/memento/memento_editor/shapes/active_shape.dart b/patterns/memento/memento_editor/shapes/active_shape.dart new file mode 100644 index 0000000..b13a21d --- /dev/null +++ b/patterns/memento/memento_editor/shapes/active_shape.dart @@ -0,0 +1,56 @@ +part of 'shape.dart'; + +class ActiveShape { + final Shape shape; + final void Function() repaint; + + ActiveShape(this.shape, this._xStart, this._yStart, this.repaint); + + void changeSize(double delta) { + final currentSize = shape.size; + var newSize = currentSize - delta; + + if (newSize == shape.size) { + return; + } + + if (newSize < 10) { + newSize = 10; + } else if (newSize > 200) { + newSize = 200; + } + + shape._size = newSize; + repaint(); + } + + final double _xStart; + final double _yStart; + + void dragTo(double x, double y) { + shape._x = x + _xStart; + shape._y = y + _yStart; + + repaint(); + } + + void changeColor(Color newColor) { + if (shape.color == newColor) { + return; + } + + shape._color = newColor; + repaint(); + } + + void paintSelectionBox(Canvas canvas) { + final x = (shape.x - shape.size).roundToDouble() - 1.5; + final y = (shape.y - shape.size).roundToDouble() - 1.5; + canvas.drawRect( + Rect.fromLTWH(x, y, shape.size * 2 + 3, shape.size * 2 + 3), + Paint() + ..style = PaintingStyle.stroke + ..color = Color(0xff26e6ff), + ); + } +} diff --git a/patterns/memento/memento_editor/shapes/shape.dart b/patterns/memento/memento_editor/shapes/shape.dart new file mode 100644 index 0000000..fa27539 --- /dev/null +++ b/patterns/memento/memento_editor/shapes/shape.dart @@ -0,0 +1,50 @@ +// ignore_for_file: prefer_final_fields + +import 'dart:ui'; + +part 'active_shape.dart'; + +class Shape { + double _x; + + double get x => _x; + + double _y; + + double get y => _y; + + Color _color; + + Color get color => _color; + + double _size; + + double get size => _size; + + Shape( + this._x, + this._y, [ + this._color = const Color(0xFFFFFFFF), + this._size = 60.0, + ]); + + static final _paintStroke = Paint() + ..style = PaintingStyle.stroke + ..color = Color(0xFFD81B60) + ..strokeWidth = 2; + + void paint(Canvas canvas) { + final paintFill = Paint() + ..style = PaintingStyle.fill + ..color = color; + + final offset = Offset(x, y); + canvas.drawCircle(offset, _size, paintFill); + canvas.drawCircle(offset, _size, _paintStroke); + } + + bool isBounded(double x, double y) { + return ((x - this.x) * (x - this.x) + (y - this.y) * (y - this.y) <= + _size * _size); + } +} diff --git a/patterns/memento/memento_editor/shapes/shapes.dart b/patterns/memento/memento_editor/shapes/shapes.dart new file mode 100644 index 0000000..83370b7 --- /dev/null +++ b/patterns/memento/memento_editor/shapes/shapes.dart @@ -0,0 +1,51 @@ +import 'dart:ui'; +import '../../../adapter/flutter_adapter/classic_app/classic_app.dart'; +import 'shape.dart'; + +mixin Shapes implements ClassicApp { + final shapes = []; + + ActiveShape? _activeShape; + + ActiveShape? get activeShape => _activeShape; + + void select(double x, double y) { + final shape = findShape(x, y); + + if (shape != null) { + _activeShape = ActiveShape(shape, shape.x - x, shape.y - y, repaint); + } else { + _activeShape = null; + } + } + + void selectByIndex(int index) { + if (index == -1) { + return; + } + + if (index <= shapes.length - 1) { + _activeShape = ActiveShape(shapes[index], 0, 0, repaint); + } + } + + void unSelect() { + _activeShape = null; + } + + Shape? findShape(double x, double y) { + for (final shape in shapes.reversed) { + if (shape.isBounded(x, y)) { + return shape; + } + } + + return null; + } + + void paintShapes(Canvas canvas) { + for (final shape in shapes) { + shape.paint(canvas); + } + } +} diff --git a/patterns/memento/memento_editor/widgets/composite/colors_widget.dart b/patterns/memento/memento_editor/widgets/composite/colors_widget.dart new file mode 100644 index 0000000..829fff6 --- /dev/null +++ b/patterns/memento/memento_editor/widgets/composite/colors_widget.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; + +class ColorsWidget extends StatelessWidget { + final Color? currentColor; + final List colors; + final void Function(Color color) onColorSelect; + + const ColorsWidget({ + Key? key, + required this.currentColor, + required this.colors, + required this.onColorSelect, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Opacity( + opacity: currentColor == null ? 0.2 : 1.0, + child: Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.black87), + ), + child: Row( + children: colors.map(_buildColorButton).toList(), + ), + ), + ); + } + + Widget _buildColorButton(Color color) { + final isColorSelect = (color == currentColor); + return GestureDetector( + onTap: () { + onColorSelect(color); + }, + child: Container( + width: 20, + height: 20, + color: color, + child: isColorSelect ? _buildSelectColorIcon() : null, + ), + ); + } + + Widget _buildSelectColorIcon() { + return Center( + child: Container( + width: 5, + height: 5, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.8), + borderRadius: BorderRadius.all(Radius.circular(2)), + border: Border.all( + color: Colors.black.withOpacity(0.2), + ), + ), + ), + ); + } +} diff --git a/patterns/memento/memento_editor/widgets/composite/named_panel.dart b/patterns/memento/memento_editor/widgets/composite/named_panel.dart new file mode 100644 index 0000000..a6637d4 --- /dev/null +++ b/patterns/memento/memento_editor/widgets/composite/named_panel.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +class NamedPanel extends StatelessWidget { + final String name; + final List children; + + NamedPanel({ + Key? key, + required this.name, + required this.children, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 15), + ...children, + ], + ); + } +} diff --git a/patterns/memento/memento_editor/widgets/composite/snapshot_list_widget.dart b/patterns/memento/memento_editor/widgets/composite/snapshot_list_widget.dart new file mode 100644 index 0000000..33968f2 --- /dev/null +++ b/patterns/memento/memento_editor/widgets/composite/snapshot_list_widget.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; + +import '../../memento_pattern/memento.dart'; + +class SnapshotListWidget extends StatelessWidget { + final List mementoList; + final void Function(Memento) onMementoRestore; + + const SnapshotListWidget({ + Key? key, + required this.mementoList, + required this.onMementoRestore, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return ColoredBox( + color: Colors.white, + child: Material( + type: MaterialType.transparency, + child: ListView( + padding: EdgeInsets.all(5), + children: mementoList.map((e) => _buildItem('Snapshot', e)).toList(), + ), + ), + ); + } + + Widget _buildItem(String name, Memento memento) { + return Container( + margin: EdgeInsets.only(bottom: 4), + color: Colors.black.withOpacity(0.02), + child: ListTile( + leading: Container( + color: Colors.grey.shade200, + width: 50, + height: double.infinity, + child: Icon(Icons.backup), + ), + title: Text(name ), + subtitle: SingleChildScrollView( + child: Text(memento.time.toIso8601String()), + scrollDirection: Axis.horizontal, + ), + onTap: () { + onMementoRestore(memento); + }, + ), + ); + } +} diff --git a/patterns/memento/memento_editor/widgets/panels/memento_widget.dart b/patterns/memento/memento_editor/widgets/panels/memento_widget.dart new file mode 100644 index 0000000..c61dc67 --- /dev/null +++ b/patterns/memento/memento_editor/widgets/panels/memento_widget.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; + +import '../../../../observer/subscriber_flutter_widget/subscriber/subscriber_widget.dart'; +import '../../application.dart'; +import '../../editor/memento_create_event.dart'; +import '../composite/named_panel.dart'; +import '../composite/snapshot_list_widget.dart'; + +class MementoWidget extends StatelessWidget { + final MementoEditorApplication app; + + const MementoWidget({Key? key, required this.app}) : super(key: key); + + @override + Widget build(BuildContext context) { + return NamedPanel( + name: 'MEMENTO', + children: [ + ..._buildDescription(Theme.of(context).textTheme.bodyMedium!), + SizedBox(height: 20), + _buildSaveStateButton(), + SizedBox(height: 5), + Expanded( + child: SubscriberWidget( + observer: app.editor.events, + builder: (buildContext, event) { + return SnapshotListWidget( + mementoList: app.caretaker.list, + onMementoRestore: app.restoreState, + ); + }, + ), + ), + ], + ); + } + + List _buildDescription(TextStyle style) { + return [ + Text( + '1. Select the shape.', + style: style, + ), + SizedBox(height: 5), + Text( + '2. Change color, size or position.', + style: style, + ), + SizedBox(height: 5), + Text( + '3. Click the "save state" button.', + style: style, + ), + SizedBox(height: 5), + Text( + 'Now you can restore states by selecting them from the list.', + style: style, + ), + ]; + } + + Widget _buildSaveStateButton() { + return Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + OutlinedButton( + child: Text('Save state'), + onPressed: app.saveState, + ), + ], + ); + } +} diff --git a/patterns/memento/memento_editor/widgets/panels/shape_properties_widget.dart b/patterns/memento/memento_editor/widgets/panels/shape_properties_widget.dart new file mode 100644 index 0000000..f923727 --- /dev/null +++ b/patterns/memento/memento_editor/widgets/panels/shape_properties_widget.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; + +import '../../../../adapter/flutter_adapter/classic_app/repaint_event.dart'; +import '../../../../observer/subscriber_flutter_widget/subscriber/subscriber_widget.dart'; +import '../../application.dart'; +import '../composite/colors_widget.dart'; +import '../composite/named_panel.dart'; + +class ShapePropertiesWidget extends StatelessWidget { + final MementoEditorApplication app; + final List colors; + + const ShapePropertiesWidget({ + Key? key, + required this.app, + required this.colors, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SubscriberWidget( + observer: app.editor.events, + builder: (buildContext, event) { + return NamedPanel( + name: 'SHAPE PROPERTIES', + children: [ + Row( + children: [ + _buildNumberField('x:', app.editor.activeShape?.shape.x), + SizedBox(width: 20), + _buildNumberField('y:', app.editor.activeShape?.shape.y), + ], + ), + SizedBox(height: 20), + _buildNumberField( + 'size:', + app.editor.activeShape?.shape.size, + ), + SizedBox(height: 20), + Row( + children: [ + Text( + 'color:', + style: TextStyle( + color: Colors.black.withOpacity( + app.editor.activeShape == null ? 0.5 : 1.0, + ), + ), + ), + SizedBox(width: 10), + ColorsWidget( + currentColor: app.editor.activeShape?.shape.color, + colors: colors, + onColorSelect: (newColor) { + app.editor.activeShape?.changeColor(newColor); + + }, + ), + ], + ), + ], + ); + }, + ); + } + + Widget _buildNumberField(String name, double? value) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + name, + style: TextStyle( + color: Colors.black.withOpacity(value == null ? 0.5 : 1.0), + ), + ), + SizedBox(width: 10), + SizedBox( + width: 60, + child: TextField( + enabled: value != null, + controller: TextEditingController( + text: value == null ? '' : value.toStringAsFixed(0), + ), + decoration: InputDecoration( + filled: value != null, + fillColor: Colors.white, + ), + ), + ), + ], + ); + } +} diff --git a/patterns/memento/memento_editor/widgets/right_panel_widget.dart b/patterns/memento/memento_editor/widgets/right_panel_widget.dart new file mode 100644 index 0000000..6c29bb9 --- /dev/null +++ b/patterns/memento/memento_editor/widgets/right_panel_widget.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; + +import '../application.dart'; +import 'panels/shape_properties_widget.dart'; +import 'panels/memento_widget.dart'; + +class RightPanelWidget extends StatelessWidget { + final MementoEditorApplication app; + + RightPanelWidget({Key? key, required this.app}) : super(key: key); + + final colors = [ + Color(0xFF000000), + Color(0xFFD81B60), + Color(0xFF5E35B1), + Color(0xFF1E88E5), + Color(0xFF43A047), + Color(0xFFFFFFFF), + ]; + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.all(20), + width: 300, + child: Column( + children: [ + ShapePropertiesWidget(app: app, colors: colors), + Container( + margin: EdgeInsets.symmetric(vertical: 20), + height: 2, + color: Colors.black.withOpacity(.2), + ), + Expanded( + child: MementoWidget(app: app), + ), + ], + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 9499ccb..43d8765 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: design_patterns_dart description: Dart examples for all classic GoF design patterns. -version: 0.17.16 +version: 0.18.0 homepage: https://refactoring.guru/design-patterns repository: https://github.com/RefactoringGuru/design-patterns-dart issue_tracker: https://github.com/RefactoringGuru/design-patterns-dart/issue @@ -12,8 +12,10 @@ dependencies: collection: ^1.15.0 flutter: sdk: flutter - + cupertino_icons: ^1.0.2 + dev_dependencies: lints: ^1.0.0 - +flutter: + uses-material-design: true