fix(canvas): persist strokes, pressure, slider
All checks were successful
CI / Windows build (push) Successful in 8m33s
All checks were successful
CI / Windows build (push) Successful in 8m33s
Strokes vanished on pen-up: StaticInkPainter aliased the same mutable list so shouldRepaint saw no change. Commit/erase now replace the list. Finger-drawing toggle wins over palm-rejection; pressure surfaces even when the pen reports no min/max range; pages recenter after a flip; the keyboard page-jump (unreliable on Windows) is now a drag slider. Also register the dynamic_color plugin in generated registrants.
This commit is contained in:
@@ -93,16 +93,15 @@ class _PenCanvasState extends State<PenCanvas> {
|
|||||||
/// Live stroke snapshot handed to the LiveInkPainter; null when idle.
|
/// Live stroke snapshot handed to the LiveInkPainter; null when idle.
|
||||||
PenStroke? _liveStroke;
|
PenStroke? _liveStroke;
|
||||||
|
|
||||||
/// True once any stylus event is seen this session → finger-drawing forced
|
|
||||||
/// off so a resting palm pans instead of marking.
|
|
||||||
bool _stylusSeen = false;
|
|
||||||
|
|
||||||
/// True when the active stylus reports the eraser signal (barrel button or
|
/// True when the active stylus reports the eraser signal (barrel button or
|
||||||
/// inverted stylus), detected on hover/down.
|
/// inverted stylus), detected on hover/down.
|
||||||
bool _eraserActive = false;
|
bool _eraserActive = false;
|
||||||
|
|
||||||
bool get _fingerDrawingEnabled =>
|
// The explicit user toggle wins: if finger-drawing is ON, a single finger
|
||||||
widget.allowFingerDrawing && !_stylusSeen;
|
// draws even after a stylus has been seen. (Palm rejection when the toggle is
|
||||||
|
// OFF is automatic — fingers simply never draw — and a 2nd pointer always
|
||||||
|
// cancels an in-progress stroke regardless.)
|
||||||
|
bool get _fingerDrawingEnabled => widget.allowFingerDrawing;
|
||||||
|
|
||||||
bool _isStylus(PointerDeviceKind kind) =>
|
bool _isStylus(PointerDeviceKind kind) =>
|
||||||
kind == PointerDeviceKind.stylus ||
|
kind == PointerDeviceKind.stylus ||
|
||||||
@@ -112,10 +111,18 @@ class _PenCanvasState extends State<PenCanvas> {
|
|||||||
/// usable pressure range (then perfect_freehand simulates pressure).
|
/// usable pressure range (then perfect_freehand simulates pressure).
|
||||||
double? _normalizedPressure(PointerEvent event) {
|
double? _normalizedPressure(PointerEvent event) {
|
||||||
if (!_isStylus(event.kind)) return null;
|
if (!_isStylus(event.kind)) return null;
|
||||||
if (event.pressureMin == event.pressureMax) return null;
|
|
||||||
final range = event.pressureMax - event.pressureMin;
|
final range = event.pressureMax - event.pressureMin;
|
||||||
|
if (range > 0.0001) {
|
||||||
return ((event.pressure - event.pressureMin) / range).clamp(0.0, 1.0);
|
return ((event.pressure - event.pressureMin) / range).clamp(0.0, 1.0);
|
||||||
}
|
}
|
||||||
|
// No advertised range (some Windows pen stacks): use the raw normalized
|
||||||
|
// pressure directly if it's a usable non-degenerate value, so we still get
|
||||||
|
// real force instead of falling back to velocity simulation.
|
||||||
|
if (event.pressure > 0.0 && event.pressure < 1.0) {
|
||||||
|
return event.pressure;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// The eraser signal: barrel/secondary button held, or an inverted stylus.
|
/// The eraser signal: barrel/secondary button held, or an inverted stylus.
|
||||||
bool _isEraserSignal(PointerEvent event) =>
|
bool _isEraserSignal(PointerEvent event) =>
|
||||||
@@ -244,7 +251,6 @@ class _PenCanvasState extends State<PenCanvas> {
|
|||||||
|
|
||||||
void _onPointerHover(PointerHoverEvent event) {
|
void _onPointerHover(PointerHoverEvent event) {
|
||||||
if (_isStylus(event.kind)) {
|
if (_isStylus(event.kind)) {
|
||||||
_stylusSeen = true;
|
|
||||||
// Detect eraser (barrel button / inverted) while hovering.
|
// Detect eraser (barrel button / inverted) while hovering.
|
||||||
_eraserActive = _isEraserSignal(event);
|
_eraserActive = _isEraserSignal(event);
|
||||||
}
|
}
|
||||||
@@ -252,7 +258,6 @@ class _PenCanvasState extends State<PenCanvas> {
|
|||||||
|
|
||||||
void _onPointerDown(PointerDownEvent event) {
|
void _onPointerDown(PointerDownEvent event) {
|
||||||
if (event.kind == PointerDeviceKind.trackpad) return;
|
if (event.kind == PointerDeviceKind.trackpad) return;
|
||||||
if (_isStylus(event.kind)) _stylusSeen = true;
|
|
||||||
|
|
||||||
_activePointers[event.pointer] = event.kind;
|
_activePointers[event.pointer] = event.kind;
|
||||||
|
|
||||||
|
|||||||
@@ -30,10 +30,16 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
|||||||
/// Strokes per page, keyed by 0-based page index (normalized coords).
|
/// Strokes per page, keyed by 0-based page index (normalized coords).
|
||||||
final Map<int, List<PenStroke>> _strokesByPage = {};
|
final Map<int, List<PenStroke>> _strokesByPage = {};
|
||||||
|
|
||||||
/// One shared transform for the current page; reset on page change so each
|
/// One shared transform for the current page; recentred on page change so
|
||||||
/// page opens fit-to-view.
|
/// each page opens fit-to-view and centered.
|
||||||
final TransformationController _transform = TransformationController();
|
final TransformationController _transform = TransformationController();
|
||||||
|
|
||||||
|
/// Set when the page must be (re)centered on the next layout pass.
|
||||||
|
bool _needsCenter = true;
|
||||||
|
|
||||||
|
/// Live page value while dragging the page slider (null when not dragging).
|
||||||
|
double? _scrub;
|
||||||
|
|
||||||
// Tool state.
|
// Tool state.
|
||||||
CanvasTool _tool = CanvasTool.pen;
|
CanvasTool _tool = CanvasTool.pen;
|
||||||
Color _color = Colors.black;
|
Color _color = Colors.black;
|
||||||
@@ -82,7 +88,13 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
|||||||
|
|
||||||
void _commitStroke(PenStroke stroke) {
|
void _commitStroke(PenStroke stroke) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_strokesByPage.putIfAbsent(_pageIndex, () => <PenStroke>[]).add(stroke);
|
// Replace with a NEW list so StaticInkPainter sees a fresh identity and
|
||||||
|
// actually repaints (mutating in place would alias the old painter's list
|
||||||
|
// and shouldRepaint would see no change → committed strokes vanish).
|
||||||
|
_strokesByPage[_pageIndex] = [
|
||||||
|
...?_strokesByPage[_pageIndex],
|
||||||
|
stroke,
|
||||||
|
];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +102,8 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
final list = _strokesByPage[_pageIndex];
|
final list = _strokesByPage[_pageIndex];
|
||||||
if (list != null && index >= 0 && index < list.length) {
|
if (list != null && index >= 0 && index < list.length) {
|
||||||
list.removeAt(index);
|
final next = List<PenStroke>.of(list)..removeAt(index);
|
||||||
|
_strokesByPage[_pageIndex] = next;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -102,40 +115,15 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
|||||||
if (clamped == _pageIndex) return;
|
if (clamped == _pageIndex) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_pageIndex = clamped;
|
_pageIndex = clamped;
|
||||||
_transform.value = Matrix4.identity();
|
_needsCenter = true; // recenter the new page on next layout
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _promptJumpToPage() async {
|
/// Centre [pageSize] within [viewport] via the shared transform.
|
||||||
final doc = _document;
|
void _centerPage(Size viewport, Size pageSize) {
|
||||||
if (doc == null) return;
|
final tx = (viewport.width - pageSize.width) / 2;
|
||||||
final controller = TextEditingController(text: '${_pageIndex + 1}');
|
final ty = (viewport.height - pageSize.height) / 2;
|
||||||
final result = await showDialog<int>(
|
_transform.value = Matrix4.identity()..setTranslationRaw(tx, ty, 0);
|
||||||
context: context,
|
|
||||||
builder: (context) => AlertDialog(
|
|
||||||
title: const Text('Go to page'),
|
|
||||||
content: TextField(
|
|
||||||
controller: controller,
|
|
||||||
autofocus: true,
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
decoration: InputDecoration(hintText: '1 – ${doc.pages.length}'),
|
|
||||||
onSubmitted: (v) =>
|
|
||||||
Navigator.of(context).pop(int.tryParse(v.trim())),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
child: const Text('Cancel'),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () =>
|
|
||||||
Navigator.of(context).pop(int.tryParse(controller.text.trim())),
|
|
||||||
child: const Text('Go'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (result != null) _goToPage(result - 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -206,8 +194,15 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
|||||||
final scale = fit < fitH ? fit : fitH;
|
final scale = fit < fitH ? fit : fitH;
|
||||||
final pageSize = Size(page.width * scale, page.height * scale);
|
final pageSize = Size(page.width * scale, page.height * scale);
|
||||||
|
|
||||||
return Center(
|
if (_needsCenter) {
|
||||||
child: PenCanvas(
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
_centerPage(
|
||||||
|
Size(constraints.maxWidth, constraints.maxHeight), pageSize);
|
||||||
|
setState(() => _needsCenter = false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return PenCanvas(
|
||||||
key: ValueKey(_pageIndex),
|
key: ValueKey(_pageIndex),
|
||||||
pageSize: pageSize,
|
pageSize: pageSize,
|
||||||
strokes: _currentStrokes,
|
strokes: _currentStrokes,
|
||||||
@@ -229,7 +224,6 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
|||||||
decoration: const BoxDecoration(color: Colors.white),
|
decoration: const BoxDecoration(color: Colors.white),
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -305,14 +299,19 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Floating page-control pill: prev / "n / total" / next.
|
/// Floating page-control pill: prev / drag-slider / next. No keyboard input
|
||||||
|
/// (Windows on-screen keyboard is unreliable) — the slider scrubs pages.
|
||||||
Widget _buildPagePill() {
|
Widget _buildPagePill() {
|
||||||
final doc = _document!;
|
final doc = _document!;
|
||||||
final cs = Theme.of(context).colorScheme;
|
final cs = Theme.of(context).colorScheme;
|
||||||
|
final total = doc.pages.length;
|
||||||
|
final shown = (_scrub ?? (_pageIndex + 1).toDouble()).round();
|
||||||
return Material(
|
return Material(
|
||||||
color: cs.surfaceContainerHigh,
|
color: cs.surfaceContainerHigh,
|
||||||
elevation: 3,
|
elevation: 3,
|
||||||
borderRadius: BorderRadius.circular(28),
|
borderRadius: BorderRadius.circular(28),
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 560),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -321,12 +320,29 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
tooltip: 'Previous page',
|
tooltip: 'Previous page',
|
||||||
icon: const Icon(Icons.chevron_left),
|
icon: const Icon(Icons.chevron_left),
|
||||||
onPressed: _pageIndex > 0 ? () => _goToPage(_pageIndex - 1) : null,
|
onPressed:
|
||||||
|
_pageIndex > 0 ? () => _goToPage(_pageIndex - 1) : null,
|
||||||
),
|
),
|
||||||
TextButton(
|
if (total > 1)
|
||||||
onPressed: _promptJumpToPage,
|
Flexible(
|
||||||
|
child: Slider(
|
||||||
|
min: 1,
|
||||||
|
max: total.toDouble(),
|
||||||
|
value: (_scrub ?? (_pageIndex + 1).toDouble())
|
||||||
|
.clamp(1, total.toDouble()),
|
||||||
|
label: '$shown',
|
||||||
|
divisions: total - 1,
|
||||||
|
onChanged: (v) => setState(() => _scrub = v),
|
||||||
|
onChangeEnd: (v) {
|
||||||
|
setState(() => _scrub = null);
|
||||||
|
_goToPage(v.round() - 1);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||||
child: Text(
|
child: Text(
|
||||||
'${_pageIndex + 1} / ${doc.pages.length}',
|
'$shown / $total',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: cs.onSurface,
|
color: cs.onSurface,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
@@ -336,13 +352,14 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
tooltip: 'Next page',
|
tooltip: 'Next page',
|
||||||
icon: const Icon(Icons.chevron_right),
|
icon: const Icon(Icons.chevron_right),
|
||||||
onPressed: _pageIndex < doc.pages.length - 1
|
onPressed: _pageIndex < total - 1
|
||||||
? () => _goToPage(_pageIndex + 1)
|
? () => _goToPage(_pageIndex + 1)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,15 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <dynamic_color/dynamic_color_plugin.h>
|
||||||
#include <file_selector_linux/file_selector_plugin.h>
|
#include <file_selector_linux/file_selector_plugin.h>
|
||||||
#include <flutter_onnxruntime/flutter_onnxruntime_plugin.h>
|
#include <flutter_onnxruntime/flutter_onnxruntime_plugin.h>
|
||||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
|
g_autoptr(FlPluginRegistrar) dynamic_color_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin");
|
||||||
|
dynamic_color_plugin_register_with_registrar(dynamic_color_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
||||||
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
dynamic_color
|
||||||
file_selector_linux
|
file_selector_linux
|
||||||
flutter_onnxruntime
|
flutter_onnxruntime
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import FlutterMacOS
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
import device_info_plus
|
import device_info_plus
|
||||||
|
import dynamic_color
|
||||||
import file_picker
|
import file_picker
|
||||||
import file_selector_macos
|
import file_selector_macos
|
||||||
import flutter_onnxruntime
|
import flutter_onnxruntime
|
||||||
@@ -16,6 +17,7 @@ import url_launcher_macos
|
|||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||||
|
DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin"))
|
||||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
FlutterOnnxruntimePlugin.register(with: registry.registrar(forPlugin: "FlutterOnnxruntimePlugin"))
|
FlutterOnnxruntimePlugin.register(with: registry.registrar(forPlugin: "FlutterOnnxruntimePlugin"))
|
||||||
|
|||||||
@@ -6,12 +6,15 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <dynamic_color/dynamic_color_plugin_c_api.h>
|
||||||
#include <file_selector_windows/file_selector_windows.h>
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
#include <flutter_onnxruntime/flutter_onnxruntime_plugin.h>
|
#include <flutter_onnxruntime/flutter_onnxruntime_plugin.h>
|
||||||
#include <syncfusion_pdfviewer_windows/syncfusion_pdfviewer_windows_plugin.h>
|
#include <syncfusion_pdfviewer_windows/syncfusion_pdfviewer_windows_plugin.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
|
DynamicColorPluginCApiRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("DynamicColorPluginCApi"));
|
||||||
FileSelectorWindowsRegisterWithRegistrar(
|
FileSelectorWindowsRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||||
FlutterOnnxruntimePluginRegisterWithRegistrar(
|
FlutterOnnxruntimePluginRegisterWithRegistrar(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
dynamic_color
|
||||||
file_selector_windows
|
file_selector_windows
|
||||||
flutter_onnxruntime
|
flutter_onnxruntime
|
||||||
syncfusion_pdfviewer_windows
|
syncfusion_pdfviewer_windows
|
||||||
|
|||||||
Reference in New Issue
Block a user