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:
@@ -31,7 +31,7 @@ enum PenButtonAction {
|
||||
/// Persisted under SharedPreferences key [PenConfigController.prefsKey].
|
||||
class PenConfig {
|
||||
const PenConfig({
|
||||
this.sideButton = PenButtonAction.select,
|
||||
this.sideButton = PenButtonAction.selectText,
|
||||
this.eraserEnd = PenButtonAction.eraser,
|
||||
this.pressureGamma = kNaturalPressureGamma,
|
||||
this.palmRejectionMs = 150.0,
|
||||
|
||||
213
lib/editor/input/pen_slots.dart
Normal file
213
lib/editor/input/pen_slots.dart
Normal file
@@ -0,0 +1,213 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../engine/brush.dart';
|
||||
|
||||
/// OneNote-style independent pen slot: brush + color + thickness together.
|
||||
///
|
||||
/// Selecting a slot restores all three; color dots / thickness controls edit
|
||||
/// only the active slot.
|
||||
class PenSlot {
|
||||
const PenSlot({
|
||||
required this.id,
|
||||
required this.brush,
|
||||
required this.color,
|
||||
required this.width,
|
||||
});
|
||||
|
||||
final String id;
|
||||
|
||||
/// Brush kind for this slot (fountain / ballpoint / pencil — not highlighter).
|
||||
final BrushKind brush;
|
||||
|
||||
final Color color;
|
||||
|
||||
/// Stroke width as a fraction of page width.
|
||||
final double width;
|
||||
|
||||
PenSlot copyWith({
|
||||
String? id,
|
||||
BrushKind? brush,
|
||||
Color? color,
|
||||
double? width,
|
||||
}) {
|
||||
return PenSlot(
|
||||
id: id ?? this.id,
|
||||
brush: brush ?? this.brush,
|
||||
color: color ?? this.color,
|
||||
width: width ?? this.width,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'brush': brush.name,
|
||||
'color': color.toARGB32(),
|
||||
'width': width,
|
||||
};
|
||||
|
||||
factory PenSlot.fromJson(Map<String, dynamic> json) {
|
||||
final brushName = json['brush'] as String? ?? '';
|
||||
return PenSlot(
|
||||
id: json['id'] as String? ?? 'slot_0',
|
||||
brush: BrushKind.values.asNameMap()[brushName] ?? BrushKind.fountainPen,
|
||||
color: Color(json['color'] as int? ?? 0xFF000000),
|
||||
width: (json['width'] as num?)?.toDouble() ?? 0.006,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is PenSlot &&
|
||||
runtimeType == other.runtimeType &&
|
||||
id == other.id &&
|
||||
brush == other.brush &&
|
||||
color.toARGB32() == other.color.toARGB32() &&
|
||||
width == other.width;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(id, brush, color.toARGB32(), width);
|
||||
}
|
||||
|
||||
/// Default pen slots seeded OneNote-style (fountain / ballpoint / pencil).
|
||||
List<PenSlot> kDefaultPenSlots() => const [
|
||||
PenSlot(
|
||||
id: 'slot_0',
|
||||
brush: BrushKind.fountainPen,
|
||||
color: Colors.black,
|
||||
width: 0.006,
|
||||
),
|
||||
PenSlot(
|
||||
id: 'slot_1',
|
||||
brush: BrushKind.ballpoint,
|
||||
color: Colors.blue,
|
||||
width: 0.0022,
|
||||
),
|
||||
PenSlot(
|
||||
id: 'slot_2',
|
||||
brush: BrushKind.pencil,
|
||||
color: Colors.green,
|
||||
width: 0.003,
|
||||
),
|
||||
];
|
||||
|
||||
/// S / M / L thickness presets (page-width fractions) for the toolbar picker.
|
||||
const double kThicknessSmall = 0.0022;
|
||||
const double kThicknessMedium = 0.006;
|
||||
const double kThicknessLarge = 0.012;
|
||||
|
||||
/// Allowed range for slot stroke width (page-width fraction).
|
||||
const double kPenSlotWidthMin = 0.001;
|
||||
const double kPenSlotWidthMax = 0.05;
|
||||
|
||||
/// Manages independent [PenSlot]s with SharedPreferences persistence.
|
||||
///
|
||||
/// Load with [PenSlotsController.load], then listen via [ChangeNotifier].
|
||||
class PenSlotsController extends ChangeNotifier {
|
||||
PenSlotsController._(this._prefs, this._slots, this._activeId);
|
||||
|
||||
/// SharedPreferences key for the slots JSON blob.
|
||||
static const prefsKey = 'pen_slots_v1';
|
||||
|
||||
final SharedPreferences _prefs;
|
||||
List<PenSlot> _slots;
|
||||
String _activeId;
|
||||
|
||||
List<PenSlot> get slots => List.unmodifiable(_slots);
|
||||
|
||||
String get activeId => _activeId;
|
||||
|
||||
PenSlot get active {
|
||||
for (final s in _slots) {
|
||||
if (s.id == _activeId) return s;
|
||||
}
|
||||
return _slots.first;
|
||||
}
|
||||
|
||||
/// Loads persisted slots, or seeds [kDefaultPenSlots] on first run / corrupt
|
||||
/// JSON.
|
||||
static Future<PenSlotsController> load() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final raw = prefs.getString(prefsKey);
|
||||
var slots = kDefaultPenSlots();
|
||||
var activeId = slots.first.id;
|
||||
if (raw != null) {
|
||||
try {
|
||||
final map = jsonDecode(raw) as Map<String, dynamic>;
|
||||
final list = map['slots'] as List<dynamic>?;
|
||||
if (list != null && list.isNotEmpty) {
|
||||
slots = [
|
||||
for (final e in list)
|
||||
PenSlot.fromJson(e as Map<String, dynamic>),
|
||||
];
|
||||
}
|
||||
final storedActive = map['activeId'] as String?;
|
||||
if (storedActive != null &&
|
||||
slots.any((s) => s.id == storedActive)) {
|
||||
activeId = storedActive;
|
||||
} else {
|
||||
activeId = slots.first.id;
|
||||
}
|
||||
} catch (_) {
|
||||
slots = kDefaultPenSlots();
|
||||
activeId = slots.first.id;
|
||||
}
|
||||
}
|
||||
return PenSlotsController._(prefs, slots, activeId);
|
||||
}
|
||||
|
||||
Future<void> _persist() async {
|
||||
await _prefs.setString(
|
||||
prefsKey,
|
||||
jsonEncode({
|
||||
'activeId': _activeId,
|
||||
'slots': [for (final s in _slots) s.toJson()],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
int _indexOfActive() {
|
||||
final i = _slots.indexWhere((s) => s.id == _activeId);
|
||||
return i >= 0 ? i : 0;
|
||||
}
|
||||
|
||||
void _replaceActive(PenSlot next) {
|
||||
final i = _indexOfActive();
|
||||
_slots = [..._slots]..[i] = next;
|
||||
}
|
||||
|
||||
/// Selects [id] as the active slot (restores brush + color + width).
|
||||
Future<void> select(String id) async {
|
||||
if (!_slots.any((s) => s.id == id)) return;
|
||||
if (_activeId == id) return;
|
||||
_activeId = id;
|
||||
notifyListeners();
|
||||
await _persist();
|
||||
}
|
||||
|
||||
/// Sets the active slot's color.
|
||||
Future<void> setActiveColor(Color c) async {
|
||||
_replaceActive(active.copyWith(color: c));
|
||||
notifyListeners();
|
||||
await _persist();
|
||||
}
|
||||
|
||||
/// Sets the active slot's stroke width (clamped).
|
||||
Future<void> setActiveWidth(double w) async {
|
||||
final clamped = w.clamp(kPenSlotWidthMin, kPenSlotWidthMax);
|
||||
_replaceActive(active.copyWith(width: clamped));
|
||||
notifyListeners();
|
||||
await _persist();
|
||||
}
|
||||
|
||||
/// Sets the active slot's brush kind.
|
||||
Future<void> setActiveBrush(BrushKind b) async {
|
||||
if (b == BrushKind.highlighter) return;
|
||||
_replaceActive(active.copyWith(brush: b));
|
||||
notifyListeners();
|
||||
await _persist();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user