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,11 @@
|
||||
// test/pen_brush_color_memory_test.dart
|
||||
//
|
||||
// Pins rnote-style per-brush color memory in the note editor: each brush
|
||||
// remembers its OWN color, selecting a brush restores that brush's color (the
|
||||
// PenCanvas receives it), and picking a color updates ONLY the active brush's
|
||||
// entry — switching back to a different brush restores the other color.
|
||||
// Pins OneNote-style independent pen slots in the note editor: each slot
|
||||
// remembers its OWN brush + color + width; selecting a slot restores that
|
||||
// slot's color (the PenCanvas receives it), and picking a color updates ONLY
|
||||
// the active slot — switching back to a different slot restores the other color.
|
||||
//
|
||||
// Drives the real PenNoteScreen toolbar (BrushPickerButton popup + color dots)
|
||||
// Drives the real PenNoteScreen toolbar (PenSlotButton + color dots)
|
||||
// and reads PenCanvas.color to assert the active drawing color.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -15,6 +15,8 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:badnote/editor/canvas/pen_canvas.dart';
|
||||
import 'package:badnote/editor/canvas/pen_note_screen.dart';
|
||||
import 'package:badnote/editor/canvas/pen_palette_widgets.dart';
|
||||
import 'package:badnote/editor/engine/brush.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -22,18 +24,17 @@ void main() {
|
||||
Color canvasColor(WidgetTester tester) =>
|
||||
tester.widget<PenCanvas>(find.byType(PenCanvas)).color;
|
||||
|
||||
/// Pick the brush named [label] from the BrushPickerButton popup menu.
|
||||
Future<void> selectBrush(WidgetTester tester, String label) async {
|
||||
// The brush picker carries the 'Brush' tooltip.
|
||||
await tester.tap(find.byTooltip('Brush'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text(label).last);
|
||||
/// Activate the [PenSlotButton] for [kind] via its onPressed (avoids hit-test
|
||||
/// collisions with the floating back button over the left of the palette).
|
||||
Future<void> selectSlot(WidgetTester tester, BrushKind kind) async {
|
||||
final finder = find.byWidgetPredicate(
|
||||
(w) => w is PenSlotButton && w.kind == kind,
|
||||
);
|
||||
tester.widget<PenSlotButton>(finder).onPressed();
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
/// Tap the toolbar color dot whose swatch is exactly [c]. The dot is an
|
||||
/// AnimatedContainer (the swatch) inside a GestureDetector; tap the gesture
|
||||
/// detector ancestor so the onTap fires.
|
||||
/// Tap the toolbar color dot whose swatch is exactly [c].
|
||||
Future<void> tapColorDot(WidgetTester tester, Color c) async {
|
||||
final swatch = find.byWidgetPredicate((w) =>
|
||||
w is AnimatedContainer &&
|
||||
@@ -41,40 +42,49 @@ void main() {
|
||||
(w.decoration as BoxDecoration).color == c &&
|
||||
(w.decoration as BoxDecoration).shape == BoxShape.circle);
|
||||
final gd = find.ancestor(of: swatch, matching: find.byType(GestureDetector));
|
||||
await tester.tap(gd.first);
|
||||
await tester.ensureVisible(gd.first);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(gd.first, warnIfMissed: false);
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
testWidgets('each brush remembers its own color; switching restores it',
|
||||
testWidgets('each pen slot remembers its own color; switching restores it',
|
||||
(tester) async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
// Wide surface so the palette + color dots aren't crushed under chrome.
|
||||
await tester.binding.setSurfaceSize(const Size(1280, 800));
|
||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||||
|
||||
await tester.pumpWidget(const ProviderScope(
|
||||
child: MaterialApp(home: PenNoteScreen()),
|
||||
));
|
||||
await tester.pump(); // let PenConfig load
|
||||
await tester.pump(); // let PenConfig + PenSlots load
|
||||
await tester.pump();
|
||||
|
||||
// Defaults from _brushColors: fountain pen = black, ballpoint = blue.
|
||||
// Defaults: fountain pen = black, ballpoint = blue.
|
||||
expect(canvasColor(tester), Colors.black,
|
||||
reason: 'fountain pen starts black');
|
||||
|
||||
// Switch to the ballpoint brush → its remembered color (blue) becomes active.
|
||||
await selectBrush(tester, 'Ballpoint');
|
||||
// Switch to the ballpoint slot → its remembered color (blue) becomes active.
|
||||
await selectSlot(tester, BrushKind.ballpoint);
|
||||
expect(canvasColor(tester), Colors.blue,
|
||||
reason: 'selecting ballpoint restores ITS remembered color');
|
||||
|
||||
// Change the ACTIVE (ballpoint) brush's color to red via a color dot.
|
||||
await tapColorDot(tester, Colors.red);
|
||||
expect(canvasColor(tester), Colors.red,
|
||||
reason: 'color change applies to the active brush');
|
||||
// Change the ACTIVE (ballpoint) slot's color via a palette color dot.
|
||||
const paletteRed = Color(0xFFC62828);
|
||||
await tapColorDot(tester, paletteRed);
|
||||
await tester.pump();
|
||||
expect(canvasColor(tester), paletteRed,
|
||||
reason: 'color change applies to the active slot');
|
||||
|
||||
// Switch back to the fountain pen → its color is still black (unchanged).
|
||||
await selectBrush(tester, 'Fountain pen');
|
||||
await selectSlot(tester, BrushKind.fountainPen);
|
||||
expect(canvasColor(tester), Colors.black,
|
||||
reason: 'fountain pen color was NOT affected by changing ballpoint');
|
||||
|
||||
// Back to ballpoint → it remembers the red we set.
|
||||
await selectBrush(tester, 'Ballpoint');
|
||||
expect(canvasColor(tester), Colors.red,
|
||||
await selectSlot(tester, BrushKind.ballpoint);
|
||||
expect(canvasColor(tester), paletteRed,
|
||||
reason: 'ballpoint remembers its own updated color');
|
||||
});
|
||||
}
|
||||
|
||||
75
test/pen_slots_test.dart
Normal file
75
test/pen_slots_test.dart
Normal file
@@ -0,0 +1,75 @@
|
||||
// Unit tests for PenSlot / PenSlotsController persistence and independence.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:badnote/editor/engine/brush.dart';
|
||||
import 'package:badnote/editor/input/pen_slots.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
});
|
||||
|
||||
group('PenSlot toJson / fromJson', () {
|
||||
test('round-trips defaults', () {
|
||||
for (final slot in kDefaultPenSlots()) {
|
||||
final restored = PenSlot.fromJson(slot.toJson());
|
||||
expect(restored, slot);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group('PenSlotsController', () {
|
||||
test('seeds three default pens on first load', () async {
|
||||
final c = await PenSlotsController.load();
|
||||
expect(c.slots.length, 3);
|
||||
expect(c.active.brush, BrushKind.fountainPen);
|
||||
expect(c.active.color, Colors.black);
|
||||
expect(c.active.width, 0.006);
|
||||
expect(c.slots[1].brush, BrushKind.ballpoint);
|
||||
expect(c.slots[1].width, 0.0022);
|
||||
expect(c.slots[2].brush, BrushKind.pencil);
|
||||
expect(c.slots[2].width, 0.003);
|
||||
c.dispose();
|
||||
});
|
||||
|
||||
test('select restores brush + color + width as a unit', () async {
|
||||
final c = await PenSlotsController.load();
|
||||
await c.select('slot_1');
|
||||
expect(c.active.brush, BrushKind.ballpoint);
|
||||
expect(c.active.color, Colors.blue);
|
||||
expect(c.active.width, 0.0022);
|
||||
|
||||
await c.setActiveColor(Colors.red);
|
||||
await c.setActiveWidth(0.01);
|
||||
|
||||
await c.select('slot_0');
|
||||
expect(c.active.color, Colors.black);
|
||||
expect(c.active.width, 0.006);
|
||||
|
||||
await c.select('slot_1');
|
||||
expect(c.active.color, Colors.red);
|
||||
expect(c.active.width, 0.01);
|
||||
c.dispose();
|
||||
});
|
||||
|
||||
test('persists across load()', () async {
|
||||
final first = await PenSlotsController.load();
|
||||
await first.select('slot_2');
|
||||
await first.setActiveColor(Colors.purple);
|
||||
await first.setActiveWidth(0.008);
|
||||
first.dispose();
|
||||
|
||||
final second = await PenSlotsController.load();
|
||||
expect(second.activeId, 'slot_2');
|
||||
expect(second.active.brush, BrushKind.pencil);
|
||||
expect(second.active.color.toARGB32(), Colors.purple.toARGB32());
|
||||
expect(second.active.width, 0.008);
|
||||
second.dispose();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -122,6 +122,26 @@ void main() {
|
||||
reopened.dispose();
|
||||
});
|
||||
|
||||
test('schedulePageCountSave persists and restores pageCount', () async {
|
||||
final repo = await SidecarRepository.open(src, debounce: _fast);
|
||||
repo.schedulePageCountSave(5);
|
||||
await repo.flush();
|
||||
expect(repo.sidecar.pageCount, 5);
|
||||
repo.dispose();
|
||||
|
||||
final reopened = await SidecarRepository.open(src, debounce: _fast);
|
||||
expect(reopened.sidecar.pageCount, 5);
|
||||
reopened.dispose();
|
||||
});
|
||||
|
||||
test('notebook open normalizes missing pageCount to 1', () async {
|
||||
final noteSrc = '${tmpDir.path}/notebook';
|
||||
final repo =
|
||||
await SidecarRepository.open(noteSrc, docType: 'notebook', debounce: _fast);
|
||||
expect(repo.sidecar.pageCount, 1);
|
||||
repo.dispose();
|
||||
});
|
||||
|
||||
test('removing a highlight persists (un-highlight)', () async {
|
||||
final repo = await SidecarRepository.open(src, debounce: _fast);
|
||||
repo.scheduleHighlightSave(0, const [
|
||||
|
||||
@@ -237,6 +237,7 @@ void main() {
|
||||
expect(loaded, isNotNull);
|
||||
expect(loaded!.title, 'My Algebra Notes');
|
||||
expect(loaded.docType, 'notebook');
|
||||
expect(loaded.pageCount, 1);
|
||||
// The folder holds only the sidecar (and possibly its .bak/.tmp), never
|
||||
// an importable source file.
|
||||
final files = Directory(expectedFolder)
|
||||
|
||||
Reference in New Issue
Block a user