From d441dbcd571faad6bd19826fe9873a471ddead2c Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Fri, 13 Dec 2019 22:51:55 +0100 Subject: [PATCH] Add basic parts editing to work editor The work editor contains a ReorderableList view now. This list consists of instances of the new PartTile widget, which only supports editing the parts title and its level (meaning how deep it is down the tree) for now. --- lib/editors/work.dart | 228 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 202 insertions(+), 26 deletions(-) diff --git a/lib/editors/work.dart b/lib/editors/work.dart index 9bdbc7d..24c399d 100644 --- a/lib/editors/work.dart +++ b/lib/editors/work.dart @@ -5,11 +5,14 @@ import '../database.dart'; import '../selectors/instruments.dart'; import '../selectors/person.dart'; -class WorkData { - String title = ''; +class PartData { + final titleController = TextEditingController(); + + int level; Person composer; List instruments = []; - List parts = []; + + PartData(this.level); } class WorkProperties extends StatelessWidget { @@ -83,6 +86,97 @@ class WorkProperties extends StatelessWidget { } } +class PartTile extends StatefulWidget { + final PartData part; + final void Function() onMore; + final void Function() onAdd; + final void Function() onDelete; + final void Function(int levels) onMove; + + PartTile({ + Key key, + @required this.part, + @required this.onMore, + @required this.onAdd, + @required this.onDelete, + @required this.onMove, + }) : super(key: key); + + @override + _PartTileState createState() => _PartTileState(); +} + +class _PartTileState extends State { + static const unit = 16.0; + static const iconShrink = 4.0; + + double dragStart; + double dragDelta = 0.0; + + @override + Widget build(BuildContext context) { + final padding = widget.part.level * unit + dragDelta; + final iconSize = 24 - widget.part.level * iconShrink; + + return GestureDetector( + child: Padding( + padding: EdgeInsets.only(left: padding > 0.0 ? padding : 0.0), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 16.0, right: 8.0), + child: Icon( + Icons.drag_handle, + size: iconSize, + ), + ), + Expanded( + child: TextField( + controller: widget.part.titleController, + decoration: InputDecoration( + border: InputBorder.none, + hintText: 'Part title', + ), + ), + ), + IconButton( + icon: const Icon(Icons.more_horiz), + iconSize: iconSize, + onPressed: widget.onMore, + ), + IconButton( + icon: const Icon(Icons.add), + iconSize: iconSize, + onPressed: widget.onAdd, + ), + IconButton( + icon: const Icon(Icons.delete), + iconSize: iconSize, + onPressed: widget.onDelete, + ), + ], + ), + ), + onHorizontalDragStart: (details) { + dragStart = details.localPosition.dx; + }, + onHorizontalDragUpdate: (details) { + setState(() { + dragDelta = details.localPosition.dx - dragStart; + }); + }, + onHorizontalDragEnd: (details) { + if (dragDelta.abs() >= unit) { + widget.onMove((dragDelta / unit).round()); + } + setState(() { + dragDelta = 0.0; + }); + }, + ); + } +} + class WorkEditor extends StatefulWidget { final Work work; @@ -98,7 +192,11 @@ class _WorkEditorState extends State { final titleController = TextEditingController(); Backend backend; - final WorkData data = WorkData(); + + String title = ''; + Person composer; + List instruments = []; + List parts = []; @override void initState() { @@ -115,6 +213,7 @@ class _WorkEditorState extends State { backend = Backend.of(context); + // TODO: Initialize parts if (widget.work != null) { if (widget.work.composer != null) { () async { @@ -122,9 +221,9 @@ class _WorkEditorState extends State { await backend.db.personById(widget.work.composer).getSingle(); // We don't want to override a newly selected composer. - if (data.composer != null) { + if (composer != null) { setState(() { - data.composer = person; + composer = person; }); } }(); @@ -135,17 +234,63 @@ class _WorkEditorState extends State { await backend.db.instrumentsByWork(widget.work.id).get(); // We don't want to override already selected instruments. - if (data.instruments.isEmpty) { + if (instruments.isEmpty) { setState(() { - data.instruments = selection; + instruments = selection; }); } }(); } } + void cleanLevels() { + var previousLevel = -1; + for (var i = 0; i < parts.length; i++) { + final part = parts[i]; + if (part.level > previousLevel + 1) { + part.level = previousLevel + 1; + } + previousLevel = part.level; + } + } + @override Widget build(BuildContext context) { + final List partTiles = []; + for (var i = 0; i < parts.length; i++) { + final part = parts[i]; + + partTiles.add(PartTile( + key: Key(part.hashCode.toString()), + part: part, + // TODO: Make part details editable + onMore: () {}, + onAdd: () { + setState(() { + parts.insert(i + 1, PartData(part.level + 1)); + }); + }, + onDelete: () { + setState(() { + parts.removeAt(i); + cleanLevels(); + }); + }, + onMove: (levels) { + if (levels > 0 && i > 0 && parts[i - 1].level >= part.level) { + setState(() { + part.level++; + }); + } else if (levels < 0) { + final newLevel = part.level + levels; + setState(() { + part.level = newLevel > 0 ? newLevel : 0; + }); + } + }, + )); + } + return Scaffold( appBar: AppBar( title: Text('Work'), @@ -156,24 +301,55 @@ class _WorkEditorState extends State { ), ], ), - body: ListView( - children: [ - WorkProperties( - titleController: titleController, - composer: data.composer, - instruments: data.instruments, - onComposerChanged: (composer) { - setState(() { - data.composer = composer; - }); - }, - onInstrumentsChanged: (instruments) { - setState(() { - data.instruments = instruments; - }); - }, - ), - ], + body: ReorderableListView( + header: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + WorkProperties( + titleController: titleController, + composer: composer, + instruments: instruments, + onComposerChanged: (newComposer) { + setState(() { + composer = newComposer; + }); + }, + onInstrumentsChanged: (newInstruments) { + setState(() { + instruments = newInstruments; + }); + }, + ), + if (parts.length > 0) + Padding( + padding: const EdgeInsets.only(left: 16.0, top: 16.0), + child: Text( + 'Parts', + style: Theme.of(context).textTheme.subhead, + ), + ), + ], + ), + children: partTiles, + onReorder: (i1, i2) { + setState(() { + final part = parts.removeAt(i1); + final newIndex = i2 > i1 ? i2 - 1 : i2; + + parts.insert(newIndex, part); + + cleanLevels(); + }); + }, + ), + floatingActionButton: FloatingActionButton.extended( + icon: const Icon(Icons.add), + label: Text('Add part'), + onPressed: () { + setState(() { + parts.add(PartData(0)); + }); + }, ), ); }