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:
@@ -1,11 +1,10 @@
|
||||
// lib/editor/canvas/sticky_note_overlay.dart
|
||||
//
|
||||
// Paper-sticky UX for PDF scratch links: a floating card on the viewer that
|
||||
// inks into the SAME SidecarRepository the PDF editor holds. Shares the parent
|
||||
// editor's brush/color/tool so there is one toolbar mental model (OneNote-like).
|
||||
// Page-anchored paper sticky: sized/positioned by the parent in page space,
|
||||
// shares the editor brush/color/tool, locks inner pan/zoom so writing feels
|
||||
// like drawing on the sticky surface itself.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
@@ -24,7 +23,7 @@ import 'pen_stroke.dart';
|
||||
/// Default world size for a fresh sticky scratchpad (absolute px).
|
||||
const Size kStickyWorldSize = Size(1200, 900);
|
||||
|
||||
/// Floating sticky-note card: write → autosave into [repo] under [link.id].
|
||||
/// Floating sticky-note card glued to a PDF page (parent supplies pixel size).
|
||||
class StickyNoteOverlay extends StatefulWidget {
|
||||
const StickyNoteOverlay({
|
||||
super.key,
|
||||
@@ -32,9 +31,12 @@ class StickyNoteOverlay extends StatefulWidget {
|
||||
required this.repo,
|
||||
required this.onClose,
|
||||
required this.onDelete,
|
||||
required this.onDragPx,
|
||||
required this.onResizePx,
|
||||
this.brush = BrushKind.ballpoint,
|
||||
this.color = const Color(0xFF1A1A1A),
|
||||
this.tool = EditorToolKind.brush,
|
||||
this.strokeWidth = 0.008,
|
||||
this.allowFingerDrawing = false,
|
||||
});
|
||||
|
||||
@@ -43,10 +45,16 @@ class StickyNoteOverlay extends StatefulWidget {
|
||||
final VoidCallback onClose;
|
||||
final VoidCallback onDelete;
|
||||
|
||||
/// Shared from the parent PDF toolbar (no mini duplicate palette).
|
||||
/// Header drag delta in viewer/page pixels.
|
||||
final void Function(double dx, double dy) onDragPx;
|
||||
|
||||
/// Corner resize delta in viewer/page pixels.
|
||||
final void Function(double dx, double dy) onResizePx;
|
||||
|
||||
final BrushKind brush;
|
||||
final Color color;
|
||||
final EditorToolKind tool;
|
||||
final double strokeWidth;
|
||||
final bool allowFingerDrawing;
|
||||
|
||||
@override
|
||||
@@ -62,10 +70,6 @@ class _StickyNoteOverlayState extends State<StickyNoteOverlay> {
|
||||
Timer? _saveTimer;
|
||||
bool _dirty = false;
|
||||
|
||||
/// On-screen card size (user-resizable). World canvas stays [_world].
|
||||
double _cardW = 300;
|
||||
double _cardH = 360;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -73,10 +77,6 @@ class _StickyNoteOverlayState extends State<StickyNoteOverlay> {
|
||||
if (pad != null) {
|
||||
_world = Size(pad.canvasWidth, pad.canvasHeight);
|
||||
_strokes = pad.strokes.where((s) => isFreehandTool(s.tool)).toList();
|
||||
// Prefer a card that roughly matches aspect of the world, clamped.
|
||||
final aspect = _world.width / math.max(_world.height, 1);
|
||||
_cardW = (280.0 * aspect).clamp(220.0, 520.0);
|
||||
_cardH = (_cardW / aspect + 40).clamp(260.0, 640.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,15 +84,7 @@ class _StickyNoteOverlayState extends State<StickyNoteOverlay> {
|
||||
void dispose() {
|
||||
_saveTimer?.cancel();
|
||||
if (_dirty) {
|
||||
widget.repo.scheduleScratchpadSave(
|
||||
widget.link.id,
|
||||
SidecarScratchpad(
|
||||
canvasWidth: _world.width,
|
||||
canvasHeight: _world.height,
|
||||
strokes: List<InkStroke>.of(_strokes),
|
||||
),
|
||||
);
|
||||
widget.repo.flush();
|
||||
_persist(flush: true);
|
||||
}
|
||||
_transform.dispose();
|
||||
super.dispose();
|
||||
@@ -101,11 +93,11 @@ class _StickyNoteOverlayState extends State<StickyNoteOverlay> {
|
||||
void _scheduleSave() {
|
||||
_dirty = true;
|
||||
_saveTimer?.cancel();
|
||||
_saveTimer = Timer(const Duration(milliseconds: 600), _saveNow);
|
||||
_saveTimer = Timer(const Duration(milliseconds: 600), () => _persist());
|
||||
}
|
||||
|
||||
Future<void> _saveNow() async {
|
||||
if (!_dirty) return;
|
||||
Future<void> _persist({bool flush = false}) async {
|
||||
if (!_dirty && !flush) return;
|
||||
widget.repo.scheduleScratchpadSave(
|
||||
widget.link.id,
|
||||
SidecarScratchpad(
|
||||
@@ -115,7 +107,7 @@ class _StickyNoteOverlayState extends State<StickyNoteOverlay> {
|
||||
),
|
||||
);
|
||||
_dirty = false;
|
||||
await widget.repo.flush();
|
||||
if (flush) await widget.repo.flush();
|
||||
}
|
||||
|
||||
void _onStrokeComplete(PenStroke pen) {
|
||||
@@ -145,7 +137,7 @@ class _StickyNoteOverlayState extends State<StickyNoteOverlay> {
|
||||
|
||||
Future<void> _close() async {
|
||||
_saveTimer?.cancel();
|
||||
await _saveNow();
|
||||
await _persist(flush: true);
|
||||
widget.onClose();
|
||||
}
|
||||
|
||||
@@ -156,6 +148,7 @@ class _StickyNoteOverlayState extends State<StickyNoteOverlay> {
|
||||
case EditorToolKind.select:
|
||||
return CanvasTool.select;
|
||||
case EditorToolKind.highlighter:
|
||||
return CanvasTool.highlighter;
|
||||
case EditorToolKind.brush:
|
||||
case EditorToolKind.shape:
|
||||
case EditorToolKind.text:
|
||||
@@ -168,13 +161,6 @@ class _StickyNoteOverlayState extends State<StickyNoteOverlay> {
|
||||
? BrushKind.highlighter
|
||||
: widget.brush;
|
||||
|
||||
void _onResizeDrag(DragUpdateDetails d) {
|
||||
setState(() {
|
||||
_cardW = (_cardW + d.delta.dx).clamp(220.0, 640.0);
|
||||
_cardH = (_cardH + d.delta.dy).clamp(240.0, 720.0);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
@@ -182,99 +168,121 @@ class _StickyNoteOverlayState extends State<StickyNoteOverlay> {
|
||||
elevation: 8,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: const Color(0xFFFFF8E1),
|
||||
child: SizedBox(
|
||||
width: _cardW,
|
||||
height: _cardH,
|
||||
child: Stack(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Container(
|
||||
height: 36,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFFFE082),
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(4)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.sticky_note_2, size: 18),
|
||||
const SizedBox(width: 6),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'便利贴',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'用顶栏笔/色',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: cs.onSurface.withValues(alpha: 0.55),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: '删除',
|
||||
icon: const Icon(Icons.delete_outline, size: 18),
|
||||
visualDensity: VisualDensity.compact,
|
||||
onPressed: () async {
|
||||
await _saveNow();
|
||||
widget.onDelete();
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
tooltip: '收起',
|
||||
icon: const Icon(Icons.close, size: 18),
|
||||
visualDensity: VisualDensity.compact,
|
||||
onPressed: _close,
|
||||
),
|
||||
],
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Stack(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
_StickyHeader(
|
||||
onDragDelta: widget.onDragPx,
|
||||
onClose: _close,
|
||||
onDelete: () async {
|
||||
await _persist(flush: true);
|
||||
widget.onDelete();
|
||||
},
|
||||
cs: cs,
|
||||
),
|
||||
Expanded(
|
||||
child: PenCanvas(
|
||||
pageSize: _world,
|
||||
strokes: penStrokesFromInk(_strokes, _world),
|
||||
transformationController: _transform,
|
||||
tool: _canvasTool,
|
||||
brush: _canvasBrush,
|
||||
color: widget.color,
|
||||
strokeWidth: widget.strokeWidth,
|
||||
eraserRadius: kDefaultEraserRadius,
|
||||
allowFingerDrawing: widget.allowFingerDrawing,
|
||||
scaleEnabled: false,
|
||||
panEnabled: false,
|
||||
minScale: 1.0,
|
||||
maxScale: 1.0,
|
||||
onStrokeComplete: _onStrokeComplete,
|
||||
onEraseStroke: _onErase,
|
||||
pageWidget: const ColoredBox(color: Color(0xFFFFFDE7)),
|
||||
),
|
||||
Expanded(
|
||||
child: ClipRect(
|
||||
child: PenCanvas(
|
||||
pageSize: _world,
|
||||
strokes: penStrokesFromInk(_strokes, _world),
|
||||
transformationController: _transform,
|
||||
tool: _canvasTool,
|
||||
brush: _canvasBrush,
|
||||
color: widget.color,
|
||||
strokeWidth: brushProfileFor(_canvasBrush).baseWidthFraction,
|
||||
eraserRadius: kDefaultEraserRadius,
|
||||
allowFingerDrawing: widget.allowFingerDrawing,
|
||||
minScale: 0.2,
|
||||
maxScale: 4.0,
|
||||
onStrokeComplete: _onStrokeComplete,
|
||||
onEraseStroke: _onErase,
|
||||
pageWidget: const ColoredBox(color: Color(0xFFFFFDE7)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: GestureDetector(
|
||||
onPanUpdate: _onResizeDrag,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.resizeUpLeftDownRight,
|
||||
child: SizedBox(
|
||||
width: 28,
|
||||
height: 28,
|
||||
child: Icon(
|
||||
Icons.south_east,
|
||||
size: 16,
|
||||
color: cs.onSurface.withValues(alpha: 0.45),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: GestureDetector(
|
||||
onPanUpdate: (d) => widget.onResizePx(d.delta.dx, d.delta.dy),
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.resizeUpLeftDownRight,
|
||||
child: SizedBox(
|
||||
width: 28,
|
||||
height: 28,
|
||||
child: Icon(
|
||||
Icons.south_east,
|
||||
size: 16,
|
||||
color: cs.onSurface.withValues(alpha: 0.45),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StickyHeader extends StatelessWidget {
|
||||
const _StickyHeader({
|
||||
required this.onDragDelta,
|
||||
required this.onClose,
|
||||
required this.onDelete,
|
||||
required this.cs,
|
||||
});
|
||||
|
||||
final void Function(double dx, double dy) onDragDelta;
|
||||
final VoidCallback onClose;
|
||||
final VoidCallback onDelete;
|
||||
final ColorScheme cs;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onPanUpdate: (d) => onDragDelta(d.delta.dx, d.delta.dy),
|
||||
child: Container(
|
||||
height: 36,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFFFE082),
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(4)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.drag_indicator, size: 18),
|
||||
const SizedBox(width: 4),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'便利贴',
|
||||
style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'拖标题定位 · 角缩放',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: cs.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: '删除',
|
||||
icon: const Icon(Icons.delete_outline, size: 18),
|
||||
visualDensity: VisualDensity.compact,
|
||||
onPressed: onDelete,
|
||||
),
|
||||
IconButton(
|
||||
tooltip: '收起',
|
||||
icon: const Icon(Icons.close, size: 18),
|
||||
visualDensity: VisualDensity.compact,
|
||||
onPressed: onClose,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user