commit
3ca7b3237d
70 changed files with 4306 additions and 1718 deletions
@ -0,0 +1,15 @@ |
||||
import 'package:flutter/material.dart'; |
||||
|
||||
class QuillDialogTheme { |
||||
QuillDialogTheme( |
||||
{this.labelTextStyle, this.inputTextStyle, this.dialogBackgroundColor}); |
||||
|
||||
///The text style to use for the label shown in the link-input dialog |
||||
final TextStyle? labelTextStyle; |
||||
|
||||
///The text style to use for the input text shown in the link-input dialog |
||||
final TextStyle? inputTextStyle; |
||||
|
||||
///The background color for the [LinkDialog()] |
||||
final Color? dialogBackgroundColor; |
||||
} |
@ -0,0 +1,30 @@ |
||||
import 'package:flutter/material.dart'; |
||||
|
||||
class QuillIconTheme { |
||||
const QuillIconTheme({ |
||||
this.iconSelectedColor, |
||||
this.iconUnselectedColor, |
||||
this.iconSelectedFillColor, |
||||
this.iconUnselectedFillColor, |
||||
this.disabledIconColor, |
||||
this.disabledIconFillColor, |
||||
}); |
||||
|
||||
///The color to use for selected icons in the toolbar |
||||
final Color? iconSelectedColor; |
||||
|
||||
///The color to use for unselected icons in the toolbar |
||||
final Color? iconUnselectedColor; |
||||
|
||||
///The fill color to use for the selected icons in the toolbar |
||||
final Color? iconSelectedFillColor; |
||||
|
||||
///The fill color to use for the unselected icons in the toolbar |
||||
final Color? iconUnselectedFillColor; |
||||
|
||||
///The color to use for disabled icons in the toolbar |
||||
final Color? disabledIconColor; |
||||
|
||||
///The fill color to use for disabled icons in the toolbar |
||||
final Color? disabledIconFillColor; |
||||
} |
@ -0,0 +1,110 @@ |
||||
import 'package:i18n_extension/i18n_extension.dart'; |
||||
|
||||
extension Localization on String { |
||||
static final _t = Translations.byLocale('en') + |
||||
{ |
||||
'en': { |
||||
'Paste a link': 'Paste a link', |
||||
'Ok': 'Ok', |
||||
'Select Color': 'Select Color', |
||||
'Gallery': 'Gallery', |
||||
'Link': 'Link', |
||||
'Please first select some text to transform into a link.': |
||||
'Please first select some text to transform into a link.', |
||||
}, |
||||
'ar': { |
||||
'Paste a link': 'نسخ الرابط', |
||||
'Ok': 'نعم', |
||||
'Select Color': 'اختار اللون', |
||||
'Gallery': 'الصور', |
||||
'Link': 'الرابط', |
||||
'Please first select some text to transform into a link.': |
||||
'يرجى اختيار نص للتحويل إلى رابط', |
||||
}, |
||||
'da': { |
||||
'Paste a link': 'Indsæt link', |
||||
'Ok': 'Ok', |
||||
'Select Color': 'Vælg farve', |
||||
'Gallery': 'Galleri', |
||||
'Link': 'Link', |
||||
'Please first select some text to transform into a link.': |
||||
'Vælg venligst først noget tekst for at lave det om til et link.', |
||||
}, |
||||
'de': { |
||||
'Paste a link': 'Link hinzufügen', |
||||
'Ok': 'Ok', |
||||
'Select Color': 'Farbe auswählen', |
||||
'Gallery': 'Gallerie', |
||||
'Link': 'Link', |
||||
'Please first select some text to transform into a link.': |
||||
'Markiere bitte zuerst einen Text, um diesen in einen Link zu ' |
||||
'verwandeln.', |
||||
}, |
||||
'fr': { |
||||
'Paste a link': 'Coller un lien', |
||||
'Ok': 'Ok', |
||||
'Select Color': 'Choisir une couleur', |
||||
'Gallery': 'Galerie', |
||||
'Link': 'Lien', |
||||
'Please first select some text to transform into a link.': |
||||
"Veuillez d'abord sélectionner un texte à transformer en lien.", |
||||
}, |
||||
'zh_CN': { |
||||
'Paste a link': '粘贴链接', |
||||
'Ok': '好', |
||||
'Select Color': '选择颜色', |
||||
'Gallery': '相簿', |
||||
'Link': '链接', |
||||
'Please first select some text to transform into a link.': |
||||
'请先选择一些要转化为链接的文本', |
||||
}, |
||||
'ko': { |
||||
'Paste a link': '링크를 붙여넣어 주세요.', |
||||
'Ok': '확인', |
||||
'Select Color': '색상 선택', |
||||
'Gallery': '갤러리', |
||||
'Link': '링크', |
||||
'Please first select some text to transform into a link.': |
||||
'링크로 전환할 글자를 먼저 선택해주세요.', |
||||
}, |
||||
'ru': { |
||||
'Paste a link': 'Вставить ссылку', |
||||
'Ok': 'ОК', |
||||
'Select Color': 'Выбрать цвет', |
||||
'Gallery': 'Галерея', |
||||
'Link': 'Ссылка', |
||||
'Please first select some text to transform into a link.': |
||||
'Выделите часть текста для создания ссылки.', |
||||
}, |
||||
'es': { |
||||
'Paste a link': 'Pega un enlace', |
||||
'Ok': 'Ok', |
||||
'Select Color': 'Selecciona un color', |
||||
'Gallery': 'Galeria', |
||||
'Link': 'Enlace', |
||||
'Please first select some text to transform into a link.': |
||||
'Por favor selecciona primero un texto para transformarlo ' |
||||
'en un enlace', |
||||
}, |
||||
'tr': { |
||||
'Paste a link': 'Bağlantıyı Yapıştır', |
||||
'Ok': 'Tamam', |
||||
'Select Color': 'Renk Seçin', |
||||
'Gallery': 'Galeri', |
||||
'Link': 'Bağlantı', |
||||
'Please first select some text to transform into a link.': |
||||
'Lütfen bağlantıya dönüştürmek için bir metin seçin.', |
||||
}, |
||||
'uk': { |
||||
'Paste a link': 'Вставити посилання', |
||||
'Ok': 'ОК', |
||||
'Select Color': 'Вибрати колір', |
||||
'Gallery': 'Галерея', |
||||
'Link': 'Посилання', |
||||
'Please first select some text to transform into a link.': |
||||
'Виділіть текст для створення посилання.', |
||||
}, |
||||
}; |
||||
|
||||
String get i18n => localize(this, _t); |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,31 @@ |
||||
// The corner radius of the floating cursor in pixels. |
||||
import 'dart:ui'; |
||||
|
||||
import '../../widgets/cursor.dart'; |
||||
|
||||
const Radius _kFloatingCaretRadius = Radius.circular(1); |
||||
|
||||
/// Floating painter responsible for painting the floating cursor when |
||||
/// floating mode is activated |
||||
class FloatingCursorPainter { |
||||
FloatingCursorPainter({ |
||||
required this.floatingCursorRect, |
||||
required this.style, |
||||
}); |
||||
|
||||
CursorStyle style; |
||||
|
||||
Rect? floatingCursorRect; |
||||
|
||||
final Paint floatingCursorPaint = Paint(); |
||||
|
||||
void paint(Canvas canvas) { |
||||
final floatingCursorRect = this.floatingCursorRect; |
||||
final floatingCursorColor = style.color.withOpacity(0.75); |
||||
if (floatingCursorRect == null) return; |
||||
canvas.drawRRect( |
||||
RRect.fromRectAndRadius(floatingCursorRect, _kFloatingCaretRadius), |
||||
floatingCursorPaint..color = floatingCursorColor, |
||||
); |
||||
} |
||||
} |
@ -1,106 +1,89 @@ |
||||
import 'package:flutter/foundation.dart'; |
||||
import 'package:flutter/material.dart'; |
||||
import 'package:flutter/services.dart'; |
||||
import 'package:flutter/widgets.dart'; |
||||
|
||||
enum InputShortcut { CUT, COPY, PASTE, SELECT_ALL } |
||||
|
||||
typedef CursorMoveCallback = void Function( |
||||
LogicalKeyboardKey key, bool wordModifier, bool lineModifier, bool shift); |
||||
typedef InputShortcutCallback = void Function(InputShortcut? shortcut); |
||||
typedef OnDeleteCallback = void Function(bool forward); |
||||
|
||||
class KeyboardListener { |
||||
KeyboardListener(this.onCursorMove, this.onShortcut, this.onDelete); |
||||
|
||||
final CursorMoveCallback onCursorMove; |
||||
final InputShortcutCallback onShortcut; |
||||
final OnDeleteCallback onDelete; |
||||
|
||||
static final Set<LogicalKeyboardKey> _moveKeys = <LogicalKeyboardKey>{ |
||||
LogicalKeyboardKey.arrowRight, |
||||
LogicalKeyboardKey.arrowLeft, |
||||
LogicalKeyboardKey.arrowUp, |
||||
LogicalKeyboardKey.arrowDown, |
||||
}; |
||||
|
||||
static final Set<LogicalKeyboardKey> _shortcutKeys = <LogicalKeyboardKey>{ |
||||
LogicalKeyboardKey.keyA, |
||||
LogicalKeyboardKey.keyC, |
||||
LogicalKeyboardKey.keyV, |
||||
LogicalKeyboardKey.keyX, |
||||
LogicalKeyboardKey.delete, |
||||
LogicalKeyboardKey.backspace, |
||||
}; |
||||
|
||||
static final Set<LogicalKeyboardKey> _nonModifierKeys = <LogicalKeyboardKey>{ |
||||
..._shortcutKeys, |
||||
..._moveKeys, |
||||
}; |
||||
|
||||
static final Set<LogicalKeyboardKey> _modifierKeys = <LogicalKeyboardKey>{ |
||||
LogicalKeyboardKey.shift, |
||||
LogicalKeyboardKey.control, |
||||
LogicalKeyboardKey.alt, |
||||
}; |
||||
|
||||
static final Set<LogicalKeyboardKey> _macOsModifierKeys = |
||||
<LogicalKeyboardKey>{ |
||||
LogicalKeyboardKey.shift, |
||||
LogicalKeyboardKey.meta, |
||||
LogicalKeyboardKey.alt, |
||||
}; |
||||
|
||||
static final Set<LogicalKeyboardKey> _interestingKeys = <LogicalKeyboardKey>{ |
||||
..._modifierKeys, |
||||
..._macOsModifierKeys, |
||||
..._nonModifierKeys, |
||||
}; |
||||
|
||||
static final Map<LogicalKeyboardKey, InputShortcut> _keyToShortcut = { |
||||
LogicalKeyboardKey.keyX: InputShortcut.CUT, |
||||
LogicalKeyboardKey.keyC: InputShortcut.COPY, |
||||
LogicalKeyboardKey.keyV: InputShortcut.PASTE, |
||||
LogicalKeyboardKey.keyA: InputShortcut.SELECT_ALL, |
||||
}; |
||||
|
||||
KeyEventResult handleRawKeyEvent(RawKeyEvent event) { |
||||
if (kIsWeb) { |
||||
// On web platform, we ignore the key because it's already processed. |
||||
return KeyEventResult.ignored; |
||||
} |
||||
|
||||
if (event is! RawKeyDownEvent) { |
||||
return KeyEventResult.ignored; |
||||
} |
||||
class QuillPressedKeys extends ChangeNotifier { |
||||
static QuillPressedKeys of(BuildContext context) { |
||||
final widget = |
||||
context.dependOnInheritedWidgetOfExactType<_QuillPressedKeysAccess>(); |
||||
return widget!.pressedKeys; |
||||
} |
||||
|
||||
final keysPressed = |
||||
LogicalKeyboardKey.collapseSynonyms(RawKeyboard.instance.keysPressed); |
||||
final key = event.logicalKey; |
||||
final isMacOS = event.data is RawKeyEventDataMacOs; |
||||
if (!_nonModifierKeys.contains(key) || |
||||
keysPressed |
||||
.difference(isMacOS ? _macOsModifierKeys : _modifierKeys) |
||||
.length > |
||||
1 || |
||||
keysPressed.difference(_interestingKeys).isNotEmpty) { |
||||
return KeyEventResult.ignored; |
||||
} |
||||
bool _metaPressed = false; |
||||
bool _controlPressed = false; |
||||
|
||||
if (_moveKeys.contains(key)) { |
||||
onCursorMove( |
||||
key, |
||||
isMacOS ? event.isAltPressed : event.isControlPressed, |
||||
isMacOS ? event.isMetaPressed : event.isAltPressed, |
||||
event.isShiftPressed); |
||||
} else if (isMacOS |
||||
? event.isMetaPressed |
||||
: event.isControlPressed && _shortcutKeys.contains(key)) { |
||||
onShortcut(_keyToShortcut[key]); |
||||
} else if (key == LogicalKeyboardKey.delete) { |
||||
onDelete(true); |
||||
} else if (key == LogicalKeyboardKey.backspace) { |
||||
onDelete(false); |
||||
/// Whether meta key is currently pressed. |
||||
bool get metaPressed => _metaPressed; |
||||
|
||||
/// Whether control key is currently pressed. |
||||
bool get controlPressed => _controlPressed; |
||||
|
||||
void _updatePressedKeys(Set<LogicalKeyboardKey> pressedKeys) { |
||||
final meta = pressedKeys.contains(LogicalKeyboardKey.metaLeft) || |
||||
pressedKeys.contains(LogicalKeyboardKey.metaRight); |
||||
final control = pressedKeys.contains(LogicalKeyboardKey.controlLeft) || |
||||
pressedKeys.contains(LogicalKeyboardKey.controlRight); |
||||
if (_metaPressed != meta || _controlPressed != control) { |
||||
_metaPressed = meta; |
||||
_controlPressed = control; |
||||
notifyListeners(); |
||||
} |
||||
return KeyEventResult.ignored; |
||||
} |
||||
} |
||||
|
||||
class QuillKeyboardListener extends StatefulWidget { |
||||
const QuillKeyboardListener({required this.child, Key? key}) |
||||
: super(key: key); |
||||
|
||||
final Widget child; |
||||
|
||||
@override |
||||
QuillKeyboardListenerState createState() => QuillKeyboardListenerState(); |
||||
} |
||||
|
||||
class QuillKeyboardListenerState extends State<QuillKeyboardListener> { |
||||
final QuillPressedKeys _pressedKeys = QuillPressedKeys(); |
||||
|
||||
bool _keyEvent(KeyEvent event) { |
||||
_pressedKeys |
||||
._updatePressedKeys(HardwareKeyboard.instance.logicalKeysPressed); |
||||
return false; |
||||
} |
||||
|
||||
@override |
||||
void initState() { |
||||
super.initState(); |
||||
HardwareKeyboard.instance.addHandler(_keyEvent); |
||||
_pressedKeys |
||||
._updatePressedKeys(HardwareKeyboard.instance.logicalKeysPressed); |
||||
} |
||||
|
||||
@override |
||||
void dispose() { |
||||
HardwareKeyboard.instance.removeHandler(_keyEvent); |
||||
_pressedKeys.dispose(); |
||||
super.dispose(); |
||||
} |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
return _QuillPressedKeysAccess( |
||||
pressedKeys: _pressedKeys, |
||||
child: widget.child, |
||||
); |
||||
} |
||||
} |
||||
|
||||
class _QuillPressedKeysAccess extends InheritedWidget { |
||||
const _QuillPressedKeysAccess({ |
||||
required this.pressedKeys, |
||||
required Widget child, |
||||
Key? key, |
||||
}) : super(key: key, child: child); |
||||
|
||||
final QuillPressedKeys pressedKeys; |
||||
|
||||
@override |
||||
bool updateShouldNotify(covariant _QuillPressedKeysAccess oldWidget) { |
||||
return oldWidget.pressedKeys != pressedKeys; |
||||
} |
||||
} |
||||
|
@ -0,0 +1,170 @@ |
||||
import 'package:flutter/cupertino.dart'; |
||||
import 'package:flutter/foundation.dart'; |
||||
import 'package:flutter/material.dart'; |
||||
|
||||
import '../../models/documents/nodes/node.dart'; |
||||
|
||||
/// List of possible actions returned from [LinkActionPickerDelegate]. |
||||
enum LinkMenuAction { |
||||
/// Launch the link |
||||
launch, |
||||
|
||||
/// Copy to clipboard |
||||
copy, |
||||
|
||||
/// Remove link style attribute |
||||
remove, |
||||
|
||||
/// No-op |
||||
none, |
||||
} |
||||
|
||||
/// Used internally by widget layer. |
||||
typedef LinkActionPicker = Future<LinkMenuAction> Function(Node linkNode); |
||||
|
||||
typedef LinkActionPickerDelegate = Future<LinkMenuAction> Function( |
||||
BuildContext context, String link); |
||||
|
||||
Future<LinkMenuAction> defaultLinkActionPickerDelegate( |
||||
BuildContext context, String link) async { |
||||
switch (defaultTargetPlatform) { |
||||
case TargetPlatform.iOS: |
||||
return _showCupertinoLinkMenu(context, link); |
||||
case TargetPlatform.android: |
||||
return _showMaterialMenu(context, link); |
||||
default: |
||||
assert( |
||||
false, |
||||
'defaultShowLinkActionsMenu not supposed to ' |
||||
'be invoked for $defaultTargetPlatform'); |
||||
return LinkMenuAction.none; |
||||
} |
||||
} |
||||
|
||||
Future<LinkMenuAction> _showCupertinoLinkMenu( |
||||
BuildContext context, String link) async { |
||||
final result = await showCupertinoModalPopup<LinkMenuAction>( |
||||
context: context, |
||||
builder: (ctx) { |
||||
return CupertinoActionSheet( |
||||
title: Text(link), |
||||
actions: [ |
||||
_CupertinoAction( |
||||
title: 'Open', |
||||
icon: Icons.language_sharp, |
||||
onPressed: () => Navigator.of(context).pop(LinkMenuAction.launch), |
||||
), |
||||
_CupertinoAction( |
||||
title: 'Copy', |
||||
icon: Icons.copy_sharp, |
||||
onPressed: () => Navigator.of(context).pop(LinkMenuAction.copy), |
||||
), |
||||
_CupertinoAction( |
||||
title: 'Remove', |
||||
icon: Icons.link_off_sharp, |
||||
onPressed: () => Navigator.of(context).pop(LinkMenuAction.remove), |
||||
), |
||||
], |
||||
); |
||||
}, |
||||
); |
||||
return result ?? LinkMenuAction.none; |
||||
} |
||||
|
||||
class _CupertinoAction extends StatelessWidget { |
||||
const _CupertinoAction({ |
||||
required this.title, |
||||
required this.icon, |
||||
required this.onPressed, |
||||
Key? key, |
||||
}) : super(key: key); |
||||
|
||||
final String title; |
||||
final IconData icon; |
||||
final VoidCallback onPressed; |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
final theme = Theme.of(context); |
||||
return CupertinoActionSheetAction( |
||||
onPressed: onPressed, |
||||
child: Padding( |
||||
padding: const EdgeInsets.symmetric(horizontal: 8), |
||||
child: Row( |
||||
children: [ |
||||
Expanded( |
||||
child: Text( |
||||
title, |
||||
textAlign: TextAlign.start, |
||||
style: TextStyle(color: theme.colorScheme.onSurface), |
||||
), |
||||
), |
||||
Icon( |
||||
icon, |
||||
size: theme.iconTheme.size, |
||||
color: theme.colorScheme.onSurface.withOpacity(0.75), |
||||
) |
||||
], |
||||
), |
||||
), |
||||
); |
||||
} |
||||
} |
||||
|
||||
Future<LinkMenuAction> _showMaterialMenu( |
||||
BuildContext context, String link) async { |
||||
final result = await showModalBottomSheet<LinkMenuAction>( |
||||
context: context, |
||||
builder: (ctx) { |
||||
return Column( |
||||
mainAxisSize: MainAxisSize.min, |
||||
children: [ |
||||
_MaterialAction( |
||||
title: 'Open', |
||||
icon: Icons.language_sharp, |
||||
onPressed: () => Navigator.of(context).pop(LinkMenuAction.launch), |
||||
), |
||||
_MaterialAction( |
||||
title: 'Copy', |
||||
icon: Icons.copy_sharp, |
||||
onPressed: () => Navigator.of(context).pop(LinkMenuAction.copy), |
||||
), |
||||
_MaterialAction( |
||||
title: 'Remove', |
||||
icon: Icons.link_off_sharp, |
||||
onPressed: () => Navigator.of(context).pop(LinkMenuAction.remove), |
||||
), |
||||
], |
||||
); |
||||
}, |
||||
); |
||||
|
||||
return result ?? LinkMenuAction.none; |
||||
} |
||||
|
||||
class _MaterialAction extends StatelessWidget { |
||||
const _MaterialAction({ |
||||
required this.title, |
||||
required this.icon, |
||||
required this.onPressed, |
||||
Key? key, |
||||
}) : super(key: key); |
||||
|
||||
final String title; |
||||
final IconData icon; |
||||
final VoidCallback onPressed; |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
final theme = Theme.of(context); |
||||
return ListTile( |
||||
leading: Icon( |
||||
icon, |
||||
size: theme.iconTheme.size, |
||||
color: theme.colorScheme.onSurface.withOpacity(0.75), |
||||
), |
||||
title: Text(title), |
||||
onTap: onPressed, |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,354 @@ |
||||
import 'dart:math' as math; |
||||
|
||||
import 'package:flutter/material.dart'; |
||||
import 'package:flutter/rendering.dart'; |
||||
import 'package:flutter/widgets.dart'; |
||||
|
||||
/// Very similar to [SingleChildView] but with a [ViewportBuilder] argument |
||||
/// instead of a [Widget] |
||||
/// |
||||
/// Useful when child needs [ViewportOffset] (e.g. [RenderEditor]) |
||||
/// see: [SingleChildScrollView] |
||||
class QuillSingleChildScrollView extends StatelessWidget { |
||||
/// Creates a box in which a single widget can be scrolled. |
||||
const QuillSingleChildScrollView({ |
||||
required this.controller, |
||||
required this.viewportBuilder, |
||||
Key? key, |
||||
this.physics, |
||||
this.restorationId, |
||||
}) : super(key: key); |
||||
|
||||
/// An object that can be used to control the position to which this scroll |
||||
/// view is scrolled. |
||||
/// |
||||
/// Must be null if [primary] is true. |
||||
/// |
||||
/// A [ScrollController] serves several purposes. It can be used to control |
||||
/// the initial scroll position (see [ScrollController.initialScrollOffset]). |
||||
/// It can be used to control whether the scroll view should automatically |
||||
/// save and restore its scroll position in the [PageStorage] (see |
||||
/// [ScrollController.keepScrollOffset]). It can be used to read the current |
||||
/// scroll position (see [ScrollController.offset]), or change it (see |
||||
/// [ScrollController.animateTo]). |
||||
final ScrollController controller; |
||||
|
||||
/// How the scroll view should respond to user input. |
||||
/// |
||||
/// For example, determines how the scroll view continues to animate after the |
||||
/// user stops dragging the scroll view. |
||||
/// |
||||
/// Defaults to matching platform conventions. |
||||
final ScrollPhysics? physics; |
||||
|
||||
/// {@macro flutter.widgets.scrollable.restorationId} |
||||
final String? restorationId; |
||||
|
||||
final ViewportBuilder viewportBuilder; |
||||
|
||||
AxisDirection _getDirection(BuildContext context) { |
||||
return getAxisDirectionFromAxisReverseAndDirectionality( |
||||
context, Axis.vertical, false); |
||||
} |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
final axisDirection = _getDirection(context); |
||||
final scrollController = controller; |
||||
final scrollable = Scrollable( |
||||
axisDirection: axisDirection, |
||||
controller: scrollController, |
||||
physics: physics, |
||||
restorationId: restorationId, |
||||
viewportBuilder: (context, offset) { |
||||
return _SingleChildViewport( |
||||
offset: offset, |
||||
child: viewportBuilder(context, offset), |
||||
); |
||||
}, |
||||
); |
||||
return scrollable; |
||||
} |
||||
} |
||||
|
||||
class _SingleChildViewport extends SingleChildRenderObjectWidget { |
||||
const _SingleChildViewport({ |
||||
required this.offset, |
||||
Key? key, |
||||
Widget? child, |
||||
}) : super(key: key, child: child); |
||||
|
||||
final ViewportOffset offset; |
||||
|
||||
@override |
||||
_RenderSingleChildViewport createRenderObject(BuildContext context) { |
||||
return _RenderSingleChildViewport( |
||||
offset: offset, |
||||
); |
||||
} |
||||
|
||||
@override |
||||
void updateRenderObject( |
||||
BuildContext context, _RenderSingleChildViewport renderObject) { |
||||
// Order dependency: The offset setter reads the axis direction. |
||||
renderObject.offset = offset; |
||||
} |
||||
} |
||||
|
||||
class _RenderSingleChildViewport extends RenderBox |
||||
with RenderObjectWithChildMixin<RenderBox> |
||||
implements RenderAbstractViewport { |
||||
_RenderSingleChildViewport({ |
||||
required ViewportOffset offset, |
||||
double cacheExtent = RenderAbstractViewport.defaultCacheExtent, |
||||
RenderBox? child, |
||||
}) : _offset = offset, |
||||
_cacheExtent = cacheExtent { |
||||
this.child = child; |
||||
} |
||||
|
||||
ViewportOffset get offset => _offset; |
||||
ViewportOffset _offset; |
||||
|
||||
set offset(ViewportOffset value) { |
||||
if (value == _offset) return; |
||||
if (attached) _offset.removeListener(_hasScrolled); |
||||
_offset = value; |
||||
if (attached) _offset.addListener(_hasScrolled); |
||||
markNeedsLayout(); |
||||
} |
||||
|
||||
/// {@macro flutter.rendering.RenderViewportBase.cacheExtent} |
||||
double get cacheExtent => _cacheExtent; |
||||
double _cacheExtent; |
||||
|
||||
set cacheExtent(double value) { |
||||
if (value == _cacheExtent) return; |
||||
_cacheExtent = value; |
||||
markNeedsLayout(); |
||||
} |
||||
|
||||
void _hasScrolled() { |
||||
markNeedsPaint(); |
||||
markNeedsSemanticsUpdate(); |
||||
} |
||||
|
||||
@override |
||||
void setupParentData(RenderObject child) { |
||||
// We don't actually use the offset argument in BoxParentData, so let's |
||||
// avoid allocating it at all. |
||||
if (child.parentData is! ParentData) child.parentData = ParentData(); |
||||
} |
||||
|
||||
@override |
||||
bool get isRepaintBoundary => true; |
||||
|
||||
double get _viewportExtent { |
||||
assert(hasSize); |
||||
return size.height; |
||||
} |
||||
|
||||
double get _minScrollExtent { |
||||
assert(hasSize); |
||||
return 0; |
||||
} |
||||
|
||||
double get _maxScrollExtent { |
||||
assert(hasSize); |
||||
if (child == null) return 0; |
||||
return math.max(0, child!.size.height - size.height); |
||||
} |
||||
|
||||
BoxConstraints _getInnerConstraints(BoxConstraints constraints) { |
||||
return constraints.widthConstraints(); |
||||
} |
||||
|
||||
@override |
||||
double computeMinIntrinsicWidth(double height) { |
||||
if (child != null) return child!.getMinIntrinsicWidth(height); |
||||
return 0; |
||||
} |
||||
|
||||
@override |
||||
double computeMaxIntrinsicWidth(double height) { |
||||
if (child != null) return child!.getMaxIntrinsicWidth(height); |
||||
return 0; |
||||
} |
||||
|
||||
@override |
||||
double computeMinIntrinsicHeight(double width) { |
||||
if (child != null) return child!.getMinIntrinsicHeight(width); |
||||
return 0; |
||||
} |
||||
|
||||
@override |
||||
double computeMaxIntrinsicHeight(double width) { |
||||
if (child != null) return child!.getMaxIntrinsicHeight(width); |
||||
return 0; |
||||
} |
||||
|
||||
// We don't override computeDistanceToActualBaseline(), because we |
||||
// want the default behavior (returning null). Otherwise, as you |
||||
// scroll, it would shift in its parent if the parent was baseline-aligned, |
||||
// which makes no sense. |
||||
|
||||
@override |
||||
Size computeDryLayout(BoxConstraints constraints) { |
||||
if (child == null) { |
||||
return constraints.smallest; |
||||
} |
||||
final childSize = child!.getDryLayout(_getInnerConstraints(constraints)); |
||||
return constraints.constrain(childSize); |
||||
} |
||||
|
||||
@override |
||||
void performLayout() { |
||||
final constraints = this.constraints; |
||||
if (child == null) { |
||||
size = constraints.smallest; |
||||
} else { |
||||
child!.layout(_getInnerConstraints(constraints), parentUsesSize: true); |
||||
size = constraints.constrain(child!.size); |
||||
} |
||||
|
||||
offset |
||||
..applyViewportDimension(_viewportExtent) |
||||
..applyContentDimensions(_minScrollExtent, _maxScrollExtent); |
||||
} |
||||
|
||||
Offset get _paintOffset => _paintOffsetForPosition(offset.pixels); |
||||
|
||||
Offset _paintOffsetForPosition(double position) { |
||||
return Offset(0, -position); |
||||
} |
||||
|
||||
bool _shouldClipAtPaintOffset(Offset paintOffset) { |
||||
assert(child != null); |
||||
return paintOffset.dx < 0 || |
||||
paintOffset.dy < 0 || |
||||
paintOffset.dx + child!.size.width > size.width || |
||||
paintOffset.dy + child!.size.height > size.height; |
||||
} |
||||
|
||||
@override |
||||
void paint(PaintingContext context, Offset offset) { |
||||
if (child != null) { |
||||
final paintOffset = _paintOffset; |
||||
|
||||
void paintContents(PaintingContext context, Offset offset) { |
||||
context.paintChild(child!, offset + paintOffset); |
||||
} |
||||
|
||||
if (_shouldClipAtPaintOffset(paintOffset)) { |
||||
_clipRectLayer.layer = context.pushClipRect( |
||||
needsCompositing, |
||||
offset, |
||||
Offset.zero & size, |
||||
paintContents, |
||||
oldLayer: _clipRectLayer.layer, |
||||
); |
||||
} else { |
||||
_clipRectLayer.layer = null; |
||||
paintContents(context, offset); |
||||
} |
||||
} |
||||
} |
||||
|
||||
final _clipRectLayer = LayerHandle<ClipRectLayer>(); |
||||
|
||||
@override |
||||
void applyPaintTransform(RenderBox child, Matrix4 transform) { |
||||
final paintOffset = _paintOffset; |
||||
transform.translate(paintOffset.dx, paintOffset.dy); |
||||
} |
||||
|
||||
@override |
||||
Rect? describeApproximatePaintClip(RenderObject? child) { |
||||
if (child != null && _shouldClipAtPaintOffset(_paintOffset)) { |
||||
return Offset.zero & size; |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
@override |
||||
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { |
||||
if (child != null) { |
||||
return result.addWithPaintOffset( |
||||
offset: _paintOffset, |
||||
position: position, |
||||
hitTest: (result, transformed) { |
||||
assert(transformed == position + -_paintOffset); |
||||
return child!.hitTest(result, position: transformed); |
||||
}, |
||||
); |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
@override |
||||
RevealedOffset getOffsetToReveal(RenderObject target, double alignment, |
||||
{Rect? rect}) { |
||||
rect ??= target.paintBounds; |
||||
if (target is! RenderBox) { |
||||
return RevealedOffset(offset: offset.pixels, rect: rect); |
||||
} |
||||
|
||||
final targetBox = target; |
||||
final transform = targetBox.getTransformTo(child); |
||||
final bounds = MatrixUtils.transformRect(transform, rect); |
||||
|
||||
final double leadingScrollOffset; |
||||
final double targetMainAxisExtent; |
||||
final double mainAxisExtent; |
||||
|
||||
mainAxisExtent = size.height; |
||||
leadingScrollOffset = bounds.top; |
||||
targetMainAxisExtent = bounds.height; |
||||
|
||||
final targetOffset = leadingScrollOffset - |
||||
(mainAxisExtent - targetMainAxisExtent) * alignment; |
||||
final targetRect = bounds.shift(_paintOffsetForPosition(targetOffset)); |
||||
return RevealedOffset(offset: targetOffset, rect: targetRect); |
||||
} |
||||
|
||||
@override |
||||
void showOnScreen({ |
||||
RenderObject? descendant, |
||||
Rect? rect, |
||||
Duration duration = Duration.zero, |
||||
Curve curve = Curves.ease, |
||||
}) { |
||||
if (!offset.allowImplicitScrolling) { |
||||
return super.showOnScreen( |
||||
descendant: descendant, |
||||
rect: rect, |
||||
duration: duration, |
||||
curve: curve, |
||||
); |
||||
} |
||||
|
||||
final newRect = RenderViewportBase.showInViewport( |
||||
descendant: descendant, |
||||
viewport: this, |
||||
offset: offset, |
||||
rect: rect, |
||||
duration: duration, |
||||
curve: curve, |
||||
); |
||||
super.showOnScreen( |
||||
rect: newRect, |
||||
duration: duration, |
||||
curve: curve, |
||||
); |
||||
} |
||||
|
||||
@override |
||||
Rect describeSemanticsClip(RenderObject child) { |
||||
return Rect.fromLTRB( |
||||
semanticBounds.left, |
||||
semanticBounds.top - cacheExtent, |
||||
semanticBounds.right, |
||||
semanticBounds.bottom + cacheExtent, |
||||
); |
||||
} |
||||
} |
@ -1,355 +0,0 @@ |
||||
import 'dart:ui'; |
||||
|
||||
import 'package:characters/characters.dart'; |
||||
import 'package:flutter/services.dart'; |
||||
|
||||
import '../../models/documents/document.dart'; |
||||
import '../../utils/diff_delta.dart'; |
||||
import '../editor.dart'; |
||||
import '../keyboard_listener.dart'; |
||||
|
||||
mixin RawEditorStateKeyboardMixin on EditorState { |
||||
// Holds the last cursor location the user selected in the case the user tries |
||||
// to select vertically past the end or beginning of the field. If they do, |
||||
// then we need to keep the old cursor location so that we can go back to it |
||||
// if they change their minds. Only used for moving selection up and down in a |
||||
// multiline text field when selecting using the keyboard. |
||||
int _cursorResetLocation = -1; |
||||
|
||||
// Whether we should reset the location of the cursor in the case the user |
||||
// tries to select vertically past the end or beginning of the field. If they |
||||
// do, then we need to keep the old cursor location so that we can go back to |
||||
// it if they change their minds. Only used for resetting selection up and |
||||
// down in a multiline text field when selecting using the keyboard. |
||||
bool _wasSelectingVerticallyWithKeyboard = false; |
||||
|
||||
void handleCursorMovement( |
||||
LogicalKeyboardKey key, |
||||
bool wordModifier, |
||||
bool lineModifier, |
||||
bool shift, |
||||
) { |
||||
if (wordModifier && lineModifier) { |
||||
// If both modifiers are down, nothing happens on any of the platforms. |
||||
return; |
||||
} |
||||
final selection = widget.controller.selection; |
||||
|
||||
var newSelection = widget.controller.selection; |
||||
|
||||
final plainText = getTextEditingValue().text; |
||||
|
||||
final rightKey = key == LogicalKeyboardKey.arrowRight, |
||||
leftKey = key == LogicalKeyboardKey.arrowLeft, |
||||
upKey = key == LogicalKeyboardKey.arrowUp, |
||||
downKey = key == LogicalKeyboardKey.arrowDown; |
||||
|
||||
if ((rightKey || leftKey) && !(rightKey && leftKey)) { |
||||
newSelection = _jumpToBeginOrEndOfWord(newSelection, wordModifier, |
||||
leftKey, rightKey, plainText, lineModifier, shift); |
||||
} |
||||
|
||||
if (downKey || upKey) { |
||||
newSelection = _handleMovingCursorVertically( |
||||
upKey, downKey, shift, selection, newSelection, plainText); |
||||
} |
||||
|
||||
if (!shift) { |
||||
newSelection = |
||||
_placeCollapsedSelection(selection, newSelection, leftKey, rightKey); |
||||
} |
||||
|
||||
widget.controller.updateSelection(newSelection, ChangeSource.LOCAL); |
||||
} |
||||
|
||||
// Handles shortcut functionality including cut, copy, paste and select all |
||||
// using control/command + (X, C, V, A). |
||||
// TODO: Add support for formatting shortcuts: Cmd+B (bold), Cmd+I (italic) |
||||
// set editing value from clipboard for web |
||||
Future<void> handleShortcut(InputShortcut? shortcut) async { |
||||
final selection = widget.controller.selection; |
||||
final plainText = getTextEditingValue().text; |
||||
if (shortcut == InputShortcut.COPY) { |
||||
if (!selection.isCollapsed) { |
||||
await Clipboard.setData( |
||||
ClipboardData(text: selection.textInside(plainText))); |
||||
} |
||||
return; |
||||
} |
||||
if (shortcut == InputShortcut.CUT && !widget.readOnly) { |
||||
if (!selection.isCollapsed) { |
||||
final data = selection.textInside(plainText); |
||||
await Clipboard.setData(ClipboardData(text: data)); |
||||
|
||||
widget.controller.replaceText( |
||||
selection.start, |
||||
data.length, |
||||
'', |
||||
TextSelection.collapsed(offset: selection.start), |
||||
); |
||||
|
||||
setTextEditingValue(TextEditingValue( |
||||
text: |
||||
selection.textBefore(plainText) + selection.textAfter(plainText), |
||||
selection: TextSelection.collapsed(offset: selection.start), |
||||
)); |
||||
} |
||||
return; |
||||
} |
||||
if (shortcut == InputShortcut.PASTE && !widget.readOnly) { |
||||
final data = await Clipboard.getData(Clipboard.kTextPlain); |
||||
if (data != null) { |
||||
widget.controller.replaceText( |
||||
selection.start, |
||||
selection.end - selection.start, |
||||
data.text, |
||||
TextSelection.collapsed(offset: selection.start + data.text!.length), |
||||
); |
||||
} |
||||
return; |
||||
} |
||||
if (shortcut == InputShortcut.SELECT_ALL && |
||||
widget.enableInteractiveSelection) { |
||||
widget.controller.updateSelection( |
||||
selection.copyWith( |
||||
baseOffset: 0, |
||||
extentOffset: getTextEditingValue().text.length, |
||||
), |
||||
ChangeSource.REMOTE); |
||||
return; |
||||
} |
||||
} |
||||
|
||||
void handleDelete(bool forward) { |
||||
final selection = widget.controller.selection; |
||||
final plainText = getTextEditingValue().text; |
||||
var cursorPosition = selection.start; |
||||
var textBefore = selection.textBefore(plainText); |
||||
var textAfter = selection.textAfter(plainText); |
||||
if (selection.isCollapsed) { |
||||
if (!forward && textBefore.isNotEmpty) { |
||||
final characterBoundary = |
||||
_previousCharacter(textBefore.length, textBefore, true); |
||||
textBefore = textBefore.substring(0, characterBoundary); |
||||
cursorPosition = characterBoundary; |
||||
} |
||||
if (forward && textAfter.isNotEmpty && textAfter != '\n') { |
||||
final deleteCount = _nextCharacter(0, textAfter, true); |
||||
textAfter = textAfter.substring(deleteCount); |
||||
} |
||||
} |
||||
final newSelection = TextSelection.collapsed(offset: cursorPosition); |
||||
final newText = textBefore + textAfter; |
||||
final size = plainText.length - newText.length; |
||||
widget.controller.replaceText( |
||||
cursorPosition, |
||||
size, |
||||
'', |
||||
newSelection, |
||||
); |
||||
} |
||||
|
||||
TextSelection _jumpToBeginOrEndOfWord( |
||||
TextSelection newSelection, |
||||
bool wordModifier, |
||||
bool leftKey, |
||||
bool rightKey, |
||||
String plainText, |
||||
bool lineModifier, |
||||
bool shift) { |
||||
if (wordModifier) { |
||||
if (leftKey) { |
||||
final textSelection = getRenderEditor()!.selectWordAtPosition( |
||||
TextPosition( |
||||
offset: _previousCharacter( |
||||
newSelection.extentOffset, plainText, false))); |
||||
return newSelection.copyWith(extentOffset: textSelection.baseOffset); |
||||
} |
||||
final textSelection = getRenderEditor()!.selectWordAtPosition( |
||||
TextPosition( |
||||
offset: |
||||
_nextCharacter(newSelection.extentOffset, plainText, false))); |
||||
return newSelection.copyWith(extentOffset: textSelection.extentOffset); |
||||
} else if (lineModifier) { |
||||
if (leftKey) { |
||||
final textSelection = getRenderEditor()!.selectLineAtPosition( |
||||
TextPosition( |
||||
offset: _previousCharacter( |
||||
newSelection.extentOffset, plainText, false))); |
||||
return newSelection.copyWith(extentOffset: textSelection.baseOffset); |
||||
} |
||||
final startPoint = newSelection.extentOffset; |
||||
if (startPoint < plainText.length) { |
||||
final textSelection = getRenderEditor()! |
||||
.selectLineAtPosition(TextPosition(offset: startPoint)); |
||||
return newSelection.copyWith(extentOffset: textSelection.extentOffset); |
||||
} |
||||
return newSelection; |
||||
} |
||||
|
||||
if (rightKey && newSelection.extentOffset < plainText.length) { |
||||
final nextExtent = |
||||
_nextCharacter(newSelection.extentOffset, plainText, true); |
||||
final distance = nextExtent - newSelection.extentOffset; |
||||
newSelection = newSelection.copyWith(extentOffset: nextExtent); |
||||
if (shift) { |
||||
_cursorResetLocation += distance; |
||||
} |
||||
return newSelection; |
||||
} |
||||
|
||||
if (leftKey && newSelection.extentOffset > 0) { |
||||
final previousExtent = |
||||
_previousCharacter(newSelection.extentOffset, plainText, true); |
||||
final distance = newSelection.extentOffset - previousExtent; |
||||
newSelection = newSelection.copyWith(extentOffset: previousExtent); |
||||
if (shift) { |
||||
_cursorResetLocation -= distance; |
||||
} |
||||
return newSelection; |
||||
} |
||||
return newSelection; |
||||
} |
||||
|
||||
/// Returns the index into the string of the next character boundary after the |
||||
/// given index. |
||||
/// |
||||
/// The character boundary is determined by the characters package, so |
||||
/// surrogate pairs and extended grapheme clusters are considered. |
||||
/// |
||||
/// The index must be between 0 and string.length, inclusive. If given |
||||
/// string.length, string.length is returned. |
||||
/// |
||||
/// Setting includeWhitespace to false will only return the index of non-space |
||||
/// characters. |
||||
int _nextCharacter(int index, String string, bool includeWhitespace) { |
||||
assert(index >= 0 && index <= string.length); |
||||
if (index == string.length) { |
||||
return string.length; |
||||
} |
||||
|
||||
var count = 0; |
||||
final remain = string.characters.skipWhile((currentString) { |
||||
if (count <= index) { |
||||
count += currentString.length; |
||||
return true; |
||||
} |
||||
if (includeWhitespace) { |
||||
return false; |
||||
} |
||||
return WHITE_SPACE.contains(currentString.codeUnitAt(0)); |
||||
}); |
||||
return string.length - remain.toString().length; |
||||
} |
||||
|
||||
/// Returns the index into the string of the previous character boundary |
||||
/// before the given index. |
||||
/// |
||||
/// The character boundary is determined by the characters package, so |
||||
/// surrogate pairs and extended grapheme clusters are considered. |
||||
/// |
||||
/// The index must be between 0 and string.length, inclusive. If index is 0, |
||||
/// 0 will be returned. |
||||
/// |
||||
/// Setting includeWhitespace to false will only return the index of non-space |
||||
/// characters. |
||||
int _previousCharacter(int index, String string, includeWhitespace) { |
||||
assert(index >= 0 && index <= string.length); |
||||
if (index == 0) { |
||||
return 0; |
||||
} |
||||
|
||||
var count = 0; |
||||
int? lastNonWhitespace; |
||||
for (final currentString in string.characters) { |
||||
if (!includeWhitespace && |
||||
!WHITE_SPACE.contains( |
||||
currentString.characters.first.toString().codeUnitAt(0))) { |
||||
lastNonWhitespace = count; |
||||
} |
||||
if (count + currentString.length >= index) { |
||||
return includeWhitespace ? count : lastNonWhitespace ?? 0; |
||||
} |
||||
count += currentString.length; |
||||
} |
||||
return 0; |
||||
} |
||||
|
||||
TextSelection _handleMovingCursorVertically( |
||||
bool upKey, |
||||
bool downKey, |
||||
bool shift, |
||||
TextSelection selection, |
||||
TextSelection newSelection, |
||||
String plainText) { |
||||
final originPosition = TextPosition( |
||||
offset: upKey ? selection.baseOffset : selection.extentOffset); |
||||
|
||||
final child = getRenderEditor()!.childAtPosition(originPosition); |
||||
final localPosition = TextPosition( |
||||
offset: originPosition.offset - child.getContainer().documentOffset); |
||||
|
||||
var position = upKey |
||||
? child.getPositionAbove(localPosition) |
||||
: child.getPositionBelow(localPosition); |
||||
|
||||
if (position == null) { |
||||
final sibling = upKey |
||||
? getRenderEditor()!.childBefore(child) |
||||
: getRenderEditor()!.childAfter(child); |
||||
if (sibling == null) { |
||||
position = TextPosition(offset: upKey ? 0 : plainText.length - 1); |
||||
} else { |
||||
final finalOffset = Offset( |
||||
child.getOffsetForCaret(localPosition).dx, |
||||
sibling |
||||
.getOffsetForCaret(TextPosition( |
||||
offset: upKey ? sibling.getContainer().length - 1 : 0)) |
||||
.dy); |
||||
final siblingPosition = sibling.getPositionForOffset(finalOffset); |
||||
position = TextPosition( |
||||
offset: |
||||
sibling.getContainer().documentOffset + siblingPosition.offset); |
||||
} |
||||
} else { |
||||
position = TextPosition( |
||||
offset: child.getContainer().documentOffset + position.offset); |
||||
} |
||||
|
||||
if (position.offset == newSelection.extentOffset) { |
||||
if (downKey) { |
||||
newSelection = newSelection.copyWith(extentOffset: plainText.length); |
||||
} else if (upKey) { |
||||
newSelection = newSelection.copyWith(extentOffset: 0); |
||||
} |
||||
_wasSelectingVerticallyWithKeyboard = shift; |
||||
return newSelection; |
||||
} |
||||
|
||||
if (_wasSelectingVerticallyWithKeyboard && shift) { |
||||
newSelection = newSelection.copyWith(extentOffset: _cursorResetLocation); |
||||
_wasSelectingVerticallyWithKeyboard = false; |
||||
return newSelection; |
||||
} |
||||
newSelection = newSelection.copyWith(extentOffset: position.offset); |
||||
_cursorResetLocation = newSelection.extentOffset; |
||||
return newSelection; |
||||
} |
||||
|
||||
TextSelection _placeCollapsedSelection(TextSelection selection, |
||||
TextSelection newSelection, bool leftKey, bool rightKey) { |
||||
var newOffset = newSelection.extentOffset; |
||||
if (!selection.isCollapsed) { |
||||
if (leftKey) { |
||||
newOffset = newSelection.baseOffset < newSelection.extentOffset |
||||
? newSelection.baseOffset |
||||
: newSelection.extentOffset; |
||||
} else if (rightKey) { |
||||
newOffset = newSelection.baseOffset > newSelection.extentOffset |
||||
? newSelection.baseOffset |
||||
: newSelection.extentOffset; |
||||
} |
||||
} |
||||
return TextSelection.fromPosition(TextPosition(offset: newOffset)); |
||||
} |
||||
} |
@ -1,358 +0,0 @@ |
||||
import 'dart:convert'; |
||||
import 'dart:io' as io; |
||||
|
||||
import 'package:flutter/foundation.dart'; |
||||
import 'package:flutter/material.dart'; |
||||
import 'package:flutter/rendering.dart'; |
||||
import 'package:flutter/services.dart'; |
||||
import 'package:string_validator/string_validator.dart'; |
||||
import 'package:tuple/tuple.dart'; |
||||
|
||||
import '../models/documents/attribute.dart'; |
||||
import '../models/documents/document.dart'; |
||||
import '../models/documents/nodes/block.dart'; |
||||
import '../models/documents/nodes/leaf.dart' as leaf; |
||||
import '../models/documents/nodes/line.dart'; |
||||
import 'controller.dart'; |
||||
import 'cursor.dart'; |
||||
import 'default_styles.dart'; |
||||
import 'delegate.dart'; |
||||
import 'editor.dart'; |
||||
import 'text_block.dart'; |
||||
import 'text_line.dart'; |
||||
import 'video_app.dart'; |
||||
import 'youtube_video_app.dart'; |
||||
|
||||
class QuillSimpleViewer extends StatefulWidget { |
||||
const QuillSimpleViewer({ |
||||
required this.controller, |
||||
required this.readOnly, |
||||
this.customStyles, |
||||
this.truncate = false, |
||||
this.truncateScale, |
||||
this.truncateAlignment, |
||||
this.truncateHeight, |
||||
this.truncateWidth, |
||||
this.scrollBottomInset = 0, |
||||
this.padding = EdgeInsets.zero, |
||||
this.embedBuilder, |
||||
Key? key, |
||||
}) : assert(truncate || |
||||
((truncateScale == null) && |
||||
(truncateAlignment == null) && |
||||
(truncateHeight == null) && |
||||
(truncateWidth == null))), |
||||
super(key: key); |
||||
|
||||
final QuillController controller; |
||||
final DefaultStyles? customStyles; |
||||
final bool truncate; |
||||
final double? truncateScale; |
||||
final Alignment? truncateAlignment; |
||||
final double? truncateHeight; |
||||
final double? truncateWidth; |
||||
final double scrollBottomInset; |
||||
final EdgeInsetsGeometry padding; |
||||
final EmbedBuilder? embedBuilder; |
||||
final bool readOnly; |
||||
|
||||
@override |
||||
_QuillSimpleViewerState createState() => _QuillSimpleViewerState(); |
||||
} |
||||
|
||||
class _QuillSimpleViewerState extends State<QuillSimpleViewer> |
||||
with SingleTickerProviderStateMixin { |
||||
late DefaultStyles _styles; |
||||
final LayerLink _toolbarLayerLink = LayerLink(); |
||||
final LayerLink _startHandleLayerLink = LayerLink(); |
||||
final LayerLink _endHandleLayerLink = LayerLink(); |
||||
late CursorCont _cursorCont; |
||||
|
||||
@override |
||||
void initState() { |
||||
super.initState(); |
||||
|
||||
_cursorCont = CursorCont( |
||||
show: ValueNotifier<bool>(false), |
||||
style: const CursorStyle( |
||||
color: Colors.black, |
||||
backgroundColor: Colors.grey, |
||||
width: 2, |
||||
radius: Radius.zero, |
||||
offset: Offset.zero, |
||||
), |
||||
tickerProvider: this, |
||||
); |
||||
} |
||||
|
||||
@override |
||||
void didChangeDependencies() { |
||||
super.didChangeDependencies(); |
||||
final parentStyles = QuillStyles.getStyles(context, true); |
||||
final defaultStyles = DefaultStyles.getInstance(context); |
||||
_styles = (parentStyles != null) |
||||
? defaultStyles.merge(parentStyles) |
||||
: defaultStyles; |
||||
|
||||
if (widget.customStyles != null) { |
||||
_styles = _styles.merge(widget.customStyles!); |
||||
} |
||||
} |
||||
|
||||
EmbedBuilder get embedBuilder => widget.embedBuilder ?? _defaultEmbedBuilder; |
||||
|
||||
Widget _defaultEmbedBuilder( |
||||
BuildContext context, leaf.Embed node, bool readOnly) { |
||||
assert(!kIsWeb, 'Please provide EmbedBuilder for Web'); |
||||
switch (node.value.type) { |
||||
case 'image': |
||||
final imageUrl = _standardizeImageUrl(node.value.data); |
||||
return imageUrl.startsWith('http') |
||||
? Image.network(imageUrl) |
||||
: isBase64(imageUrl) |
||||
? Image.memory(base64.decode(imageUrl)) |
||||
: Image.file(io.File(imageUrl)); |
||||
case 'video': |
||||
final videoUrl = node.value.data; |
||||
if (videoUrl.contains('youtube.com') || videoUrl.contains('youtu.be')) { |
||||
return YoutubeVideoApp( |
||||
videoUrl: videoUrl, context: context, readOnly: readOnly); |
||||
} |
||||
return VideoApp( |
||||
videoUrl: videoUrl, context: context, readOnly: readOnly); |
||||
default: |
||||
throw UnimplementedError( |
||||
'Embeddable type "${node.value.type}" is not supported by default ' |
||||
'embed builder of QuillEditor. You must pass your own builder ' |
||||
'function to embedBuilder property of QuillEditor or QuillField ' |
||||
'widgets.', |
||||
); |
||||
} |
||||
} |
||||
|
||||
String _standardizeImageUrl(String url) { |
||||
if (url.contains('base64')) { |
||||
return url.split(',')[1]; |
||||
} |
||||
return url; |
||||
} |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
final _doc = widget.controller.document; |
||||
// if (_doc.isEmpty() && |
||||
// !widget.focusNode.hasFocus && |
||||
// widget.placeholder != null) { |
||||
// _doc = Document.fromJson(jsonDecode( |
||||
// '[{"attributes":{"placeholder":true},"insert":"${widget.placeholder}\\n"}]')); |
||||
// } |
||||
|
||||
Widget child = CompositedTransformTarget( |
||||
link: _toolbarLayerLink, |
||||
child: Semantics( |
||||
child: _SimpleViewer( |
||||
document: _doc, |
||||
textDirection: _textDirection, |
||||
startHandleLayerLink: _startHandleLayerLink, |
||||
endHandleLayerLink: _endHandleLayerLink, |
||||
onSelectionChanged: _nullSelectionChanged, |
||||
scrollBottomInset: widget.scrollBottomInset, |
||||
padding: widget.padding, |
||||
children: _buildChildren(_doc, context), |
||||
), |
||||
), |
||||
); |
||||
|
||||
if (widget.truncate) { |
||||
if (widget.truncateScale != null) { |
||||
child = Container( |
||||
height: widget.truncateHeight, |
||||
child: Align( |
||||
heightFactor: widget.truncateScale, |
||||
widthFactor: widget.truncateScale, |
||||
alignment: widget.truncateAlignment ?? Alignment.topLeft, |
||||
child: Container( |
||||
width: widget.truncateWidth! / widget.truncateScale!, |
||||
child: SingleChildScrollView( |
||||
physics: const NeverScrollableScrollPhysics(), |
||||
child: Transform.scale( |
||||
scale: widget.truncateScale!, |
||||
alignment: |
||||
widget.truncateAlignment ?? Alignment.topLeft, |
||||
child: child))))); |
||||
} else { |
||||
child = Container( |
||||
height: widget.truncateHeight, |
||||
width: widget.truncateWidth, |
||||
child: SingleChildScrollView( |
||||
physics: const NeverScrollableScrollPhysics(), child: child)); |
||||
} |
||||
} |
||||
|
||||
return QuillStyles(data: _styles, child: child); |
||||
} |
||||
|
||||
List<Widget> _buildChildren(Document doc, BuildContext context) { |
||||
final result = <Widget>[]; |
||||
final indentLevelCounts = <int, int>{}; |
||||
for (final node in doc.root.children) { |
||||
if (node is Line) { |
||||
final editableTextLine = _getEditableTextLineFromNode(node, context); |
||||
result.add(editableTextLine); |
||||
} else if (node is Block) { |
||||
final attrs = node.style.attributes; |
||||
final editableTextBlock = EditableTextBlock( |
||||
block: node, |
||||
textDirection: _textDirection, |
||||
scrollBottomInset: widget.scrollBottomInset, |
||||
verticalSpacing: _getVerticalSpacingForBlock(node, _styles), |
||||
textSelection: widget.controller.selection, |
||||
color: Colors.black, |
||||
styles: _styles, |
||||
enableInteractiveSelection: false, |
||||
hasFocus: false, |
||||
contentPadding: attrs.containsKey(Attribute.codeBlock.key) |
||||
? const EdgeInsets.all(16) |
||||
: null, |
||||
embedBuilder: embedBuilder, |
||||
cursorCont: _cursorCont, |
||||
indentLevelCounts: indentLevelCounts, |
||||
onCheckboxTap: _handleCheckboxTap, |
||||
readOnly: widget.readOnly); |
||||
result.add(editableTextBlock); |
||||
} else { |
||||
throw StateError('Unreachable.'); |
||||
} |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/// Updates the checkbox positioned at [offset] in document |
||||
/// by changing its attribute according to [value]. |
||||
void _handleCheckboxTap(int offset, bool value) { |
||||
// readonly - do nothing |
||||
} |
||||
|
||||
TextDirection get _textDirection { |
||||
final result = Directionality.of(context); |
||||
return result; |
||||
} |
||||
|
||||
EditableTextLine _getEditableTextLineFromNode( |
||||
Line node, BuildContext context) { |
||||
final textLine = TextLine( |
||||
line: node, |
||||
textDirection: _textDirection, |
||||
embedBuilder: embedBuilder, |
||||
styles: _styles, |
||||
readOnly: widget.readOnly, |
||||
); |
||||
final editableTextLine = EditableTextLine( |
||||
node, |
||||
null, |
||||
textLine, |
||||
0, |
||||
_getVerticalSpacingForLine(node, _styles), |
||||
_textDirection, |
||||
widget.controller.selection, |
||||
Colors.black, |
||||
//widget.selectionColor, |
||||
false, |
||||
//enableInteractiveSelection, |
||||
false, |
||||
//_hasFocus, |
||||
MediaQuery.of(context).devicePixelRatio, |
||||
_cursorCont); |
||||
return editableTextLine; |
||||
} |
||||
|
||||
Tuple2<double, double> _getVerticalSpacingForLine( |
||||
Line line, DefaultStyles? defaultStyles) { |
||||
final attrs = line.style.attributes; |
||||
if (attrs.containsKey(Attribute.header.key)) { |
||||
final int? level = attrs[Attribute.header.key]!.value; |
||||
switch (level) { |
||||
case 1: |
||||
return defaultStyles!.h1!.verticalSpacing; |
||||
case 2: |
||||
return defaultStyles!.h2!.verticalSpacing; |
||||
case 3: |
||||
return defaultStyles!.h3!.verticalSpacing; |
||||
default: |
||||
throw 'Invalid level $level'; |
||||
} |
||||
} |
||||
|
||||
return defaultStyles!.paragraph!.verticalSpacing; |
||||
} |
||||
|
||||
Tuple2<double, double> _getVerticalSpacingForBlock( |
||||
Block node, DefaultStyles? defaultStyles) { |
||||
final attrs = node.style.attributes; |
||||
if (attrs.containsKey(Attribute.blockQuote.key)) { |
||||
return defaultStyles!.quote!.verticalSpacing; |
||||
} else if (attrs.containsKey(Attribute.codeBlock.key)) { |
||||
return defaultStyles!.code!.verticalSpacing; |
||||
} else if (attrs.containsKey(Attribute.indent.key)) { |
||||
return defaultStyles!.indent!.verticalSpacing; |
||||
} |
||||
return defaultStyles!.lists!.verticalSpacing; |
||||
} |
||||
|
||||
void _nullSelectionChanged( |
||||
TextSelection selection, SelectionChangedCause cause) {} |
||||
} |
||||
|
||||
class _SimpleViewer extends MultiChildRenderObjectWidget { |
||||
_SimpleViewer({ |
||||
required List<Widget> children, |
||||
required this.document, |
||||
required this.textDirection, |
||||
required this.startHandleLayerLink, |
||||
required this.endHandleLayerLink, |
||||
required this.onSelectionChanged, |
||||
required this.scrollBottomInset, |
||||
this.padding = EdgeInsets.zero, |
||||
Key? key, |
||||
}) : super(key: key, children: children); |
||||
|
||||
final Document document; |
||||
final TextDirection textDirection; |
||||
final LayerLink startHandleLayerLink; |
||||
final LayerLink endHandleLayerLink; |
||||
final TextSelectionChangedHandler onSelectionChanged; |
||||
final double scrollBottomInset; |
||||
final EdgeInsetsGeometry padding; |
||||
|
||||
@override |
||||
RenderEditor createRenderObject(BuildContext context) { |
||||
return RenderEditor( |
||||
null, |
||||
textDirection, |
||||
scrollBottomInset, |
||||
padding, |
||||
document, |
||||
const TextSelection(baseOffset: 0, extentOffset: 0), |
||||
false, |
||||
// hasFocus, |
||||
onSelectionChanged, |
||||
startHandleLayerLink, |
||||
endHandleLayerLink, |
||||
const EdgeInsets.fromLTRB(4, 4, 4, 5), |
||||
); |
||||
} |
||||
|
||||
@override |
||||
void updateRenderObject( |
||||
BuildContext context, covariant RenderEditor renderObject) { |
||||
renderObject |
||||
..document = document |
||||
..setContainer(document.root) |
||||
..textDirection = textDirection |
||||
..setStartHandleLayerLink(startHandleLayerLink) |
||||
..setEndHandleLayerLink(endHandleLayerLink) |
||||
..onSelectionChanged = onSelectionChanged |
||||
..setScrollBottomInset(scrollBottomInset) |
||||
..setPadding(padding); |
||||
} |
||||
} |
@ -0,0 +1,22 @@ |
||||
import 'package:flutter/material.dart'; |
||||
|
||||
class QuillBulletPoint extends StatelessWidget { |
||||
const QuillBulletPoint({ |
||||
required this.style, |
||||
required this.width, |
||||
Key? key, |
||||
}) : super(key: key); |
||||
|
||||
final TextStyle style; |
||||
final double width; |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
return Container( |
||||
alignment: AlignmentDirectional.topEnd, |
||||
width: width, |
||||
padding: const EdgeInsetsDirectional.only(end: 13), |
||||
child: Text('•', style: style), |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,78 @@ |
||||
import 'package:flutter/material.dart'; |
||||
|
||||
class CheckboxPoint extends StatefulWidget { |
||||
const CheckboxPoint({ |
||||
required this.size, |
||||
required this.value, |
||||
required this.enabled, |
||||
required this.onChanged, |
||||
this.uiBuilder, |
||||
Key? key, |
||||
}) : super(key: key); |
||||
|
||||
final double size; |
||||
final bool value; |
||||
final bool enabled; |
||||
final ValueChanged<bool> onChanged; |
||||
final QuillCheckboxBuilder? uiBuilder; |
||||
|
||||
@override |
||||
_CheckboxPointState createState() => _CheckboxPointState(); |
||||
} |
||||
|
||||
class _CheckboxPointState extends State<CheckboxPoint> { |
||||
@override |
||||
Widget build(BuildContext context) { |
||||
if (widget.uiBuilder != null) { |
||||
return widget.uiBuilder!.build( |
||||
context: context, |
||||
isChecked: widget.value, |
||||
onChanged: widget.onChanged, |
||||
); |
||||
} |
||||
final theme = Theme.of(context); |
||||
final fillColor = widget.value |
||||
? (widget.enabled |
||||
? theme.colorScheme.primary |
||||
: theme.colorScheme.onSurface.withOpacity(0.5)) |
||||
: theme.colorScheme.surface; |
||||
final borderColor = widget.value |
||||
? (widget.enabled |
||||
? theme.colorScheme.primary |
||||
: theme.colorScheme.onSurface.withOpacity(0)) |
||||
: (widget.enabled |
||||
? theme.colorScheme.onSurface.withOpacity(0.5) |
||||
: theme.colorScheme.onSurface.withOpacity(0.3)); |
||||
return Center( |
||||
child: SizedBox( |
||||
width: widget.size, |
||||
height: widget.size, |
||||
child: Material( |
||||
color: fillColor, |
||||
shape: RoundedRectangleBorder( |
||||
side: BorderSide( |
||||
color: borderColor, |
||||
), |
||||
borderRadius: BorderRadius.circular(2), |
||||
), |
||||
child: InkWell( |
||||
onTap: |
||||
widget.enabled ? () => widget.onChanged(!widget.value) : null, |
||||
child: widget.value |
||||
? Icon(Icons.check, |
||||
size: widget.size, color: theme.colorScheme.onPrimary) |
||||
: null, |
||||
), |
||||
), |
||||
), |
||||
); |
||||
} |
||||
} |
||||
|
||||
abstract class QuillCheckboxBuilder { |
||||
Widget build({ |
||||
required BuildContext context, |
||||
required bool isChecked, |
||||
required ValueChanged<bool> onChanged, |
||||
}); |
||||
} |
@ -0,0 +1,108 @@ |
||||
import 'package:flutter/material.dart'; |
||||
|
||||
import '../../models/documents/attribute.dart'; |
||||
import '../text_block.dart'; |
||||
|
||||
class QuillNumberPoint extends StatelessWidget { |
||||
const QuillNumberPoint({ |
||||
required this.index, |
||||
required this.indentLevelCounts, |
||||
required this.count, |
||||
required this.style, |
||||
required this.width, |
||||
required this.attrs, |
||||
this.withDot = true, |
||||
this.padding = 0.0, |
||||
Key? key, |
||||
}) : super(key: key); |
||||
|
||||
final int index; |
||||
final Map<int?, int> indentLevelCounts; |
||||
final int count; |
||||
final TextStyle style; |
||||
final double width; |
||||
final Map<String, Attribute> attrs; |
||||
final bool withDot; |
||||
final double padding; |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
var s = index.toString(); |
||||
int? level = 0; |
||||
if (!attrs.containsKey(Attribute.indent.key) && |
||||
!indentLevelCounts.containsKey(1)) { |
||||
indentLevelCounts.clear(); |
||||
return Container( |
||||
alignment: AlignmentDirectional.topEnd, |
||||
width: width, |
||||
padding: EdgeInsetsDirectional.only(end: padding), |
||||
child: Text(withDot ? '$s.' : s, style: style), |
||||
); |
||||
} |
||||
if (attrs.containsKey(Attribute.indent.key)) { |
||||
level = attrs[Attribute.indent.key]!.value; |
||||
} else { |
||||
// first level but is back from previous indent level |
||||
// supposed to be "2." |
||||
indentLevelCounts[0] = 1; |
||||
} |
||||
if (indentLevelCounts.containsKey(level! + 1)) { |
||||
// last visited level is done, going up |
||||
indentLevelCounts.remove(level + 1); |
||||
} |
||||
final count = (indentLevelCounts[level] ?? 0) + 1; |
||||
indentLevelCounts[level] = count; |
||||
|
||||
s = count.toString(); |
||||
if (level % 3 == 1) { |
||||
// a. b. c. d. e. ... |
||||
s = _toExcelSheetColumnTitle(count); |
||||
} else if (level % 3 == 2) { |
||||
// i. ii. iii. ... |
||||
s = _intToRoman(count); |
||||
} |
||||
// level % 3 == 0 goes back to 1. 2. 3. |
||||
|
||||
return Container( |
||||
alignment: AlignmentDirectional.topEnd, |
||||
width: width, |
||||
padding: EdgeInsetsDirectional.only(end: padding), |
||||
child: Text(withDot ? '$s.' : s, style: style), |
||||
); |
||||
} |
||||
|
||||
String _toExcelSheetColumnTitle(int n) { |
||||
final result = StringBuffer(); |
||||
while (n > 0) { |
||||
n--; |
||||
result.write(String.fromCharCode((n % 26).floor() + 97)); |
||||
n = (n / 26).floor(); |
||||
} |
||||
|
||||
return result.toString().split('').reversed.join(); |
||||
} |
||||
|
||||
String _intToRoman(int input) { |
||||
var num = input; |
||||
|
||||
if (num < 0) { |
||||
return ''; |
||||
} else if (num == 0) { |
||||
return 'nulla'; |
||||
} |
||||
|
||||
final builder = StringBuffer(); |
||||
for (var a = 0; a < arabianRomanNumbers.length; a++) { |
||||
final times = (num / arabianRomanNumbers[a]) |
||||
.truncate(); // equals 1 only when arabianRomanNumbers[a] = num |
||||
// executes n times where n is the number of times you have to add |
||||
// the current roman number value to reach current num. |
||||
builder.write(romanNumbers[a] * times); |
||||
num -= times * |
||||
arabianRomanNumbers[ |
||||
a]; // subtract previous roman number value from num |
||||
} |
||||
|
||||
return builder.toString().toLowerCase(); |
||||
} |
||||
} |
@ -0,0 +1,3 @@ |
||||
export 'bullet_point.dart'; |
||||
export 'checkbox_point.dart'; |
||||
export 'number_point.dart'; |
@ -0,0 +1,154 @@ |
||||
import 'package:flutter/foundation.dart'; |
||||
import 'package:flutter/material.dart'; |
||||
|
||||
import '../../models/documents/attribute.dart'; |
||||
import '../../models/documents/style.dart'; |
||||
import '../../models/themes/quill_icon_theme.dart'; |
||||
import '../controller.dart'; |
||||
import '../toolbar.dart'; |
||||
|
||||
class SelectAlignmentButton extends StatefulWidget { |
||||
const SelectAlignmentButton({ |
||||
required this.controller, |
||||
this.iconSize = kDefaultIconSize, |
||||
this.iconTheme, |
||||
this.showLeftAlignment, |
||||
this.showCenterAlignment, |
||||
this.showRightAlignment, |
||||
this.showJustifyAlignment, |
||||
Key? key, |
||||
}) : super(key: key); |
||||
|
||||
final QuillController controller; |
||||
final double iconSize; |
||||
|
||||
final QuillIconTheme? iconTheme; |
||||
final bool? showLeftAlignment; |
||||
final bool? showCenterAlignment; |
||||
final bool? showRightAlignment; |
||||
final bool? showJustifyAlignment; |
||||
|
||||
@override |
||||
_SelectAlignmentButtonState createState() => _SelectAlignmentButtonState(); |
||||
} |
||||
|
||||
class _SelectAlignmentButtonState extends State<SelectAlignmentButton> { |
||||
Attribute? _value; |
||||
|
||||
Style get _selectionStyle => widget.controller.getSelectionStyle(); |
||||
|
||||
@override |
||||
void initState() { |
||||
super.initState(); |
||||
setState(() { |
||||
_value = _selectionStyle.attributes[Attribute.align.key] ?? |
||||
Attribute.leftAlignment; |
||||
}); |
||||
widget.controller.addListener(_didChangeEditingValue); |
||||
} |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
final _valueToText = <Attribute, String>{ |
||||
if (widget.showLeftAlignment!) |
||||
Attribute.leftAlignment: Attribute.leftAlignment.value!, |
||||
if (widget.showCenterAlignment!) |
||||
Attribute.centerAlignment: Attribute.centerAlignment.value!, |
||||
if (widget.showRightAlignment!) |
||||
Attribute.rightAlignment: Attribute.rightAlignment.value!, |
||||
if (widget.showJustifyAlignment!) |
||||
Attribute.justifyAlignment: Attribute.justifyAlignment.value!, |
||||
}; |
||||
|
||||
final _valueAttribute = <Attribute>[ |
||||
if (widget.showLeftAlignment!) Attribute.leftAlignment, |
||||
if (widget.showCenterAlignment!) Attribute.centerAlignment, |
||||
if (widget.showRightAlignment!) Attribute.rightAlignment, |
||||
if (widget.showJustifyAlignment!) Attribute.justifyAlignment |
||||
]; |
||||
final _valueString = <String>[ |
||||
if (widget.showLeftAlignment!) Attribute.leftAlignment.value!, |
||||
if (widget.showCenterAlignment!) Attribute.centerAlignment.value!, |
||||
if (widget.showRightAlignment!) Attribute.rightAlignment.value!, |
||||
if (widget.showJustifyAlignment!) Attribute.justifyAlignment.value!, |
||||
]; |
||||
|
||||
final theme = Theme.of(context); |
||||
|
||||
final buttonCount = ((widget.showLeftAlignment!) ? 1 : 0) + |
||||
((widget.showCenterAlignment!) ? 1 : 0) + |
||||
((widget.showRightAlignment!) ? 1 : 0) + |
||||
((widget.showJustifyAlignment!) ? 1 : 0); |
||||
|
||||
return Row( |
||||
mainAxisSize: MainAxisSize.min, |
||||
children: List.generate(buttonCount, (index) { |
||||
return Padding( |
||||
padding: EdgeInsets.symmetric(horizontal: !kIsWeb ? 1.0 : 5.0), |
||||
child: ConstrainedBox( |
||||
constraints: BoxConstraints.tightFor( |
||||
width: widget.iconSize * kIconButtonFactor, |
||||
height: widget.iconSize * kIconButtonFactor, |
||||
), |
||||
child: RawMaterialButton( |
||||
hoverElevation: 0, |
||||
highlightElevation: 0, |
||||
elevation: 0, |
||||
visualDensity: VisualDensity.compact, |
||||
shape: RoundedRectangleBorder( |
||||
borderRadius: BorderRadius.circular(2)), |
||||
fillColor: _valueToText[_value] == _valueString[index] |
||||
? (widget.iconTheme?.iconSelectedFillColor ?? |
||||
theme.toggleableActiveColor) |
||||
: (widget.iconTheme?.iconUnselectedFillColor ?? |
||||
theme.canvasColor), |
||||
onPressed: () => _valueAttribute[index] == Attribute.leftAlignment |
||||
? widget.controller |
||||
.formatSelection(Attribute.clone(Attribute.align, null)) |
||||
: widget.controller.formatSelection(_valueAttribute[index]), |
||||
child: Icon( |
||||
_valueString[index] == Attribute.leftAlignment.value |
||||
? Icons.format_align_left |
||||
: _valueString[index] == Attribute.centerAlignment.value |
||||
? Icons.format_align_center |
||||
: _valueString[index] == Attribute.rightAlignment.value |
||||
? Icons.format_align_right |
||||
: Icons.format_align_justify, |
||||
size: widget.iconSize, |
||||
color: _valueToText[_value] == _valueString[index] |
||||
? (widget.iconTheme?.iconSelectedColor ?? |
||||
theme.primaryIconTheme.color) |
||||
: (widget.iconTheme?.iconUnselectedColor ?? |
||||
theme.iconTheme.color), |
||||
), |
||||
), |
||||
), |
||||
); |
||||
}), |
||||
); |
||||
} |
||||
|
||||
void _didChangeEditingValue() { |
||||
setState(() { |
||||
_value = _selectionStyle.attributes[Attribute.align.key] ?? |
||||
Attribute.leftAlignment; |
||||
}); |
||||
} |
||||
|
||||
@override |
||||
void didUpdateWidget(covariant SelectAlignmentButton oldWidget) { |
||||
super.didUpdateWidget(oldWidget); |
||||
if (oldWidget.controller != widget.controller) { |
||||
oldWidget.controller.removeListener(_didChangeEditingValue); |
||||
widget.controller.addListener(_didChangeEditingValue); |
||||
_value = _selectionStyle.attributes[Attribute.align.key] ?? |
||||
Attribute.leftAlignment; |
||||
} |
||||
} |
||||
|
||||
@override |
||||
void dispose() { |
||||
widget.controller.removeListener(_didChangeEditingValue); |
||||
super.dispose(); |
||||
} |
||||
} |
@ -1,3 +0,0 @@ |
||||
/// TODO: Remove this file in the next breaking release, because implementation |
||||
/// files should be located in the src folder, https://bit.ly/3fA23Yz. |
||||
export '../src/widgets/keyboard_listener.dart'; |
@ -1,3 +0,0 @@ |
||||
/// TODO: Remove this file in the next breaking release, because implementation |
||||
/// files should be located in the src folder, https://bit.ly/3fA23Yz. |
||||
export '../../src/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart'; |
@ -1,3 +0,0 @@ |
||||
/// TODO: Remove this file in the next breaking release, because implementation |
||||
/// files should be located in the src folder, https://bit.ly/3fA23Yz. |
||||
export '../src/widgets/simple_viewer.dart'; |
Loading…
Reference in new issue