feat(tools): rnote-style toolbar core writing batch
Some checks failed
CI / Windows build (push) Has been cancelled
Some checks failed
CI / Windows build (push) Has been cancelled
Replace the ad-hoc tool palette with a shared tool system (EditorToolKind) across the PDF, note and slide editors, and add the core writing tools. - Multiple brushes, each remembering its OWN color (rnote-style): selecting a brush restores its color, changing color updates only that brush, and each brush button shows its current color. - Select tool: tap-select a committed stroke, drag to move it, delete it — persisted and undoable. - Shape tool: line / rectangle / ellipse / arrow, drawn with a live preview and committed as generated PenStrokes (shape_geometry.dart) so they reuse stroke rendering, erase, persistence and undo. - Highlighter + eraser fold into the same tool system. Text/bookmark/search+OCR/backgrounds/Windows-Ink are later batches (TODO). Brush opacity still deferred. analyze clean, 302 tests.
This commit is contained in:
@@ -16,12 +16,14 @@ import 'package:path/path.dart' as p;
|
||||
import 'package:syncfusion_flutter_pdf/pdf.dart';
|
||||
|
||||
import '../engine/brush.dart';
|
||||
import '../engine/shape_geometry.dart';
|
||||
import '../input/pen_config.dart';
|
||||
import '../input/pen_input_service.dart';
|
||||
import '../input/pressure_curve.dart' show kNaturalPressureGamma;
|
||||
import '../layout/viewport_fit.dart';
|
||||
import '../pdf/slide_export.dart';
|
||||
import '../ui/pen_settings_page.dart';
|
||||
import 'editor_tool.dart';
|
||||
import 'pen_canvas.dart';
|
||||
import 'pen_palette_widgets.dart';
|
||||
import 'pen_stroke.dart';
|
||||
@@ -52,13 +54,33 @@ class _PenSlideScreenState extends State<PenSlideScreen> {
|
||||
/// keeps the slide's aspect (no distortion). Null until loaded.
|
||||
Map<int, Size>? _slideSizes;
|
||||
|
||||
CanvasTool _tool = CanvasTool.pen;
|
||||
/// The single active-tool state (shared model across the 3 editors).
|
||||
EditorToolKind _tool = EditorToolKind.brush;
|
||||
|
||||
/// Selected brush for the PEN tool. Highlighter tool uses the highlighter
|
||||
/// Selected brush for the BRUSH tool. Highlighter tool uses the highlighter
|
||||
/// brush; local state only (not persisted — TODO(brush-persist-selection)).
|
||||
BrushKind _penBrush = BrushKind.fountainPen;
|
||||
|
||||
Color _color = Colors.black;
|
||||
/// Selected shape for the SHAPE tool.
|
||||
ShapeKind _shapeKind = ShapeKind.line;
|
||||
|
||||
/// Index of the currently selected committed stroke (SELECT tool), or null.
|
||||
int? _selectedStroke;
|
||||
|
||||
/// rnote-style per-brush color memory (see PenNoteScreen). In-memory only.
|
||||
final Map<BrushKind, Color> _brushColors = {
|
||||
BrushKind.fountainPen: Colors.black,
|
||||
BrushKind.ballpoint: Colors.blue,
|
||||
BrushKind.pencil: Colors.green,
|
||||
BrushKind.highlighter: Colors.orange,
|
||||
};
|
||||
|
||||
BrushKind get _activeColorBrush => _tool == EditorToolKind.highlighter
|
||||
? BrushKind.highlighter
|
||||
: _penBrush;
|
||||
|
||||
Color get _color => _brushColors[_activeColorBrush] ?? Colors.black;
|
||||
|
||||
bool _allowFingerDrawing = false;
|
||||
bool _needsCenter = true;
|
||||
bool _showSlider = false;
|
||||
@@ -189,6 +211,7 @@ class _PenSlideScreenState extends State<PenSlideScreen> {
|
||||
setState(() {
|
||||
_slideIndex = clamped;
|
||||
_needsCenter = true;
|
||||
_selectedStroke = null; // selection is per-slide
|
||||
});
|
||||
}
|
||||
|
||||
@@ -280,10 +303,41 @@ class _PenSlideScreenState extends State<PenSlideScreen> {
|
||||
_transform.value = Matrix4.identity()..translateByDouble(o.dx, o.dy, 0, 1);
|
||||
}
|
||||
|
||||
double get _strokeWidth => _tool == CanvasTool.highlighter
|
||||
double get _strokeWidth => _tool == EditorToolKind.highlighter
|
||||
? (_penConfig?.value.highlighterWidth ?? _highlighterWidthFraction)
|
||||
: (_penConfig?.value.penWidth ?? _penWidthFraction);
|
||||
|
||||
// ── SELECT tool: select / move / delete (per-slide, reuses the undo stacks) ──
|
||||
|
||||
void _selectStroke(int? index) {
|
||||
setState(() => _selectedStroke = index);
|
||||
}
|
||||
|
||||
void _moveStroke(int index, double dx, double dy, bool isDragStart) {
|
||||
final strokes = _currentStrokes;
|
||||
if (index < 0 || index >= strokes.length) return;
|
||||
setState(() {
|
||||
if (isDragStart) _pushUndo();
|
||||
final next = List<PenStroke>.from(strokes);
|
||||
next[index] = translateStroke(next[index], dx, dy);
|
||||
_strokesBySlide[_slideIndex] = next;
|
||||
});
|
||||
}
|
||||
|
||||
void _deleteSelected() {
|
||||
final idx = _selectedStroke;
|
||||
final strokes = _currentStrokes;
|
||||
if (idx == null || idx < 0 || idx >= strokes.length) return;
|
||||
setState(() {
|
||||
_pushUndo();
|
||||
_strokesBySlide[_slideIndex] = [
|
||||
...strokes.sublist(0, idx),
|
||||
...strokes.sublist(idx + 1),
|
||||
];
|
||||
_selectedStroke = null;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
@@ -368,10 +422,14 @@ class _PenSlideScreenState extends State<PenSlideScreen> {
|
||||
pageSize: pageSize,
|
||||
strokes: _currentStrokes,
|
||||
transformationController: _transform,
|
||||
tool: _tool,
|
||||
tool: editorToolToCanvas(_tool),
|
||||
brush: _penBrush,
|
||||
shapeKind: _shapeKind,
|
||||
color: _color,
|
||||
strokeWidth: _strokeWidth,
|
||||
selectedStrokeIndex: _selectedStroke,
|
||||
onSelectStroke: _selectStroke,
|
||||
onMoveStroke: _moveStroke,
|
||||
pressureGamma:
|
||||
_penConfig?.value.pressureGamma ?? kNaturalPressureGamma,
|
||||
eraserRadius: _penConfig?.value.eraserRadius ?? kDefaultEraserRadius,
|
||||
@@ -405,26 +463,51 @@ class _PenSlideScreenState extends State<PenSlideScreen> {
|
||||
children: [
|
||||
BrushPickerButton(
|
||||
selected: _penBrush,
|
||||
active: _tool == CanvasTool.pen,
|
||||
active: _tool == EditorToolKind.brush,
|
||||
tooltip: 'Brush',
|
||||
labelFor: brushLabelEn,
|
||||
colorFor: (b) => _brushColors[b] ?? Colors.black,
|
||||
onSelected: (b) => setState(() {
|
||||
_penBrush = b;
|
||||
_tool = CanvasTool.pen;
|
||||
_tool = EditorToolKind.brush;
|
||||
}),
|
||||
),
|
||||
ToolButton(
|
||||
icon: Icons.brush_outlined,
|
||||
selected: _tool == CanvasTool.highlighter,
|
||||
selected: _tool == EditorToolKind.highlighter,
|
||||
tooltip: 'Highlighter',
|
||||
onPressed: () => setState(() => _tool = CanvasTool.highlighter),
|
||||
onPressed: () => setState(() => _tool = EditorToolKind.highlighter),
|
||||
),
|
||||
ToolButton(
|
||||
icon: Icons.cleaning_services_outlined,
|
||||
selected: _tool == CanvasTool.eraser,
|
||||
selected: _tool == EditorToolKind.eraser,
|
||||
tooltip: 'Eraser',
|
||||
onPressed: () => setState(() => _tool = CanvasTool.eraser),
|
||||
onPressed: () => setState(() => _tool = EditorToolKind.eraser),
|
||||
),
|
||||
ToolButton(
|
||||
icon: Icons.ads_click,
|
||||
selected: _tool == EditorToolKind.select,
|
||||
tooltip: 'Select',
|
||||
onPressed: () => setState(() => _tool = EditorToolKind.select),
|
||||
),
|
||||
ShapePickerButton(
|
||||
selected: _shapeKind,
|
||||
active: _tool == EditorToolKind.shape,
|
||||
tooltip: 'Shape',
|
||||
labelFor: shapeLabelEn,
|
||||
onActivate: () => setState(() => _tool = EditorToolKind.shape),
|
||||
onSelected: (s) => setState(() {
|
||||
_shapeKind = s;
|
||||
_tool = EditorToolKind.shape;
|
||||
}),
|
||||
),
|
||||
if (_tool == EditorToolKind.select && _selectedStroke != null)
|
||||
ToolButton(
|
||||
icon: Icons.delete_outline,
|
||||
selected: false,
|
||||
tooltip: 'Delete selection',
|
||||
onPressed: _deleteSelected,
|
||||
),
|
||||
PaletteDivider(cs: cs),
|
||||
ToolButton(
|
||||
icon: Icons.undo,
|
||||
@@ -466,12 +549,16 @@ class _PenSlideScreenState extends State<PenSlideScreen> {
|
||||
}
|
||||
|
||||
Widget _colorDot(Color c, ColorScheme cs) {
|
||||
final selected =
|
||||
_color.toARGB32() == c.toARGB32() && _tool != CanvasTool.eraser;
|
||||
final selected = _color.toARGB32() == c.toARGB32() &&
|
||||
_tool != EditorToolKind.eraser &&
|
||||
_tool != EditorToolKind.select;
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() {
|
||||
_color = c;
|
||||
if (_tool == CanvasTool.eraser) _tool = CanvasTool.pen;
|
||||
if (_tool == EditorToolKind.eraser ||
|
||||
_tool == EditorToolKind.select) {
|
||||
_tool = EditorToolKind.brush;
|
||||
}
|
||||
_brushColors[_activeColorBrush] = c;
|
||||
}),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
|
||||
Reference in New Issue
Block a user