feat: OneNote-style notebooks, text fonts, and page navigation
All checks were successful
CI / Windows build (push) Successful in 8m42s
All checks were successful
CI / Windows build (push) Successful in 8m42s
Add notebook.json containers with multi-member pages, fix PDF text editing (size/bold/drag/double-tap), index SidecarText in search, and share keyboard page shortcuts plus a PDF scrubber. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import '../../services/office/docx_parser.dart';
|
||||
import '../../services/office/office_document.dart';
|
||||
import '../../services/office/pptx_parser.dart';
|
||||
import '../../theme/app_theme.dart';
|
||||
import '../ui/page_nav_shortcuts.dart';
|
||||
|
||||
/// Unified native Office viewer + ink annotation (PPTX / DOCX).
|
||||
class OfficeDocumentScreen extends StatefulWidget {
|
||||
@@ -172,39 +173,47 @@ class _OfficeDocumentScreenState extends State<OfficeDocumentScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(p.basename(widget.filePath)),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: _pageIndex > 0 ? () => _goPage(_pageIndex - 1) : null,
|
||||
icon: const Icon(Icons.chevron_left),
|
||||
),
|
||||
Center(child: Text('${_pageIndex + 1} / $pageCount')),
|
||||
IconButton(
|
||||
onPressed:
|
||||
_pageIndex < pageCount - 1 ? () => _goPage(_pageIndex + 1) : null,
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: InteractiveViewer(
|
||||
transformationController: _transform,
|
||||
minScale: 0.5,
|
||||
maxScale: 4,
|
||||
child: Listener(
|
||||
onPointerDown: _onPointerDown,
|
||||
onPointerMove: _onPointerMove,
|
||||
onPointerUp: _onPointerUp,
|
||||
child: CustomPaint(
|
||||
painter: _OfficePagePainter(
|
||||
pptx: _pptx,
|
||||
docx: _docx,
|
||||
pageIndex: _pageIndex,
|
||||
strokes: _strokes,
|
||||
live: _live,
|
||||
return pageNavShortcuts(
|
||||
onPrevious: _pageIndex > 0 ? () => _goPage(_pageIndex - 1) : null,
|
||||
onNext:
|
||||
_pageIndex < pageCount - 1 ? () => _goPage(_pageIndex + 1) : null,
|
||||
onFirst: pageCount > 0 ? () => _goPage(0) : null,
|
||||
onLast: pageCount > 0 ? () => _goPage(pageCount - 1) : null,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(p.basename(widget.filePath)),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: _pageIndex > 0 ? () => _goPage(_pageIndex - 1) : null,
|
||||
icon: const Icon(Icons.chevron_left),
|
||||
),
|
||||
Center(child: Text('${_pageIndex + 1} / $pageCount')),
|
||||
IconButton(
|
||||
onPressed: _pageIndex < pageCount - 1
|
||||
? () => _goPage(_pageIndex + 1)
|
||||
: null,
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: InteractiveViewer(
|
||||
transformationController: _transform,
|
||||
minScale: 0.5,
|
||||
maxScale: 4,
|
||||
child: Listener(
|
||||
onPointerDown: _onPointerDown,
|
||||
onPointerMove: _onPointerMove,
|
||||
onPointerUp: _onPointerUp,
|
||||
child: CustomPaint(
|
||||
painter: _OfficePagePainter(
|
||||
pptx: _pptx,
|
||||
docx: _docx,
|
||||
pageIndex: _pageIndex,
|
||||
strokes: _strokes,
|
||||
live: _live,
|
||||
),
|
||||
size: _pageSize,
|
||||
),
|
||||
size: _pageSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -33,6 +33,7 @@ import '../../l10n/app_localizations.dart';
|
||||
import '../../models/bookmark.dart';
|
||||
import '../../models/scratch_link.dart';
|
||||
import '../../storage/badnote_sidecar.dart';
|
||||
import '../../storage/notebook_manifest.dart' show kAnnotationFontFamily;
|
||||
import '../engine/brush.dart';
|
||||
import '../engine/shape_geometry.dart';
|
||||
import '../engine/stroke_eraser.dart';
|
||||
@@ -47,6 +48,7 @@ import '../input/pressure_curve.dart'
|
||||
show PressureCurve, kNaturalPressureFloor;
|
||||
import '../pdf/pen_capture_region.dart';
|
||||
import '../persistence/sidecar_repository.dart';
|
||||
import '../ui/page_nav_shortcuts.dart';
|
||||
import '../ui/pen_settings_page.dart';
|
||||
import '../ui/thumbnail_grid.dart';
|
||||
import 'editor_tool.dart';
|
||||
@@ -66,6 +68,13 @@ const double _kMarkerSize = 36.0;
|
||||
/// page.
|
||||
const double _kDefaultTextFontFraction = 0.03;
|
||||
|
||||
/// Convert a CSS-like numeric weight (100–900) to a [FontWeight], clamped to
|
||||
/// the nearest of the 9 standard weights ([FontWeight.values] is w100..w900).
|
||||
FontWeight _fontWeightFromValue(int weight) {
|
||||
final idx = ((weight ~/ 100) - 1).clamp(0, FontWeight.values.length - 1);
|
||||
return FontWeight.values[idx];
|
||||
}
|
||||
|
||||
class PenEditorScreen extends StatefulWidget {
|
||||
const PenEditorScreen({
|
||||
super.key,
|
||||
@@ -98,6 +107,10 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
/// page pill + thumbnail highlight.
|
||||
int _pageIndex = 0;
|
||||
|
||||
/// Live 1-based value while the page pill's scrubber slider is being
|
||||
/// dragged (like the slide editor's scrubber); null when not scrubbing.
|
||||
double? _pageScrub;
|
||||
|
||||
/// Strokes per page, keyed by 0-based page index (normalized coords).
|
||||
final Map<int, List<PenStroke>> _strokesByPage = {};
|
||||
|
||||
@@ -1079,6 +1092,7 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
text: '',
|
||||
fontSize: _kDefaultTextFontFraction,
|
||||
color: _color.toARGB32(),
|
||||
fontFamily: kAnnotationFontFamily,
|
||||
);
|
||||
setState(() {
|
||||
_textsByPage[pageIndex] = [...?_textsByPage[pageIndex], box];
|
||||
@@ -1134,6 +1148,55 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
_bumpOverlay();
|
||||
}
|
||||
|
||||
/// TEXT tool drag-to-move: nudge box [id] on [pageIndex] by [deltaPx] (a
|
||||
/// screen-pixel pan delta), converting to a normalized delta via the page's
|
||||
/// on-screen [pageW]/[pageH]. Persists (debounced) like other text edits.
|
||||
void _dragTextBox(
|
||||
int pageIndex,
|
||||
String id,
|
||||
Offset deltaPx,
|
||||
double pageW,
|
||||
double pageH,
|
||||
) {
|
||||
if (pageW <= 0 || pageH <= 0) return;
|
||||
final list = _textsByPage[pageIndex];
|
||||
if (list == null) return;
|
||||
final idx = list.indexWhere((t) => t.id == id);
|
||||
if (idx == -1) return;
|
||||
final t = list[idx];
|
||||
final nx = (t.nx + deltaPx.dx / pageW).clamp(0.0, 1.0);
|
||||
final ny = (t.ny + deltaPx.dy / pageH).clamp(0.0, 1.0);
|
||||
setState(() {
|
||||
final next = List<SidecarText>.of(list);
|
||||
next[idx] = t.copyWith(nx: nx, ny: ny);
|
||||
_textsByPage[pageIndex] = next;
|
||||
});
|
||||
_scheduleTextsSave(pageIndex);
|
||||
_bumpOverlay();
|
||||
}
|
||||
|
||||
/// Live style edit for the box currently being edited: updates its
|
||||
/// page-relative [fontSize] fraction and/or numeric [fontWeight] via
|
||||
/// copyWith, persisting (debounced) like [_updateEditingText].
|
||||
void _updateEditingStyle({double? fontSize, int? fontWeight}) {
|
||||
final editing = _editingText;
|
||||
if (editing == null) return;
|
||||
final list = _textsByPage[editing.page];
|
||||
if (list == null) return;
|
||||
final idx = list.indexWhere((t) => t.id == editing.id);
|
||||
if (idx == -1) return;
|
||||
setState(() {
|
||||
final next = List<SidecarText>.of(list);
|
||||
next[idx] = next[idx].copyWith(
|
||||
fontSize: fontSize,
|
||||
fontWeight: fontWeight,
|
||||
);
|
||||
_textsByPage[editing.page] = next;
|
||||
});
|
||||
_scheduleTextsSave(editing.page);
|
||||
_bumpOverlay();
|
||||
}
|
||||
|
||||
/// Toggle the TEXT tool (drops [_editingText] when leaving, so a half-typed
|
||||
/// box gets the empty-on-blur treatment).
|
||||
void _toggleTextMode() {
|
||||
@@ -1529,44 +1592,66 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l = AppLocalizations.of(context);
|
||||
final formatBar = _buildTextFormatBar();
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
Positioned.fill(child: _buildViewer()),
|
||||
// Floating Material You tool palette (top-center).
|
||||
SafeArea(
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: _buildToolPalette(),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Floating page-control pill (bottom-center).
|
||||
if (_viewerReady && _pageCount > 0)
|
||||
body: pageNavShortcuts(
|
||||
onPrevious:
|
||||
_pageIndex > 0 ? () => _goToPage(_pageIndex - 1) : null,
|
||||
onNext: _pageIndex < _pageCount - 1
|
||||
? () => _goToPage(_pageIndex + 1)
|
||||
: null,
|
||||
onFirst: _pageCount > 0 ? () => _goToPage(0) : null,
|
||||
onLast: _pageCount > 0 ? () => _goToPage(_pageCount - 1) : null,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(child: _buildViewer()),
|
||||
// Floating Material You tool palette (top-center).
|
||||
SafeArea(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
alignment: Alignment.topCenter,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: _buildPagePill(),
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: _buildToolPalette(),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Back button (top-left).
|
||||
SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: RoundIconButton(
|
||||
icon: Icons.arrow_back,
|
||||
tooltip: l.back,
|
||||
onPressed: () => Navigator.of(context).maybePop(),
|
||||
// Compact text-format bar (S/M/L + Bold), shown BELOW the tool
|
||||
// palette while a text box is being edited.
|
||||
if (formatBar != null)
|
||||
SafeArea(
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 68),
|
||||
child: formatBar,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Floating page-control pill (bottom-center).
|
||||
if (_viewerReady && _pageCount > 0)
|
||||
SafeArea(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: _buildPagePill(),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Back button (top-left).
|
||||
SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: RoundIconButton(
|
||||
icon: Icons.arrow_back,
|
||||
tooltip: l.back,
|
||||
onPressed: () => Navigator.of(context).maybePop(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_showPenDebug) _buildDebugReadout(context),
|
||||
],
|
||||
if (_showPenDebug) _buildDebugReadout(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1697,7 +1782,20 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
text: t.text,
|
||||
fontSizePx: t.fontSize * pageW,
|
||||
color: Color(t.color),
|
||||
fontWeight: _fontWeightFromValue(t.fontWeight),
|
||||
fontFamily: t.fontFamily,
|
||||
onTap: _textMode ? () => _editTextBox(pageIndex, t.id) : null,
|
||||
// Double-tap always edits, even outside text mode.
|
||||
onDoubleTap: () => _editTextBox(pageIndex, t.id),
|
||||
onPanUpdate: _textMode
|
||||
? (details) => _dragTextBox(
|
||||
pageIndex,
|
||||
t.id,
|
||||
details.delta,
|
||||
pageW,
|
||||
pageH,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
// Active editing field for a box on this page: a real Flutter
|
||||
@@ -1714,6 +1812,8 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
initialText: t.text,
|
||||
fontSizePx: t.fontSize * pageW,
|
||||
color: Color(t.color),
|
||||
fontWeight: _fontWeightFromValue(t.fontWeight),
|
||||
fontFamily: t.fontFamily,
|
||||
hintText: AppLocalizations.of(context).textPlaceholder,
|
||||
onChanged: _updateEditingText,
|
||||
onDone: _finishTextEdit,
|
||||
@@ -1969,6 +2069,64 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Compact text-format bar shown (below the tool palette) while a text box
|
||||
/// is being edited: S/M/L font-size presets + a Bold toggle. Edits apply
|
||||
/// live to the editing [SidecarText] via [_updateEditingStyle]. Returns null
|
||||
/// when nothing is being edited (or the box has since been removed).
|
||||
Widget? _buildTextFormatBar() {
|
||||
final editing = _editingText;
|
||||
if (editing == null) return null;
|
||||
final list = _textsByPage[editing.page];
|
||||
if (list == null) return null;
|
||||
final idx = list.indexWhere((t) => t.id == editing.id);
|
||||
if (idx == -1) return null;
|
||||
final current = list[idx];
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final l = AppLocalizations.of(context);
|
||||
final isBold = current.fontWeight >= 700;
|
||||
|
||||
Widget sizeButton(String label, double fraction) {
|
||||
final selected = (current.fontSize - fraction).abs() < 0.001;
|
||||
return TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
minimumSize: const Size(36, 36),
|
||||
padding: EdgeInsets.zero,
|
||||
backgroundColor:
|
||||
selected ? cs.secondaryContainer : Colors.transparent,
|
||||
foregroundColor:
|
||||
selected ? cs.onSecondaryContainer : cs.onSurfaceVariant,
|
||||
),
|
||||
onPressed: () => _updateEditingStyle(fontSize: fraction),
|
||||
child: Text(label),
|
||||
);
|
||||
}
|
||||
|
||||
return Material(
|
||||
color: cs.surfaceContainerHigh,
|
||||
elevation: 3,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
sizeButton(l.textFontSmall, 0.022),
|
||||
sizeButton(l.textFontMedium, 0.03),
|
||||
sizeButton(l.textFontLarge, 0.045),
|
||||
PaletteDivider(cs: cs),
|
||||
ToolButton(
|
||||
icon: Icons.format_bold,
|
||||
selected: isBold,
|
||||
tooltip: l.textBold,
|
||||
onPressed: () =>
|
||||
_updateEditingStyle(fontWeight: isBold ? 400 : 700),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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.
|
||||
@@ -1992,47 +2150,80 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Floating page control: a COMPACT pill (prev / "n / total" / next).
|
||||
/// Floating page control: a scrubber Slider (multi-page docs) above a
|
||||
/// COMPACT pill (prev / "n / total" / next). Mirrors the slide editor's
|
||||
/// scrubber; the center button now opens the thumbnail grid.
|
||||
Widget _buildPagePill() {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final l = AppLocalizations.of(context);
|
||||
final total = _pageCount;
|
||||
final shown = _pageIndex + 1;
|
||||
return Material(
|
||||
color: cs.surfaceContainerHigh,
|
||||
elevation: 3,
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
tooltip: l.previousPage,
|
||||
icon: const Icon(Icons.chevron_left),
|
||||
onPressed:
|
||||
_pageIndex > 0 ? () => _goToPage(_pageIndex - 1) : null,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: null,
|
||||
child: Text(
|
||||
l.pageOfPages(shown, total),
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
final scrub = _pageScrub;
|
||||
final shown = (scrub ?? (_pageIndex + 1).toDouble()).round();
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (total > 1)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
constraints: const BoxConstraints(maxWidth: 420),
|
||||
child: Material(
|
||||
color: cs.surfaceContainerHigh,
|
||||
elevation: 3,
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Slider(
|
||||
min: 1,
|
||||
max: total.toDouble(),
|
||||
value: (scrub ?? (_pageIndex + 1).toDouble())
|
||||
.clamp(1, total.toDouble()),
|
||||
divisions: total > 1 ? total - 1 : null,
|
||||
onChanged: (v) => setState(() => _pageScrub = v),
|
||||
onChangeEnd: (v) {
|
||||
setState(() => _pageScrub = null);
|
||||
_goToPage(v.round() - 1);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: l.nextPage,
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
onPressed: _pageIndex < total - 1
|
||||
? () => _goToPage(_pageIndex + 1)
|
||||
: null,
|
||||
),
|
||||
Material(
|
||||
color: cs.surfaceContainerHigh,
|
||||
elevation: 3,
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
tooltip: l.previousPage,
|
||||
icon: const Icon(Icons.chevron_left),
|
||||
onPressed:
|
||||
_pageIndex > 0 ? () => _goToPage(_pageIndex - 1) : null,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _viewerReady ? _openThumbnails : null,
|
||||
child: Text(
|
||||
l.pageOfPages(shown, total),
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: l.nextPage,
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
onPressed: _pageIndex < total - 1
|
||||
? () => _goToPage(_pageIndex + 1)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2417,10 +2608,18 @@ class _TextPlacementLayerState extends State<_TextPlacementLayer> {
|
||||
PointerDeviceKind? _downKind;
|
||||
Offset? _downLocal;
|
||||
|
||||
/// A down→up drift beyond this (px) means the gesture was a scroll/pan
|
||||
/// (e.g. a 1-finger drag that also reaches this translucent layer), not a
|
||||
/// deliberate tap-to-place.
|
||||
static const double _kMaxTapDriftPx = 12.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
// translucent: this layer must NOT swallow the touch/scroll gesture from
|
||||
// pdfrx underneath (only opaque'd taps that resolve to a genuine
|
||||
// tap-to-place, guarded by the drift check below, actually place a box).
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTapDown: (d) {
|
||||
_downKind = d.kind;
|
||||
_downLocal = d.localPosition;
|
||||
@@ -2429,6 +2628,11 @@ class _TextPlacementLayerState extends State<_TextPlacementLayer> {
|
||||
// A mouse single-click does NOT place (mouse uses double-click); pen and
|
||||
// touch place on a single tap.
|
||||
if (_downKind == PointerDeviceKind.mouse) return;
|
||||
final down = _downLocal;
|
||||
if (down != null &&
|
||||
(d.localPosition - down).distance > _kMaxTapDriftPx) {
|
||||
return;
|
||||
}
|
||||
widget.onPlace(d.localPosition);
|
||||
},
|
||||
onDoubleTapDown: (d) {
|
||||
@@ -2450,24 +2654,43 @@ class _TextAnnotationLabel extends StatelessWidget {
|
||||
required this.text,
|
||||
required this.fontSizePx,
|
||||
required this.color,
|
||||
this.fontWeight = FontWeight.w400,
|
||||
this.fontFamily,
|
||||
this.onTap,
|
||||
this.onDoubleTap,
|
||||
this.onPanUpdate,
|
||||
});
|
||||
|
||||
final String text;
|
||||
final double fontSizePx;
|
||||
final Color color;
|
||||
final FontWeight fontWeight;
|
||||
final String? fontFamily;
|
||||
|
||||
/// Single-tap handler (only wired while the TEXT tool is active).
|
||||
final VoidCallback? onTap;
|
||||
|
||||
/// Double-tap handler: ALWAYS wired (regardless of active tool) so a box can
|
||||
/// be reopened for editing at any time.
|
||||
final VoidCallback? onDoubleTap;
|
||||
|
||||
/// TEXT tool drag-to-move (only wired while the TEXT tool is active).
|
||||
final GestureDragUpdateCallback? onPanUpdate;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
onDoubleTap: onDoubleTap,
|
||||
onPanUpdate: onPanUpdate,
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontSize: fontSizePx,
|
||||
color: color,
|
||||
fontWeight: fontWeight,
|
||||
fontFamily: fontFamily ?? kAnnotationFontFamily,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
@@ -2489,6 +2712,8 @@ class _TextAnnotationField extends StatefulWidget {
|
||||
required this.hintText,
|
||||
required this.onChanged,
|
||||
required this.onDone,
|
||||
this.fontWeight = FontWeight.w400,
|
||||
this.fontFamily,
|
||||
});
|
||||
|
||||
final String initialText;
|
||||
@@ -2497,6 +2722,8 @@ class _TextAnnotationField extends StatefulWidget {
|
||||
final String hintText;
|
||||
final ValueChanged<String> onChanged;
|
||||
final VoidCallback onDone;
|
||||
final FontWeight fontWeight;
|
||||
final String? fontFamily;
|
||||
|
||||
@override
|
||||
State<_TextAnnotationField> createState() => _TextAnnotationFieldState();
|
||||
@@ -2545,6 +2772,8 @@ class _TextAnnotationFieldState extends State<_TextAnnotationField> {
|
||||
style: TextStyle(
|
||||
fontSize: widget.fontSizePx,
|
||||
color: widget.color,
|
||||
fontWeight: widget.fontWeight,
|
||||
fontFamily: widget.fontFamily ?? kAnnotationFontFamily,
|
||||
height: 1.2,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
|
||||
@@ -22,6 +22,7 @@ import '../input/pen_input_service.dart';
|
||||
import '../input/pressure_curve.dart' show kNaturalPressureGamma;
|
||||
import '../layout/viewport_fit.dart';
|
||||
import '../pdf/slide_export.dart';
|
||||
import '../ui/page_nav_shortcuts.dart';
|
||||
import '../ui/pen_settings_page.dart';
|
||||
import 'editor_tool.dart';
|
||||
import 'pen_canvas.dart';
|
||||
@@ -335,7 +336,15 @@ class _PenSlideScreenState extends State<PenSlideScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
return pageNavShortcuts(
|
||||
onPrevious:
|
||||
_slideIndex > 0 ? () => _goToSlide(_slideIndex - 1) : null,
|
||||
onNext: _slideIndex < _slideCount - 1
|
||||
? () => _goToSlide(_slideIndex + 1)
|
||||
: null,
|
||||
onFirst: _slideCount > 0 ? () => _goToSlide(0) : null,
|
||||
onLast: _slideCount > 0 ? () => _goToSlide(_slideCount - 1) : null,
|
||||
child: Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
Positioned.fill(child: _buildCanvas()),
|
||||
@@ -383,6 +392,7 @@ class _PenSlideScreenState extends State<PenSlideScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user