fix: restore PDF pen capture and overhaul sticky/pens/pages
All checks were successful
CI / Windows build (push) Successful in 9m55s
All checks were successful
CI / Windows build (push) Successful in 9m55s
Reinstall PenCaptureBinding so stylus ink hits again; keep finger Listener translucent under pinch; page-anchor sticky with drag/resize; OneNote pen slots (brush+width+color); blank-note multi-page; default side button to hold-select-text. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -48,6 +48,7 @@ import '../engine/undo_stack.dart';
|
||||
import '../input/diagnostic_logger.dart';
|
||||
import '../input/pen_config.dart';
|
||||
import '../input/pen_input_service.dart';
|
||||
import '../input/pen_slots.dart';
|
||||
import '../input/pressure_curve.dart'
|
||||
show PressureCurve, kNaturalPressureFloor;
|
||||
import '../pdf/pen_capture_region.dart';
|
||||
@@ -166,6 +167,9 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
/// Loaded asynchronously in initState; null until ready.
|
||||
PenConfigController? _penConfig;
|
||||
|
||||
/// Independent OneNote-style pen slots (brush + color + width per slot).
|
||||
PenSlotsController? _penSlots;
|
||||
|
||||
// ── Persistence ────────────────────────────────────────────────────────────
|
||||
|
||||
/// Per-file sidecar persistence. Identity is the source file PATH (the
|
||||
@@ -195,7 +199,7 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
static const double _kScaleGlitchHi = 1.18;
|
||||
|
||||
/// A single-frame focal-midpoint jump beyond this is a touch misread → drop.
|
||||
static const double _kFocalGlitchPx = 100.0;
|
||||
static const double _kFocalGlitchPx = 64.0;
|
||||
|
||||
/// Matrix scale captured at the current baseline (gesture start or the last
|
||||
/// pointer-count re-baseline). Null when no pinch is active.
|
||||
@@ -250,11 +254,6 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
// path (they disable pen capture), so they stay as their own booleans.
|
||||
EditorToolKind _tool = EditorToolKind.brush;
|
||||
|
||||
/// Selected brush for the BRUSH tool (fountain/ballpoint/pencil). The
|
||||
/// highlighter tool always uses [BrushKind.highlighter]. Local state only for
|
||||
/// this increment (not persisted — TODO(brush-persist-selection)).
|
||||
BrushKind _penBrush = BrushKind.fountainPen;
|
||||
|
||||
/// Selected shape for the SHAPE tool.
|
||||
ShapeKind _shapeKind = ShapeKind.line;
|
||||
|
||||
@@ -270,22 +269,12 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
Offset? _selectLast;
|
||||
bool _selectDragging = false;
|
||||
|
||||
/// rnote-style per-brush color memory: each brush (and the highlighter)
|
||||
/// remembers its own color. Selecting a brush restores its color; picking a
|
||||
/// color updates ONLY the active brush's entry. In-memory only for this
|
||||
/// increment (TODO(brush-color-persist)).
|
||||
final Map<BrushKind, Color> _brushColors = {
|
||||
BrushKind.fountainPen: Colors.black,
|
||||
BrushKind.ballpoint: Colors.blue,
|
||||
BrushKind.pencil: Colors.green,
|
||||
BrushKind.highlighter: Colors.orange,
|
||||
};
|
||||
/// Highlighter keeps its own color (not a pen slot).
|
||||
Color _highlighterColor = Colors.orange;
|
||||
|
||||
/// The brush whose color the color-dots edit (highlighter tool ⇒ highlighter,
|
||||
/// else the selected pen brush).
|
||||
BrushKind get _activeColorBrush => _tool == EditorToolKind.highlighter
|
||||
? BrushKind.highlighter
|
||||
: _penBrush;
|
||||
/// Active pen brush from the selected slot (fallback until slots load).
|
||||
BrushKind get _penBrush =>
|
||||
_penSlots?.active.brush ?? BrushKind.fountainPen;
|
||||
|
||||
/// When true the "select text" tool button is latched on.
|
||||
bool _selectTextTool = false;
|
||||
@@ -327,16 +316,18 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
|
||||
static const _uuid = Uuid();
|
||||
|
||||
/// The active drawing color = the active brush's remembered color.
|
||||
Color get _color => _brushColors[_activeColorBrush] ?? Colors.black;
|
||||
/// The active drawing color: highlighter tool uses [_highlighterColor], else
|
||||
/// the active pen slot's color.
|
||||
Color get _color => _tool == EditorToolKind.highlighter
|
||||
? _highlighterColor
|
||||
: (_penSlots?.active.color ?? Colors.black);
|
||||
bool _allowFingerDrawing = false;
|
||||
|
||||
/// Whether the viewer currently has a non-empty text selection (drives the
|
||||
/// "highlight selection" action's enabled state).
|
||||
bool _hasSelection = false;
|
||||
|
||||
/// Pen width as a fraction of page width (base; pressure thins it down).
|
||||
static const double _penWidthFraction = 0.006;
|
||||
/// Fallback highlighter width as a fraction of page width.
|
||||
static const double _highlighterWidthFraction = 0.02;
|
||||
|
||||
static const List<Color> _palette = kInkPalette;
|
||||
@@ -348,8 +339,7 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
!_selectTextMode &&
|
||||
!_placeLinkMode &&
|
||||
!_removeHighlightMode &&
|
||||
!_textMode &&
|
||||
_expandedSticky == null;
|
||||
!_textMode;
|
||||
|
||||
/// True when the eraser tool is active.
|
||||
bool get _isEraser => _tool == EditorToolKind.eraser && !_selectTextMode;
|
||||
@@ -373,15 +363,23 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
}
|
||||
|
||||
Future<void> _initPenConfig() async {
|
||||
final controller = await PenConfigController.load();
|
||||
final results = await Future.wait([
|
||||
PenConfigController.load(),
|
||||
PenSlotsController.load(),
|
||||
]);
|
||||
final config = results[0] as PenConfigController;
|
||||
final slots = results[1] as PenSlotsController;
|
||||
if (!mounted) {
|
||||
controller.dispose();
|
||||
config.dispose();
|
||||
slots.dispose();
|
||||
return;
|
||||
}
|
||||
controller.addListener(_onPenConfigChanged);
|
||||
config.addListener(_onPenConfigChanged);
|
||||
slots.addListener(_onPenSlotsChanged);
|
||||
setState(() {
|
||||
_penConfig = controller;
|
||||
_allowFingerDrawing = controller.value.fingerDrawing;
|
||||
_penConfig = config;
|
||||
_penSlots = slots;
|
||||
_allowFingerDrawing = config.value.fingerDrawing;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -390,6 +388,10 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
_syncBarrelSelectText();
|
||||
}
|
||||
|
||||
void _onPenSlotsChanged() {
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
void _onHwPenChanged() {
|
||||
_syncBarrelSelectText();
|
||||
_dispatchHwSideButton();
|
||||
@@ -513,6 +515,8 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
_liveStrokeVN.dispose();
|
||||
_penConfig?.removeListener(_onPenConfigChanged);
|
||||
_penConfig?.dispose();
|
||||
_penSlots?.removeListener(_onPenSlotsChanged);
|
||||
_penSlots?.dispose();
|
||||
PenInputService.instance.removeListener(_onHwPenChanged);
|
||||
PenInputService.instance.stop();
|
||||
DiagnosticLogger.instance.stop();
|
||||
@@ -700,7 +704,11 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
}
|
||||
|
||||
void _onFingerPointer(PointerEvent event) {
|
||||
if (!_allowFingerDrawing || !_penCaptureEnabled) return;
|
||||
// Finger ink is independent of sticky expand; still blocked in text-select modes.
|
||||
if (!_allowFingerDrawing) return;
|
||||
if (_selectTextMode || _placeLinkMode || _removeHighlightMode || _textMode) {
|
||||
return;
|
||||
}
|
||||
if (event.kind != PointerDeviceKind.touch) return;
|
||||
|
||||
if (event is PointerDownEvent) {
|
||||
@@ -995,7 +1003,7 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
|
||||
double _currentStrokeWidth() => _tool == EditorToolKind.highlighter
|
||||
? (_penConfig?.value.highlighterWidth ?? _highlighterWidthFraction)
|
||||
: (_penConfig?.value.penWidth ?? _penWidthFraction);
|
||||
: (_penSlots?.active.width ?? 0.006);
|
||||
|
||||
PenStrokeKind _currentKind() => _tool == EditorToolKind.highlighter
|
||||
? PenStrokeKind.highlighter
|
||||
@@ -1523,6 +1531,16 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
void _updateScratchLink(ScratchLink next) {
|
||||
final i = _scratchLinks.indexWhere((s) => s.id == next.id);
|
||||
if (i < 0) return;
|
||||
setState(() {
|
||||
_scratchLinks[i] = next;
|
||||
if (_expandedSticky?.id == next.id) _expandedSticky = next;
|
||||
});
|
||||
_repo?.scheduleScratchLinkUpsert(next);
|
||||
}
|
||||
|
||||
void _closeSticky() {
|
||||
if (!mounted) return;
|
||||
final open = _expandedSticky;
|
||||
@@ -2099,14 +2117,59 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
// delete. Sized in screen px so the tap target stays usable at any
|
||||
// zoom; positioned at (nx*pageW, ny*pageH).
|
||||
for (final link in linksOnPage)
|
||||
if (_expandedSticky?.id != link.id)
|
||||
Positioned(
|
||||
left: link.nx * pageW - _kMarkerSize / 2,
|
||||
top: link.ny * pageH - _kMarkerSize / 2,
|
||||
width: _kMarkerSize,
|
||||
height: _kMarkerSize,
|
||||
child: _ScratchLinkMarker(
|
||||
onTap: () => _openScratchLink(link),
|
||||
onLongPress: () => _confirmDeleteScratchLink(link),
|
||||
),
|
||||
),
|
||||
// Expanded sticky glued to page (moves/scales with PDF zoom).
|
||||
if (_expandedSticky != null &&
|
||||
_repo != null &&
|
||||
_expandedSticky!.pageIndex == pageIndex)
|
||||
Positioned(
|
||||
left: link.nx * pageW - _kMarkerSize / 2,
|
||||
top: link.ny * pageH - _kMarkerSize / 2,
|
||||
width: _kMarkerSize,
|
||||
height: _kMarkerSize,
|
||||
child: _ScratchLinkMarker(
|
||||
onTap: () => _openScratchLink(link),
|
||||
onLongPress: () => _confirmDeleteScratchLink(link),
|
||||
left: (_expandedSticky!.nx * pageW).clamp(0.0, pageW * 0.85),
|
||||
top: (_expandedSticky!.ny * pageH).clamp(0.0, pageH * 0.85),
|
||||
width: (_expandedSticky!.nw * pageW).clamp(120.0, pageW),
|
||||
height: (_expandedSticky!.nh * pageH).clamp(140.0, pageH),
|
||||
child: StickyNoteOverlay(
|
||||
key: ValueKey(_expandedSticky!.id),
|
||||
link: _expandedSticky!,
|
||||
repo: _repo!,
|
||||
brush: _penBrush,
|
||||
color: _color,
|
||||
tool: _tool,
|
||||
strokeWidth: _penSlots?.active.width ?? 0.006,
|
||||
allowFingerDrawing: _allowFingerDrawing,
|
||||
onClose: _closeSticky,
|
||||
onDelete: () async {
|
||||
final link = _expandedSticky!;
|
||||
_closeSticky();
|
||||
await _confirmDeleteScratchLink(link);
|
||||
},
|
||||
onDragPx: (dx, dy) {
|
||||
final link = _expandedSticky;
|
||||
if (link == null || pageW <= 0 || pageH <= 0) return;
|
||||
final next = link.copyWith(
|
||||
nx: (link.nx + dx / pageW).clamp(0.0, 0.95),
|
||||
ny: (link.ny + dy / pageH).clamp(0.0, 0.95),
|
||||
);
|
||||
_updateScratchLink(next);
|
||||
},
|
||||
onResizePx: (dx, dy) {
|
||||
final link = _expandedSticky;
|
||||
if (link == null || pageW <= 0 || pageH <= 0) return;
|
||||
final next = link.copyWith(
|
||||
nw: (link.nw + dx / pageW).clamp(0.18, 0.95),
|
||||
nh: (link.nh + dy / pageH).clamp(0.18, 0.95),
|
||||
);
|
||||
_updateScratchLink(next);
|
||||
},
|
||||
),
|
||||
),
|
||||
];
|
||||
@@ -2119,10 +2182,21 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
// outside the arena by PenCaptureRegion) is never stolen.
|
||||
viewerOverlayBuilder: (context, size, handleLinkTap) {
|
||||
return [
|
||||
// Finger ink FIRST (under pinch). Always translucent so 2nd finger
|
||||
// still reaches the pinch recognizer above — opaque stole pinch.
|
||||
Positioned.fill(
|
||||
child: Listener(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onPointerDown: _onFingerPointer,
|
||||
onPointerMove: _onFingerPointer,
|
||||
onPointerUp: _onFingerPointer,
|
||||
onPointerCancel: _onFingerPointer,
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
),
|
||||
// Pinch on TOP of finger listener (Stack hit-tests last child first).
|
||||
Positioned.fill(
|
||||
child: RawGestureDetector(
|
||||
// translucent (NOT opaque): the touch must ALSO hit-test pdfrx
|
||||
// underneath so its pan recognizer can win the 1-finger case.
|
||||
behavior: HitTestBehavior.translucent,
|
||||
gestures: {
|
||||
_TwoFingerPinch:
|
||||
@@ -2137,18 +2211,6 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Listener(
|
||||
behavior: _fingerStrokeActive
|
||||
? HitTestBehavior.opaque
|
||||
: HitTestBehavior.translucent,
|
||||
onPointerDown: _onFingerPointer,
|
||||
onPointerMove: _onFingerPointer,
|
||||
onPointerUp: _onFingerPointer,
|
||||
onPointerCancel: _onFingerPointer,
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: PenCaptureRegion(
|
||||
captureEnabled: _penCaptureEnabled,
|
||||
@@ -2166,26 +2228,7 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
onBookmark: _addBookmark,
|
||||
),
|
||||
),
|
||||
if (_expandedSticky != null && _repo != null)
|
||||
Positioned(
|
||||
right: 20,
|
||||
top: 72,
|
||||
child: StickyNoteOverlay(
|
||||
key: ValueKey(_expandedSticky!.id),
|
||||
link: _expandedSticky!,
|
||||
repo: _repo!,
|
||||
brush: _penBrush,
|
||||
color: _color,
|
||||
tool: _tool,
|
||||
allowFingerDrawing: _allowFingerDrawing,
|
||||
onClose: _closeSticky,
|
||||
onDelete: () async {
|
||||
final link = _expandedSticky!;
|
||||
_closeSticky();
|
||||
await _confirmDeleteScratchLink(link);
|
||||
},
|
||||
),
|
||||
),
|
||||
// Sticky is rendered in pageOverlaysBuilder (page-anchored).
|
||||
];
|
||||
},
|
||||
),
|
||||
@@ -2205,20 +2248,25 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// OneNote-style: each pen is its own slot with remembered color.
|
||||
for (final b in kPenToolBrushes)
|
||||
// OneNote-style: each pen slot restores brush + color + thickness.
|
||||
for (final slot in _penSlots?.slots ?? kDefaultPenSlots())
|
||||
PenSlotButton(
|
||||
kind: b,
|
||||
kind: slot.brush,
|
||||
selected: _tool == EditorToolKind.brush &&
|
||||
_penBrush == b &&
|
||||
(_penSlots?.activeId ?? 'slot_0') == slot.id &&
|
||||
_penCaptureEnabled,
|
||||
color: _brushColors[b] ?? Colors.black,
|
||||
tooltip: brushLabel(b, l),
|
||||
color: slot.color,
|
||||
widthHint: slot.width,
|
||||
tooltip: brushLabel(slot.brush, l),
|
||||
onPressed: () {
|
||||
setState(() => _penBrush = b);
|
||||
_penSlots?.select(slot.id);
|
||||
_setTool(EditorToolKind.brush);
|
||||
},
|
||||
),
|
||||
ThicknessPickerButton(
|
||||
width: _penSlots?.active.width ?? 0.006,
|
||||
onChanged: (w) => _penSlots?.setActiveWidth(w),
|
||||
),
|
||||
ToolButton(
|
||||
icon: Icons.brush_outlined,
|
||||
selected: _tool == EditorToolKind.highlighter && _penCaptureEnabled,
|
||||
@@ -2323,6 +2371,10 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
),
|
||||
PaletteDivider(cs: cs),
|
||||
for (final c in _palette) _colorDot(c, cs),
|
||||
ThicknessPickerButton(
|
||||
width: _penSlots?.active.width ?? 0.006,
|
||||
onChanged: (w) => _penSlots?.setActiveWidth(w),
|
||||
),
|
||||
PaletteDivider(cs: cs),
|
||||
ToolButton(
|
||||
icon: _allowFingerDrawing ? Icons.touch_app : Icons.do_not_touch,
|
||||
@@ -2423,11 +2475,17 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
}
|
||||
|
||||
Widget _colorDot(Color c, ColorScheme cs) {
|
||||
// Selected against the ACTIVE brush's remembered color; a tap updates only
|
||||
// that brush's entry (rnote per-brush color memory). Inert in select mode.
|
||||
final selected = _color == c && !_isSelect;
|
||||
// Selected against the ACTIVE slot (or highlighter) color; a tap updates
|
||||
// only that entry. Inert highlight in select mode.
|
||||
final selected = _color.toARGB32() == c.toARGB32() && !_isSelect;
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() => _brushColors[_activeColorBrush] = c),
|
||||
onTap: () {
|
||||
if (_tool == EditorToolKind.highlighter) {
|
||||
setState(() => _highlighterColor = c);
|
||||
} else {
|
||||
_penSlots?.setActiveColor(c);
|
||||
}
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
width: 28,
|
||||
|
||||
Reference in New Issue
Block a user