[flutter]: toolbar color selection
3
app_flowy/.gitignore
vendored
@ -45,4 +45,5 @@ app.*.map.json
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
/packages/flowy_protobuf
|
||||
/packages/flowy_protobuf
|
||||
/packages/flutter-quill
|
@ -1,6 +1,6 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:editor/flutter_quill.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:flowy_log/flowy_log.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-workspace/errors.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'dart:io';
|
||||
import 'package:app_flowy/startup/startup.dart';
|
||||
import 'package:app_flowy/workspace/application/doc/doc_bloc.dart';
|
||||
import 'package:editor/flutter_quill.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/progress_indicator.dart';
|
||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-workspace/view_create.pb.dart';
|
||||
|
@ -1,5 +1,5 @@
|
||||
import 'package:editor/flutter_quill.dart';
|
||||
import 'package:editor/models/documents/style.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:flutter_quill/models/documents/style.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'toolbar_icon_button.dart';
|
||||
@ -8,7 +8,7 @@ class FlowyCheckListButton extends StatefulWidget {
|
||||
const FlowyCheckListButton({
|
||||
required this.controller,
|
||||
required this.attribute,
|
||||
this.iconSize = kDefaultIconSize,
|
||||
this.iconSize = defaultIconSize,
|
||||
this.fillColor,
|
||||
this.childBuilder = defaultToggleStyleButtonBuilder,
|
||||
Key? key,
|
||||
|
@ -0,0 +1,272 @@
|
||||
import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:flutter_quill/models/documents/style.dart';
|
||||
import 'package:flutter_quill/utils/color.dart';
|
||||
|
||||
import 'toolbar_icon_button.dart';
|
||||
|
||||
class FlowyColorButton extends StatefulWidget {
|
||||
const FlowyColorButton({
|
||||
required this.icon,
|
||||
required this.controller,
|
||||
required this.background,
|
||||
this.iconSize = defaultIconSize,
|
||||
this.iconTheme,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
final IconData icon;
|
||||
final double iconSize;
|
||||
final bool background;
|
||||
final QuillController controller;
|
||||
final QuillIconTheme? iconTheme;
|
||||
|
||||
@override
|
||||
_FlowyColorButtonState createState() => _FlowyColorButtonState();
|
||||
}
|
||||
|
||||
class _FlowyColorButtonState extends State<FlowyColorButton> {
|
||||
late bool _isToggledColor;
|
||||
late bool _isToggledBackground;
|
||||
late bool _isWhite;
|
||||
late bool _isWhitebackground;
|
||||
|
||||
Style get _selectionStyle => widget.controller.getSelectionStyle();
|
||||
|
||||
void _didChangeEditingValue() {
|
||||
setState(() {
|
||||
_isToggledColor = _getIsToggledColor(widget.controller.getSelectionStyle().attributes);
|
||||
_isToggledBackground = _getIsToggledBackground(widget.controller.getSelectionStyle().attributes);
|
||||
_isWhite = _isToggledColor && _selectionStyle.attributes['color']!.value == '#ffffff';
|
||||
_isWhitebackground = _isToggledBackground && _selectionStyle.attributes['background']!.value == '#ffffff';
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_isToggledColor = _getIsToggledColor(_selectionStyle.attributes);
|
||||
_isToggledBackground = _getIsToggledBackground(_selectionStyle.attributes);
|
||||
_isWhite = _isToggledColor && _selectionStyle.attributes['color']!.value == '#ffffff';
|
||||
_isWhitebackground = _isToggledBackground && _selectionStyle.attributes['background']!.value == '#ffffff';
|
||||
widget.controller.addListener(_didChangeEditingValue);
|
||||
}
|
||||
|
||||
bool _getIsToggledColor(Map<String, Attribute> attrs) {
|
||||
return attrs.containsKey(Attribute.color.key);
|
||||
}
|
||||
|
||||
bool _getIsToggledBackground(Map<String, Attribute> attrs) {
|
||||
return attrs.containsKey(Attribute.background.key);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant FlowyColorButton oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.controller != widget.controller) {
|
||||
oldWidget.controller.removeListener(_didChangeEditingValue);
|
||||
widget.controller.addListener(_didChangeEditingValue);
|
||||
_isToggledColor = _getIsToggledColor(_selectionStyle.attributes);
|
||||
_isToggledBackground = _getIsToggledBackground(_selectionStyle.attributes);
|
||||
_isWhite = _isToggledColor && _selectionStyle.attributes['color']!.value == '#ffffff';
|
||||
_isWhitebackground = _isToggledBackground && _selectionStyle.attributes['background']!.value == '#ffffff';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.controller.removeListener(_didChangeEditingValue);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final iconColor = _isToggledColor && !widget.background && !_isWhite
|
||||
? stringToColor(_selectionStyle.attributes['color']!.value)
|
||||
: (widget.iconTheme?.iconUnselectedColor ?? theme.iconTheme.color);
|
||||
|
||||
final iconColorBackground = _isToggledBackground && widget.background && !_isWhitebackground
|
||||
? stringToColor(_selectionStyle.attributes['background']!.value)
|
||||
: (widget.iconTheme?.iconUnselectedColor ?? theme.iconTheme.color);
|
||||
|
||||
final fillColor = _isToggledColor && !widget.background && _isWhite
|
||||
? stringToColor('#ffffff')
|
||||
: (widget.iconTheme?.iconUnselectedFillColor ?? theme.canvasColor);
|
||||
final fillColorBackground = _isToggledBackground && widget.background && _isWhitebackground
|
||||
? stringToColor('#ffffff')
|
||||
: (widget.iconTheme?.iconUnselectedFillColor ?? theme.canvasColor);
|
||||
|
||||
return QuillIconButton(
|
||||
highlightElevation: 0,
|
||||
hoverElevation: 0,
|
||||
size: widget.iconSize * kIconButtonFactor,
|
||||
icon: Icon(widget.icon, size: widget.iconSize, color: widget.background ? iconColorBackground : iconColor),
|
||||
fillColor: widget.background ? fillColorBackground : fillColor,
|
||||
onPressed: _showColorPicker,
|
||||
);
|
||||
}
|
||||
|
||||
void _changeColor(BuildContext context, Color color) {
|
||||
var hex = color.value.toRadixString(16);
|
||||
if (hex.startsWith('ff')) {
|
||||
hex = hex.substring(2);
|
||||
}
|
||||
hex = '#$hex';
|
||||
widget.controller.formatSelection(widget.background ? BackgroundAttribute(hex) : ColorAttribute(hex));
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
void _showColorPicker() {
|
||||
// FlowyPoppuWindow.show(
|
||||
// context,
|
||||
// size: Size(600, 200),
|
||||
// child: MaterialPicker(
|
||||
// pickerColor: const Color(0x00000000),
|
||||
// onColorChanged: (color) => _changeColor(context, color),
|
||||
// ),
|
||||
// );
|
||||
|
||||
final style = widget.controller.getSelectionStyle();
|
||||
final values = style.values.where((v) => v.key == Attribute.background.key).map((v) => v.value);
|
||||
int initailColor = 0;
|
||||
if (values.isNotEmpty) {
|
||||
assert(values.length == 1);
|
||||
initailColor = stringToHex(values.first);
|
||||
}
|
||||
|
||||
StyledDialog(
|
||||
child: SingleChildScrollView(
|
||||
child: FlowyColorPicker(
|
||||
onColorChanged: (color) {
|
||||
if (color == null) {
|
||||
widget.controller.formatSelection(BackgroundAttribute(null));
|
||||
Navigator.of(context).pop();
|
||||
} else {
|
||||
_changeColor(context, color);
|
||||
}
|
||||
},
|
||||
initailColor: initailColor,
|
||||
),
|
||||
),
|
||||
).show(context);
|
||||
}
|
||||
}
|
||||
|
||||
int stringToHex(String code) {
|
||||
return int.parse(code.substring(1, 7), radix: 16) + 0xFF000000;
|
||||
}
|
||||
|
||||
class FlowyColorPicker extends StatefulWidget {
|
||||
final List<int> colors = [
|
||||
0xffe8e0ff,
|
||||
0xffffe7fd,
|
||||
0xffffe7ee,
|
||||
0xffffefe3,
|
||||
0xfffff2cd,
|
||||
0xfff5ffdc,
|
||||
0xffddffd6,
|
||||
0xffdefff1,
|
||||
0xffdefff1,
|
||||
];
|
||||
final Function(Color?) onColorChanged;
|
||||
final int initailColor;
|
||||
FlowyColorPicker({Key? key, required this.onColorChanged, this.initailColor = 0}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<FlowyColorPicker> createState() => _FlowyColorPickerState();
|
||||
}
|
||||
|
||||
// if (shrinkWrap) {
|
||||
// innerContent = IntrinsicWidth(child: IntrinsicHeight(child: innerContent));
|
||||
// }
|
||||
class _FlowyColorPickerState extends State<FlowyColorPicker> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const double width = 480;
|
||||
const int crossAxisCount = 6;
|
||||
const double mainAxisSpacing = 10;
|
||||
const double crossAxisSpacing = 10;
|
||||
final numberOfRows = (widget.colors.length / crossAxisCount).ceil();
|
||||
|
||||
const perRowHeight = ((width - ((crossAxisCount - 1) * mainAxisSpacing)) / crossAxisCount);
|
||||
final totalHeight = numberOfRows * perRowHeight + numberOfRows * crossAxisSpacing;
|
||||
|
||||
return Container(
|
||||
constraints: BoxConstraints.tightFor(width: width, height: totalHeight),
|
||||
child: CustomScrollView(
|
||||
scrollDirection: Axis.vertical,
|
||||
controller: ScrollController(),
|
||||
physics: const ClampingScrollPhysics(),
|
||||
slivers: [
|
||||
SliverGrid(
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: crossAxisCount,
|
||||
mainAxisSpacing: mainAxisSpacing,
|
||||
crossAxisSpacing: crossAxisSpacing,
|
||||
childAspectRatio: 1.0,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
if (widget.colors.length > index) {
|
||||
final isSelected = widget.colors[index] == widget.initailColor;
|
||||
return ColorItem(
|
||||
color: Color(widget.colors[index]),
|
||||
onPressed: widget.onColorChanged,
|
||||
isSelected: isSelected,
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
childCount: widget.colors.length,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ColorItem extends StatelessWidget {
|
||||
final Function(Color?) onPressed;
|
||||
final bool isSelected;
|
||||
final Color color;
|
||||
const ColorItem({
|
||||
Key? key,
|
||||
required this.color,
|
||||
required this.onPressed,
|
||||
this.isSelected = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!isSelected) {
|
||||
return RawMaterialButton(
|
||||
onPressed: () {
|
||||
onPressed(color);
|
||||
},
|
||||
elevation: 0,
|
||||
hoverElevation: 0.6,
|
||||
fillColor: color,
|
||||
shape: const CircleBorder(),
|
||||
);
|
||||
} else {
|
||||
return RawMaterialButton(
|
||||
shape: const CircleBorder(side: BorderSide(color: Colors.white, width: 8)) +
|
||||
CircleBorder(side: BorderSide(color: color, width: 4)),
|
||||
onPressed: () {
|
||||
if (isSelected) {
|
||||
onPressed(null);
|
||||
} else {
|
||||
onPressed(color);
|
||||
}
|
||||
},
|
||||
elevation: 1.0,
|
||||
hoverElevation: 0.6,
|
||||
fillColor: color,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,18 +1,14 @@
|
||||
import 'package:editor/flutter_quill.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:editor/models/documents/style.dart';
|
||||
import 'package:flutter_quill/models/documents/style.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'toolbar_icon_button.dart';
|
||||
|
||||
class FlowyHeaderStyleButton extends StatefulWidget {
|
||||
const FlowyHeaderStyleButton({
|
||||
required this.controller,
|
||||
this.iconSize = kDefaultIconSize,
|
||||
this.iconSize = defaultIconSize,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
|
@ -1,13 +1,13 @@
|
||||
import 'package:editor/flutter_quill.dart';
|
||||
import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
import 'toolbar_icon_button.dart';
|
||||
|
||||
class FlowyImageButton extends StatelessWidget {
|
||||
const FlowyImageButton({
|
||||
required this.controller,
|
||||
this.iconSize = kDefaultIconSize,
|
||||
this.iconSize = defaultIconSize,
|
||||
this.onImagePickCallback,
|
||||
this.fillColor,
|
||||
this.filePickImpl,
|
||||
@ -41,43 +41,43 @@ class FlowyImageButton extends StatelessWidget {
|
||||
}
|
||||
|
||||
Future<void> _onPressedHandler(BuildContext context) async {
|
||||
if (onImagePickCallback != null) {
|
||||
final selector = mediaPickSettingSelector ?? ImageVideoUtils.selectMediaPickSetting;
|
||||
final source = await selector(context);
|
||||
if (source != null) {
|
||||
if (source == MediaPickSetting.Gallery) {
|
||||
_pickImage(context);
|
||||
} else {
|
||||
_typeLink(context);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_typeLink(context);
|
||||
}
|
||||
// if (onImagePickCallback != null) {
|
||||
// final selector = mediaPickSettingSelector ?? ImageVideoUtils.selectMediaPickSetting;
|
||||
// final source = await selector(context);
|
||||
// if (source != null) {
|
||||
// if (source == MediaPickSetting.Gallery) {
|
||||
// _pickImage(context);
|
||||
// } else {
|
||||
// _typeLink(context);
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// _typeLink(context);
|
||||
// }
|
||||
}
|
||||
|
||||
void _pickImage(BuildContext context) => ImageVideoUtils.handleImageButtonTap(
|
||||
context,
|
||||
controller,
|
||||
ImageSource.gallery,
|
||||
onImagePickCallback!,
|
||||
filePickImpl: filePickImpl,
|
||||
webImagePickImpl: webImagePickImpl,
|
||||
);
|
||||
// void _pickImage(BuildContext context) => ImageVideoUtils.handleImageButtonTap(
|
||||
// context,
|
||||
// controller,
|
||||
// ImageSource.gallery,
|
||||
// onImagePickCallback!,
|
||||
// filePickImpl: filePickImpl,
|
||||
// webImagePickImpl: webImagePickImpl,
|
||||
// );
|
||||
|
||||
void _typeLink(BuildContext context) {
|
||||
// showDialog<String>(
|
||||
// context: context,
|
||||
// builder: (_) => const LinkDialog(),
|
||||
// ).then(_linkSubmitted);
|
||||
}
|
||||
TextFieldDialog(
|
||||
title: 'URL',
|
||||
value: "",
|
||||
confirm: (newValue) {
|
||||
if (newValue.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final index = controller.selection.baseOffset;
|
||||
final length = controller.selection.extentOffset - index;
|
||||
|
||||
void _linkSubmitted(String? value) {
|
||||
if (value != null && value.isNotEmpty) {
|
||||
final index = controller.selection.baseOffset;
|
||||
final length = controller.selection.extentOffset - index;
|
||||
|
||||
controller.replaceText(index, length, BlockEmbed.image(value), null);
|
||||
}
|
||||
controller.replaceText(index, length, BlockEmbed.image(newValue), null);
|
||||
},
|
||||
).show(context);
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,17 @@
|
||||
import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:editor/flutter_quill.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'toolbar_icon_button.dart';
|
||||
|
||||
class FlowyLinkStyleButton extends StatefulWidget {
|
||||
const FlowyLinkStyleButton({
|
||||
required this.controller,
|
||||
this.iconSize = kDefaultIconSize,
|
||||
this.iconSize = defaultIconSize,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import 'package:editor/flutter_quill.dart';
|
||||
import 'package:editor/models/documents/style.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:flutter_quill/models/documents/style.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'toolbar_icon_button.dart';
|
||||
@ -14,7 +14,7 @@ class FlowyToggleStyleButton extends StatefulWidget {
|
||||
required this.attribute,
|
||||
required this.normalIcon,
|
||||
required this.controller,
|
||||
this.iconSize = kDefaultIconSize,
|
||||
this.iconSize = defaultIconSize,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
|
@ -1,14 +1,16 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:editor/flutter_quill.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'check_button.dart';
|
||||
import 'color_picker.dart';
|
||||
import 'header_button.dart';
|
||||
import 'image_button.dart';
|
||||
import 'link_button.dart';
|
||||
import 'toggle_button.dart';
|
||||
import 'toolbar_icon_button.dart';
|
||||
|
||||
class EditorToolbar extends StatelessWidget implements PreferredSizeWidget {
|
||||
final List<Widget> children;
|
||||
@ -36,7 +38,7 @@ class EditorToolbar extends StatelessWidget implements PreferredSizeWidget {
|
||||
|
||||
factory EditorToolbar.basic({
|
||||
required QuillController controller,
|
||||
double toolbarIconSize = kDefaultIconSize,
|
||||
double toolbarIconSize = defaultIconSize,
|
||||
OnImagePickCallback? onImagePickCallback,
|
||||
OnVideoPickCallback? onVideoPickCallback,
|
||||
MediaPickSettingSelector? mediaPickSettingSelector,
|
||||
@ -85,7 +87,7 @@ class EditorToolbar extends StatelessWidget implements PreferredSizeWidget {
|
||||
iconSize: toolbarIconSize,
|
||||
controller: controller,
|
||||
),
|
||||
ColorButton(
|
||||
FlowyColorButton(
|
||||
icon: Icons.format_color_fill,
|
||||
iconSize: toolbarIconSize,
|
||||
controller: controller,
|
||||
@ -175,14 +177,14 @@ class _ToolbarButtonListState extends State<ToolbarButtonList> with WidgetsBindi
|
||||
return LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
List<Widget> children = [];
|
||||
double width = (widget.buttons.length + 2) * kDefaultIconSize * kIconButtonFactor;
|
||||
double width = (widget.buttons.length + 2) * defaultIconSize * kIconButtonFactor;
|
||||
final isFit = constraints.maxWidth > width;
|
||||
if (!isFit) {
|
||||
children.add(_buildLeftArrow());
|
||||
width = width + 18;
|
||||
}
|
||||
|
||||
children.add(_buildScrollableList(constraints));
|
||||
children.add(_buildScrollableList(constraints, isFit));
|
||||
|
||||
if (!isFit) {
|
||||
children.add(_buildRightArrow());
|
||||
@ -228,31 +230,38 @@ class _ToolbarButtonListState extends State<ToolbarButtonList> with WidgetsBindi
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildScrollableList(BoxConstraints constraints) {
|
||||
return ScrollConfiguration(
|
||||
// Remove the glowing effect, as we already have the arrow indicators
|
||||
behavior: _NoGlowBehavior(),
|
||||
// The CustomScrollView is necessary so that the children are not
|
||||
// stretched to the height of the toolbar, https://bit.ly/3uC3bjI
|
||||
child: Expanded(
|
||||
child: CustomScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
controller: _controller,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
slivers: [
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
return widget.buttons[index];
|
||||
},
|
||||
childCount: widget.buttons.length,
|
||||
addAutomaticKeepAlives: false,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
// [[sliver: https://medium.com/flutter/slivers-demystified-6ff68ab0296f]]
|
||||
Widget _buildScrollableList(BoxConstraints constraints, bool isFit) {
|
||||
Widget child = Expanded(
|
||||
child: CustomScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
controller: _controller,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
slivers: [
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
return widget.buttons[index];
|
||||
},
|
||||
childCount: widget.buttons.length,
|
||||
addAutomaticKeepAlives: false,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (!isFit) {
|
||||
child = ScrollConfiguration(
|
||||
// Remove the glowing effect, as we already have the arrow indicators
|
||||
behavior: _NoGlowBehavior(),
|
||||
// The CustomScrollView is necessary so that the children are not
|
||||
// stretched to the height of the toolbar, https://bit.ly/3uC3bjI
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
Widget _buildRightArrow() {
|
||||
|
@ -4,6 +4,8 @@ import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
const double defaultIconSize = 18;
|
||||
|
||||
class ToolbarIconButton extends StatelessWidget {
|
||||
final double width;
|
||||
final VoidCallback? onPressed;
|
||||
|
@ -54,24 +54,22 @@ class _CreateTextFieldDialog extends State<TextFieldDialog> {
|
||||
],
|
||||
FlowyFormTextInput(
|
||||
hintText: widget.value,
|
||||
textStyle: const TextStyle(fontSize: 28, fontWeight: FontWeight.w400),
|
||||
autoFocus: true,
|
||||
onChanged: (text) {
|
||||
newValue = text;
|
||||
},
|
||||
),
|
||||
SizedBox(height: Insets.l),
|
||||
SizedBox(
|
||||
height: 40,
|
||||
child: OkCancelButton(
|
||||
onOkPressed: () {
|
||||
widget.confirm(newValue);
|
||||
},
|
||||
onCancelPressed: () {
|
||||
if (widget.cancel != null) {
|
||||
widget.cancel!();
|
||||
}
|
||||
},
|
||||
),
|
||||
const VSpace(10),
|
||||
OkCancelButton(
|
||||
onOkPressed: () {
|
||||
widget.confirm(newValue);
|
||||
},
|
||||
onCancelPressed: () {
|
||||
if (widget.cancel != null) {
|
||||
widget.cancel!();
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
@ -140,29 +138,32 @@ class OkCancelButton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
if (onCancelPressed != null)
|
||||
SecondaryTextButton(
|
||||
cancelTitle ?? S.BTN_CANCEL,
|
||||
onPressed: () {
|
||||
onCancelPressed!();
|
||||
AppGlobals.nav.pop();
|
||||
},
|
||||
bigMode: true,
|
||||
),
|
||||
HSpace(Insets.m),
|
||||
if (onOkPressed != null)
|
||||
PrimaryTextButton(
|
||||
okTitle ?? S.BTN_OK,
|
||||
onPressed: () {
|
||||
onOkPressed!();
|
||||
AppGlobals.nav.pop();
|
||||
},
|
||||
bigMode: true,
|
||||
),
|
||||
],
|
||||
return SizedBox(
|
||||
height: 48,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
if (onCancelPressed != null)
|
||||
SecondaryTextButton(
|
||||
cancelTitle ?? S.BTN_CANCEL,
|
||||
onPressed: () {
|
||||
onCancelPressed!();
|
||||
AppGlobals.nav.pop();
|
||||
},
|
||||
bigMode: true,
|
||||
),
|
||||
HSpace(Insets.m),
|
||||
if (onOkPressed != null)
|
||||
PrimaryTextButton(
|
||||
okTitle ?? S.BTN_OK,
|
||||
onPressed: () {
|
||||
onOkPressed!();
|
||||
AppGlobals.nav.pop();
|
||||
},
|
||||
bigMode: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,10 @@ class FlowyPoppuWindow extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return child;
|
||||
return Material(
|
||||
child: child,
|
||||
type: MaterialType.transparency,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> show(
|
||||
@ -27,7 +30,7 @@ class FlowyPoppuWindow extends StatelessWidget {
|
||||
anchorPosition: Offset(-size.width / 2.0, -size.height / 2.0),
|
||||
anchorSize: window.frame.size,
|
||||
anchorDirection: AnchorDirection.center,
|
||||
style: FlowyOverlayStyle(blur: true),
|
||||
style: FlowyOverlayStyle(blur: false),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -41,14 +44,11 @@ class PopupTextField extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
child: RoundedInputField(
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||
hintText: '',
|
||||
normalBorderColor: const Color(0xffbdbdbd),
|
||||
onChanged: textDidChange,
|
||||
),
|
||||
type: MaterialType.transparency,
|
||||
return RoundedInputField(
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||
hintText: '',
|
||||
normalBorderColor: const Color(0xffbdbdbd),
|
||||
onChanged: textDidChange,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,6 @@
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import editor
|
||||
import flowy_editor
|
||||
import flowy_infra_ui
|
||||
import flowy_sdk
|
||||
@ -14,7 +13,6 @@ import url_launcher_macos
|
||||
import window_size
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
EditorPlugin.register(with: registry.registrar(forPlugin: "EditorPlugin"))
|
||||
FlowyEditorPlugin.register(with: registry.registrar(forPlugin: "FlowyEditorPlugin"))
|
||||
FlowyInfraUIPlugin.register(with: registry.registrar(forPlugin: "FlowyInfraUIPlugin"))
|
||||
FlowySdkPlugin.register(with: registry.registrar(forPlugin: "FlowySdkPlugin"))
|
||||
|
@ -1,6 +1,4 @@
|
||||
PODS:
|
||||
- editor (0.0.1):
|
||||
- FlutterMacOS
|
||||
- flowy_editor (0.0.1):
|
||||
- FlutterMacOS
|
||||
- flowy_infra_ui (0.0.1):
|
||||
@ -16,7 +14,6 @@ PODS:
|
||||
- FlutterMacOS
|
||||
|
||||
DEPENDENCIES:
|
||||
- editor (from `Flutter/ephemeral/.symlinks/plugins/editor/macos`)
|
||||
- flowy_editor (from `Flutter/ephemeral/.symlinks/plugins/flowy_editor/macos`)
|
||||
- flowy_infra_ui (from `Flutter/ephemeral/.symlinks/plugins/flowy_infra_ui/macos`)
|
||||
- flowy_sdk (from `Flutter/ephemeral/.symlinks/plugins/flowy_sdk/macos`)
|
||||
@ -26,8 +23,6 @@ DEPENDENCIES:
|
||||
- window_size (from `Flutter/ephemeral/.symlinks/plugins/window_size/macos`)
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
editor:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/editor/macos
|
||||
flowy_editor:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/flowy_editor/macos
|
||||
flowy_infra_ui:
|
||||
@ -44,7 +39,6 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/window_size/macos
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
editor: 380351c0334fbeb0e431e4e49629c9e2d925b66d
|
||||
flowy_editor: 26060a984848e6afac1f6a4455511f4114119d8d
|
||||
flowy_infra_ui: 9d5021b1610fe0476eb1191bf7cd41c4a4138d8f
|
||||
flowy_sdk: c302ac0a22dea596db0df8073b9637b2bf2ff6fd
|
||||
|
29
app_flowy/packages/editor/.gitignore
vendored
@ -1,29 +0,0 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||
/pubspec.lock
|
||||
**/doc/api/
|
||||
.dart_tool/
|
||||
.packages
|
||||
build/
|
@ -1,10 +0,0 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: 4b330ddbedab445481cc73d50a4695b9154b4e4f
|
||||
channel: dev
|
||||
|
||||
project_type: plugin
|
@ -1,3 +0,0 @@
|
||||
## 0.0.1
|
||||
|
||||
* TODO: Describe initial release.
|
@ -1 +0,0 @@
|
||||
TODO: Add your license here.
|
@ -1,15 +0,0 @@
|
||||
# editor
|
||||
|
||||
A new flutter plugin project.
|
||||
|
||||
## Getting Started
|
||||
|
||||
This project is a starting point for a Flutter
|
||||
[plug-in package](https://flutter.dev/developing-packages/),
|
||||
a specialized package that includes platform-specific implementation code for
|
||||
Android and/or iOS.
|
||||
|
||||
For help getting started with Flutter, view our
|
||||
[online documentation](https://flutter.dev/docs), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
||||
|
@ -1,4 +0,0 @@
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
46
app_flowy/packages/editor/example/.gitignore
vendored
@ -1,46 +0,0 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.packages
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
|
||||
# Web related
|
||||
lib/generated_plugin_registrant.dart
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
@ -1,10 +0,0 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: 4b330ddbedab445481cc73d50a4695b9154b4e4f
|
||||
channel: dev
|
||||
|
||||
project_type: app
|
@ -1,16 +0,0 @@
|
||||
# editor_example
|
||||
|
||||
Demonstrates how to use the editor plugin.
|
||||
|
||||
## Getting Started
|
||||
|
||||
This project is a starting point for a Flutter application.
|
||||
|
||||
A few resources to get you started if this is your first Flutter project:
|
||||
|
||||
- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)
|
||||
- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)
|
||||
|
||||
For help getting started with Flutter, view our
|
||||
[online documentation](https://flutter.dev/docs), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
@ -1,29 +0,0 @@
|
||||
# This file configures the analyzer, which statically analyzes Dart code to
|
||||
# check for errors, warnings, and lints.
|
||||
#
|
||||
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||
# invoked from the command line by running `flutter analyze`.
|
||||
|
||||
# The following line activates a set of recommended lints for Flutter apps,
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||
# included above or to enable additional rules. A list of all available lints
|
||||
# and their documentation is published at
|
||||
# https://dart-lang.github.io/linter/lints/index.html.
|
||||
#
|
||||
# Instead of disabling a lint rule for the entire project in the
|
||||
# section below, it can also be suppressed for a single line of code
|
||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||
# producing the lint.
|
||||
rules:
|
||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
@ -1,33 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatefulWidget {
|
||||
const MyApp({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<MyApp> createState() => _MyAppState();
|
||||
}
|
||||
|
||||
class _MyAppState extends State<MyApp> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
home: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Plugin example app'),
|
||||
),
|
||||
body: const Center(
|
||||
child: Text(''),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
# Flutter-related
|
||||
**/Flutter/ephemeral/
|
||||
**/Pods/
|
||||
|
||||
# Xcode-related
|
||||
**/dgph
|
||||
**/xcuserdata/
|
@ -1,2 +0,0 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
#include "ephemeral/Flutter-Generated.xcconfig"
|
@ -1,2 +0,0 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "ephemeral/Flutter-Generated.xcconfig"
|
@ -1,14 +0,0 @@
|
||||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import editor
|
||||
import url_launcher_macos
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
EditorPlugin.register(with: registry.registrar(forPlugin: "EditorPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
platform :osx, '10.11'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
project 'Runner', {
|
||||
'Debug' => :debug,
|
||||
'Profile' => :release,
|
||||
'Release' => :release,
|
||||
}
|
||||
|
||||
def flutter_root
|
||||
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
|
||||
unless File.exist?(generated_xcode_build_settings_path)
|
||||
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
|
||||
end
|
||||
|
||||
File.foreach(generated_xcode_build_settings_path) do |line|
|
||||
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
||||
return matches[1].strip if matches
|
||||
end
|
||||
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
|
||||
end
|
||||
|
||||
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
||||
|
||||
flutter_macos_podfile_setup
|
||||
|
||||
target 'Runner' do
|
||||
use_frameworks!
|
||||
use_modular_headers!
|
||||
|
||||
flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_macos_build_settings(target)
|
||||
end
|
||||
end
|
@ -1,572 +0,0 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 51;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXAggregateTarget section */
|
||||
33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {
|
||||
isa = PBXAggregateTarget;
|
||||
buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */;
|
||||
buildPhases = (
|
||||
33CC111E2044C6BF0003C045 /* ShellScript */,
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = "Flutter Assemble";
|
||||
productName = FLX;
|
||||
};
|
||||
/* End PBXAggregateTarget section */
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
|
||||
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
|
||||
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
|
||||
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
|
||||
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 33CC111A2044C6BA0003C045;
|
||||
remoteInfo = FLX;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
33CC110E2044A8840003C045 /* Bundle Framework */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
);
|
||||
name = "Bundle Framework";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
|
||||
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
|
||||
33CC10ED2044A3C60003C045 /* editor_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "editor_example.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
|
||||
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
|
||||
33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; };
|
||||
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; };
|
||||
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; };
|
||||
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; };
|
||||
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; };
|
||||
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
|
||||
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
|
||||
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
33CC10EA2044A3C60003C045 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
33BA886A226E78AF003329D5 /* Configs */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33E5194F232828860026EE4D /* AppInfo.xcconfig */,
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||
333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
|
||||
);
|
||||
path = Configs;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33CC10E42044A3C60003C045 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33FAB671232836740065AC1E /* Runner */,
|
||||
33CEB47122A05771004F2AC0 /* Flutter */,
|
||||
33CC10EE2044A3C60003C045 /* Products */,
|
||||
D73912EC22F37F3D000D13A0 /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33CC10EE2044A3C60003C045 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33CC10ED2044A3C60003C045 /* editor_example.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33CC11242044D66E0003C045 /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33CC10F22044A3C60003C045 /* Assets.xcassets */,
|
||||
33CC10F42044A3C60003C045 /* MainMenu.xib */,
|
||||
33CC10F72044A3C60003C045 /* Info.plist */,
|
||||
);
|
||||
name = Resources;
|
||||
path = ..;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33CEB47122A05771004F2AC0 /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
|
||||
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
|
||||
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
|
||||
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,
|
||||
);
|
||||
path = Flutter;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33FAB671232836740065AC1E /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33CC10F02044A3C60003C045 /* AppDelegate.swift */,
|
||||
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
|
||||
33E51913231747F40026EE4D /* DebugProfile.entitlements */,
|
||||
33E51914231749380026EE4D /* Release.entitlements */,
|
||||
33CC11242044D66E0003C045 /* Resources */,
|
||||
33BA886A226E78AF003329D5 /* Configs */,
|
||||
);
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
33CC10EC2044A3C60003C045 /* Runner */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
33CC10E92044A3C60003C045 /* Sources */,
|
||||
33CC10EA2044A3C60003C045 /* Frameworks */,
|
||||
33CC10EB2044A3C60003C045 /* Resources */,
|
||||
33CC110E2044A8840003C045 /* Bundle Framework */,
|
||||
3399D490228B24CF009A79C7 /* ShellScript */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
33CC11202044C79F0003C045 /* PBXTargetDependency */,
|
||||
);
|
||||
name = Runner;
|
||||
productName = Runner;
|
||||
productReference = 33CC10ED2044A3C60003C045 /* editor_example.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
33CC10E52044A3C60003C045 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0920;
|
||||
LastUpgradeCheck = 1300;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
33CC10EC2044A3C60003C045 = {
|
||||
CreatedOnToolsVersion = 9.2;
|
||||
LastSwiftMigration = 1100;
|
||||
ProvisioningStyle = Automatic;
|
||||
SystemCapabilities = {
|
||||
com.apple.Sandbox = {
|
||||
enabled = 1;
|
||||
};
|
||||
};
|
||||
};
|
||||
33CC111A2044C6BA0003C045 = {
|
||||
CreatedOnToolsVersion = 9.2;
|
||||
ProvisioningStyle = Manual;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */;
|
||||
compatibilityVersion = "Xcode 9.3";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 33CC10E42044A3C60003C045;
|
||||
productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
33CC10EC2044A3C60003C045 /* Runner */,
|
||||
33CC111A2044C6BA0003C045 /* Flutter Assemble */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
33CC10EB2044A3C60003C045 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
|
||||
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
3399D490228B24CF009A79C7 /* ShellScript */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
|
||||
};
|
||||
33CC111E2044C6BF0003C045 /* ShellScript */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
Flutter/ephemeral/FlutterInputs.xcfilelist,
|
||||
);
|
||||
inputPaths = (
|
||||
Flutter/ephemeral/tripwire,
|
||||
);
|
||||
outputFileListPaths = (
|
||||
Flutter/ephemeral/FlutterOutputs.xcfilelist,
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
33CC10E92044A3C60003C045 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
|
||||
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
|
||||
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
33CC11202044C79F0003C045 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
|
||||
targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
33CC10F42044A3C60003C045 /* MainMenu.xib */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
33CC10F52044A3C60003C045 /* Base */,
|
||||
);
|
||||
name = MainMenu.xib;
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
338D0CE9231458BD00FA5F75 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.11;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
338D0CEA231458BD00FA5F75 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
338D0CEB231458BD00FA5F75 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
33CC10F92044A3C60003C045 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.11;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
33CC10FA2044A3C60003C045 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.11;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
33CC10FC2044A3C60003C045 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
33CC10FD2044A3C60003C045 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
33CC111C2044C6BA0003C045 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
33CC111D2044C6BA0003C045 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
33CC10F92044A3C60003C045 /* Debug */,
|
||||
33CC10FA2044A3C60003C045 /* Release */,
|
||||
338D0CE9231458BD00FA5F75 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
33CC10FC2044A3C60003C045 /* Debug */,
|
||||
33CC10FD2044A3C60003C045 /* Release */,
|
||||
338D0CEA231458BD00FA5F75 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
33CC111C2044C6BA0003C045 /* Debug */,
|
||||
33CC111D2044C6BA0003C045 /* Release */,
|
||||
338D0CEB231458BD00FA5F75 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 33CC10E52044A3C60003C045 /* Project object */;
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
@ -1,87 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1300"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "editor_example.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "editor_example.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "editor_example.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Profile"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "editor_example.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
@ -1,9 +0,0 @@
|
||||
import Cocoa
|
||||
import FlutterMacOS
|
||||
|
||||
@NSApplicationMain
|
||||
class AppDelegate: FlutterAppDelegate {
|
||||
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_16.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_32.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "32x32",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_32.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "32x32",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_64.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "128x128",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_128.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "128x128",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_256.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "256x256",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_256.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "256x256",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_512.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "512x512",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_512.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "512x512",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_1024.png",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 1.8 KiB |
@ -1,339 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
|
||||
<connections>
|
||||
<outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Runner" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="applicationMenu" destination="uQy-DD-JDr" id="XBo-yE-nKs"/>
|
||||
<outlet property="mainFlutterWindow" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
||||
<menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
|
||||
<items>
|
||||
<menuItem title="APP_NAME" id="1Xt-HY-uBw">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="APP_NAME" systemMenu="apple" id="uQy-DD-JDr">
|
||||
<items>
|
||||
<menuItem title="About APP_NAME" id="5kV-Vb-QxS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
|
||||
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
|
||||
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
|
||||
<menuItem title="Services" id="NMo-om-nkz">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
|
||||
<menuItem title="Hide APP_NAME" keyEquivalent="h" id="Olw-nP-bQN">
|
||||
<connections>
|
||||
<action selector="hide:" target="-1" id="PnN-Uc-m68"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Show All" id="Kd2-mp-pUS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
|
||||
<menuItem title="Quit APP_NAME" keyEquivalent="q" id="4sb-4s-VLi">
|
||||
<connections>
|
||||
<action selector="terminate:" target="-1" id="Te7-pn-YzF"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Edit" id="5QF-Oa-p0T">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Edit" id="W48-6f-4Dl">
|
||||
<items>
|
||||
<menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
|
||||
<connections>
|
||||
<action selector="undo:" target="-1" id="M6e-cu-g7V"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
|
||||
<connections>
|
||||
<action selector="redo:" target="-1" id="oIA-Rs-6OD"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
|
||||
<menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
|
||||
<connections>
|
||||
<action selector="cut:" target="-1" id="YJe-68-I9s"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
|
||||
<connections>
|
||||
<action selector="copy:" target="-1" id="G1f-GL-Joy"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
|
||||
<connections>
|
||||
<action selector="paste:" target="-1" id="UvS-8e-Qdg"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="pasteAsPlainText:" target="-1" id="cEh-KX-wJQ"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Delete" id="pa3-QI-u2k">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="delete:" target="-1" id="0Mk-Ml-PaM"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
|
||||
<connections>
|
||||
<action selector="selectAll:" target="-1" id="VNm-Mi-diN"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
|
||||
<menuItem title="Find" id="4EN-yA-p0u">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Find" id="1b7-l0-nxx">
|
||||
<items>
|
||||
<menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="-1" id="cD7-Qs-BN4"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="-1" id="WD3-Gg-5AJ"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="-1" id="NDo-RZ-v9R"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="-1" id="HOh-sY-3ay"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="-1" id="U76-nv-p5D"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
|
||||
<connections>
|
||||
<action selector="centerSelectionInVisibleArea:" target="-1" id="IOG-6D-g5B"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
|
||||
<items>
|
||||
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
|
||||
<connections>
|
||||
<action selector="showGuessPanel:" target="-1" id="vFj-Ks-hy3"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
|
||||
<connections>
|
||||
<action selector="checkSpelling:" target="-1" id="fz7-VC-reM"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
|
||||
<menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleContinuousSpellChecking:" target="-1" id="7w6-Qz-0kB"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleGrammarChecking:" target="-1" id="muD-Qn-j4w"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticSpellingCorrection:" target="-1" id="2lM-Qi-WAP"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Substitutions" id="9ic-FL-obx">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
|
||||
<items>
|
||||
<menuItem title="Show Substitutions" id="z6F-FW-3nz">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="orderFrontSubstitutionsPanel:" target="-1" id="oku-mr-iSq"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
|
||||
<menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleSmartInsertDelete:" target="-1" id="3IJ-Se-DZD"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Quotes" id="hQb-2v-fYv">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="ptq-xd-QOA"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Dashes" id="rgM-f4-ycn">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticDashSubstitution:" target="-1" id="oCt-pO-9gS"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Links" id="cwL-P1-jid">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticLinkDetection:" target="-1" id="Gip-E3-Fov"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Data Detectors" id="tRr-pd-1PS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticDataDetection:" target="-1" id="R1I-Nq-Kbl"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Text Replacement" id="HFQ-gK-NFA">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticTextReplacement:" target="-1" id="DvP-Fe-Py6"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Transformations" id="2oI-Rn-ZJC">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Transformations" id="c8a-y6-VQd">
|
||||
<items>
|
||||
<menuItem title="Make Upper Case" id="vmV-6d-7jI">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="uppercaseWord:" target="-1" id="sPh-Tk-edu"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Make Lower Case" id="d9M-CD-aMd">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="lowercaseWord:" target="-1" id="iUZ-b5-hil"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Capitalize" id="UEZ-Bs-lqG">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="capitalizeWord:" target="-1" id="26H-TL-nsh"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Speech" id="xrE-MZ-jX0">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Speech" id="3rS-ZA-NoH">
|
||||
<items>
|
||||
<menuItem title="Start Speaking" id="Ynk-f8-cLZ">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="startSpeaking:" target="-1" id="654-Ng-kyl"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Stop Speaking" id="Oyz-dy-DGm">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="stopSpeaking:" target="-1" id="dX8-6p-jy9"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="View" id="H8h-7b-M4v">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="View" id="HyV-fh-RgO">
|
||||
<items>
|
||||
<menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
|
||||
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="toggleFullScreen:" target="-1" id="dU3-MA-1Rq"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Window" id="aUF-d1-5bR">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
|
||||
<items>
|
||||
<menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
|
||||
<connections>
|
||||
<action selector="performMiniaturize:" target="-1" id="VwT-WD-YPe"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Zoom" id="R4o-n2-Eq4">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="performZoom:" target="-1" id="DIl-cC-cCs"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
|
||||
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
<point key="canvasLocation" x="142" y="-258"/>
|
||||
</menu>
|
||||
<window title="APP_NAME" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="MainFlutterWindow" customModule="Runner" customModuleProvider="target">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||
<rect key="contentRect" x="335" y="390" width="800" height="600"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1577"/>
|
||||
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</view>
|
||||
</window>
|
||||
</objects>
|
||||
</document>
|
@ -1,14 +0,0 @@
|
||||
// Application-level settings for the Runner target.
|
||||
//
|
||||
// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the
|
||||
// future. If not, the values below would default to using the project name when this becomes a
|
||||
// 'flutter create' template.
|
||||
|
||||
// The application's name. By default this is also the title of the Flutter window.
|
||||
PRODUCT_NAME = editor_example
|
||||
|
||||
// The application's bundle identifier
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.editorExample
|
||||
|
||||
// The copyright displayed in application information
|
||||
PRODUCT_COPYRIGHT = Copyright © 2021 com.example. All rights reserved.
|
@ -1,2 +0,0 @@
|
||||
#include "../../Flutter/Flutter-Debug.xcconfig"
|
||||
#include "Warnings.xcconfig"
|
@ -1,2 +0,0 @@
|
||||
#include "../../Flutter/Flutter-Release.xcconfig"
|
||||
#include "Warnings.xcconfig"
|
@ -1,13 +0,0 @@
|
||||
WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES
|
||||
CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
|
||||
CLANG_WARN_PRAGMA_PACK = YES
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES
|
||||
CLANG_WARN_COMMA = YES
|
||||
GCC_WARN_STRICT_SELECTOR_MATCH = YES
|
||||
CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES
|
||||
GCC_WARN_SHADOW = YES
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES
|
@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
@ -1,32 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string></string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>$(PRODUCT_COPYRIGHT)</string>
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
</dict>
|
||||
</plist>
|
@ -1,15 +0,0 @@
|
||||
import Cocoa
|
||||
import FlutterMacOS
|
||||
|
||||
class MainFlutterWindow: NSWindow {
|
||||
override func awakeFromNib() {
|
||||
let flutterViewController = FlutterViewController.init()
|
||||
let windowFrame = self.frame
|
||||
self.contentViewController = flutterViewController
|
||||
self.setFrame(windowFrame, display: true)
|
||||
|
||||
RegisterGeneratedPlugins(registry: flutterViewController)
|
||||
|
||||
super.awakeFromNib()
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
@ -1,404 +0,0 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.8.2"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
charcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: charcode
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.15.0"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cross_file
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.1+5"
|
||||
csslib:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: csslib
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.17.1"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cupertino_icons
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
diff_match_patch:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: diff_match_patch
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.1"
|
||||
editor:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: ".."
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_colorpicker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_colorpicker
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
flutter_inappwebview:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_inappwebview
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.3.2"
|
||||
flutter_keyboard_visibility:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_keyboard_visibility
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.0.3"
|
||||
flutter_keyboard_visibility_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_keyboard_visibility_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
flutter_keyboard_visibility_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_keyboard_visibility_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_plugin_android_lifecycle
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
html:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: html
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.15.0"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.13.4"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
image_picker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.8.4+2"
|
||||
image_picker_for_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_for_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
image_picker_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.6.3"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.12.11"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.7.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.0"
|
||||
pedantic:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pedantic
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.11.1"
|
||||
photo_view:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: photo_view
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.12.0"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
quiver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: quiver
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.99"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.1"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
string_validator:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_validator
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.0"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.3"
|
||||
tuple:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: tuple
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
url_launcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.0.12"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.4"
|
||||
url_launcher_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.4"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
video_player:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.5"
|
||||
video_player_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.2.0"
|
||||
video_player_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.4"
|
||||
youtube_player_flutter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: youtube_player_flutter
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "8.0.0"
|
||||
sdks:
|
||||
dart: ">=2.15.0-116.0.dev <3.0.0"
|
||||
flutter: ">=2.5.0"
|
@ -1,84 +0,0 @@
|
||||
name: editor_example
|
||||
description: Demonstrates how to use the editor plugin.
|
||||
|
||||
# The following line prevents the package from being accidentally published to
|
||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
|
||||
environment:
|
||||
sdk: ">=2.15.0-116.0.dev <3.0.0"
|
||||
|
||||
# Dependencies specify other packages that your package needs in order to work.
|
||||
# To automatically upgrade your package dependencies to the latest versions
|
||||
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
||||
# dependencies can be manually updated by changing the version numbers below to
|
||||
# the latest version available on pub.dev. To see which dependencies have newer
|
||||
# versions available, run `flutter pub outdated`.
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
editor:
|
||||
# When depending on this package from a real application you should use:
|
||||
# editor: ^x.y.z
|
||||
# See https://dart.dev/tools/pub/dependencies#version-constraints
|
||||
# The example app is bundled with the plugin so we use a path dependency on
|
||||
# the parent directory to use the current plugin's version.
|
||||
path: ../
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
# The "flutter_lints" package below contains a set of recommended lints to
|
||||
# encourage good coding practices. The lint set provided by the package is
|
||||
# activated in the `analysis_options.yaml` file located at the root of your
|
||||
# package. See that file for information about deactivating specific lint
|
||||
# rules and activating additional ones.
|
||||
flutter_lints: ^1.0.0
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
# The following section is specific to Flutter.
|
||||
flutter:
|
||||
|
||||
# The following line ensures that the Material Icons font is
|
||||
# included with your application, so that you can use the icons in
|
||||
# the material Icons class.
|
||||
uses-material-design: true
|
||||
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/assets-and-images/#resolution-aware.
|
||||
|
||||
# For details regarding adding assets from package dependencies, see
|
||||
# https://flutter.dev/assets-and-images/#from-packages
|
||||
|
||||
# To add custom fonts to your application, add a fonts section here,
|
||||
# in this "flutter" section. Each entry in this list should have a
|
||||
# "family" key with the font family name, and a "fonts" key with a
|
||||
# list giving the asset and other descriptors for the font. For
|
||||
# example:
|
||||
# fonts:
|
||||
# - family: Schyler
|
||||
# fonts:
|
||||
# - asset: fonts/Schyler-Regular.ttf
|
||||
# - asset: fonts/Schyler-Italic.ttf
|
||||
# style: italic
|
||||
# - family: Trajan Pro
|
||||
# fonts:
|
||||
# - asset: fonts/TrajanPro.ttf
|
||||
# - asset: fonts/TrajanPro_Bold.ttf
|
||||
# weight: 700
|
||||
#
|
||||
# For details regarding fonts from package dependencies,
|
||||
# see https://flutter.dev/custom-fonts/#from-packages
|
@ -1,11 +0,0 @@
|
||||
// This is a basic Flutter widget test.
|
||||
//
|
||||
// To perform an interaction with a widget in your test, use the WidgetTester
|
||||
// utility that Flutter provides. For example, you can send tap and scroll
|
||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
||||
// tree, read text, and verify that the values of widget properties are correct.
|
||||
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {}
|
@ -1,11 +0,0 @@
|
||||
library flutter_quill;
|
||||
|
||||
export 'src/models/documents/attribute.dart';
|
||||
export 'src/models/documents/document.dart';
|
||||
export 'src/models/documents/nodes/embed.dart';
|
||||
export 'src/models/documents/nodes/leaf.dart';
|
||||
export 'src/models/quill_delta.dart';
|
||||
export 'src/widgets/controller.dart';
|
||||
export 'src/widgets/default_styles.dart';
|
||||
export 'src/widgets/editor.dart';
|
||||
export 'src/widgets/toolbar.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/models/documents/attribute.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/models/documents/document.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/models/documents/history.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/models/documents/nodes/block.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/models/documents/nodes/container.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/models/documents/nodes/embed.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/models/documents/nodes/leaf.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/models/documents/nodes/line.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/models/documents/nodes/node.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/models/documents/style.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/models/quill_delta.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/models/rules/delete.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/models/rules/format.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/models/rules/insert.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/models/rules/rule.dart';
|
@ -1,313 +0,0 @@
|
||||
// ignore_for_file: constant_identifier_names
|
||||
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:quiver/core.dart';
|
||||
|
||||
enum AttributeScope {
|
||||
INLINE, // refer to https://quilljs.com/docs/formats/#inline
|
||||
BLOCK, // refer to https://quilljs.com/docs/formats/#block
|
||||
EMBEDS, // refer to https://quilljs.com/docs/formats/#embeds
|
||||
IGNORE, // attributes that can be ignored
|
||||
}
|
||||
|
||||
class Attribute<T> {
|
||||
Attribute(this.key, this.scope, this.value);
|
||||
|
||||
final String key;
|
||||
final AttributeScope scope;
|
||||
final T value;
|
||||
|
||||
static final Map<String, Attribute> _registry = LinkedHashMap.of({
|
||||
Attribute.bold.key: Attribute.bold,
|
||||
Attribute.italic.key: Attribute.italic,
|
||||
Attribute.small.key: Attribute.small,
|
||||
Attribute.underline.key: Attribute.underline,
|
||||
Attribute.strikeThrough.key: Attribute.strikeThrough,
|
||||
Attribute.inlineCode.key: Attribute.inlineCode,
|
||||
Attribute.font.key: Attribute.font,
|
||||
Attribute.size.key: Attribute.size,
|
||||
Attribute.link.key: Attribute.link,
|
||||
Attribute.color.key: Attribute.color,
|
||||
Attribute.background.key: Attribute.background,
|
||||
Attribute.placeholder.key: Attribute.placeholder,
|
||||
Attribute.header.key: Attribute.header,
|
||||
Attribute.align.key: Attribute.align,
|
||||
Attribute.list.key: Attribute.list,
|
||||
Attribute.codeBlock.key: Attribute.codeBlock,
|
||||
Attribute.blockQuote.key: Attribute.blockQuote,
|
||||
Attribute.indent.key: Attribute.indent,
|
||||
Attribute.width.key: Attribute.width,
|
||||
Attribute.height.key: Attribute.height,
|
||||
Attribute.style.key: Attribute.style,
|
||||
Attribute.token.key: Attribute.token,
|
||||
});
|
||||
|
||||
static final BoldAttribute bold = BoldAttribute();
|
||||
|
||||
static final ItalicAttribute italic = ItalicAttribute();
|
||||
|
||||
static final SmallAttribute small = SmallAttribute();
|
||||
|
||||
static final UnderlineAttribute underline = UnderlineAttribute();
|
||||
|
||||
static final StrikeThroughAttribute strikeThrough = StrikeThroughAttribute();
|
||||
|
||||
static final InlineCodeAttribute inlineCode = InlineCodeAttribute();
|
||||
|
||||
static final FontAttribute font = FontAttribute(null);
|
||||
|
||||
static final SizeAttribute size = SizeAttribute(null);
|
||||
|
||||
static final LinkAttribute link = LinkAttribute(null);
|
||||
|
||||
static final ColorAttribute color = ColorAttribute(null);
|
||||
|
||||
static final BackgroundAttribute background = BackgroundAttribute(null);
|
||||
|
||||
static final PlaceholderAttribute placeholder = PlaceholderAttribute();
|
||||
|
||||
static final HeaderAttribute header = HeaderAttribute();
|
||||
|
||||
static final IndentAttribute indent = IndentAttribute();
|
||||
|
||||
static final AlignAttribute align = AlignAttribute(null);
|
||||
|
||||
static final ListAttribute list = ListAttribute(null);
|
||||
|
||||
static final CodeBlockAttribute codeBlock = CodeBlockAttribute();
|
||||
|
||||
static final BlockQuoteAttribute blockQuote = BlockQuoteAttribute();
|
||||
|
||||
static final WidthAttribute width = WidthAttribute(null);
|
||||
|
||||
static final HeightAttribute height = HeightAttribute(null);
|
||||
|
||||
static final StyleAttribute style = StyleAttribute(null);
|
||||
|
||||
static final TokenAttribute token = TokenAttribute('');
|
||||
|
||||
static final Set<String> inlineKeys = {
|
||||
Attribute.bold.key,
|
||||
Attribute.italic.key,
|
||||
Attribute.small.key,
|
||||
Attribute.underline.key,
|
||||
Attribute.strikeThrough.key,
|
||||
Attribute.link.key,
|
||||
Attribute.color.key,
|
||||
Attribute.background.key,
|
||||
Attribute.placeholder.key,
|
||||
};
|
||||
|
||||
static final Set<String> blockKeys = LinkedHashSet.of({
|
||||
Attribute.header.key,
|
||||
Attribute.align.key,
|
||||
Attribute.list.key,
|
||||
Attribute.codeBlock.key,
|
||||
Attribute.blockQuote.key,
|
||||
Attribute.indent.key,
|
||||
});
|
||||
|
||||
static final Set<String> blockKeysExceptHeader = LinkedHashSet.of({
|
||||
Attribute.list.key,
|
||||
Attribute.align.key,
|
||||
Attribute.codeBlock.key,
|
||||
Attribute.blockQuote.key,
|
||||
Attribute.indent.key,
|
||||
});
|
||||
|
||||
static final Set<String> exclusiveBlockKeys = LinkedHashSet.of({
|
||||
Attribute.header.key,
|
||||
Attribute.list.key,
|
||||
Attribute.codeBlock.key,
|
||||
Attribute.blockQuote.key,
|
||||
});
|
||||
|
||||
static Attribute<int?> get h1 => HeaderAttribute(level: 1);
|
||||
|
||||
static Attribute<int?> get h2 => HeaderAttribute(level: 2);
|
||||
|
||||
static Attribute<int?> get h3 => HeaderAttribute(level: 3);
|
||||
|
||||
// "attributes":{"align":"left"}
|
||||
static Attribute<String?> get leftAlignment => AlignAttribute('left');
|
||||
|
||||
// "attributes":{"align":"center"}
|
||||
static Attribute<String?> get centerAlignment => AlignAttribute('center');
|
||||
|
||||
// "attributes":{"align":"right"}
|
||||
static Attribute<String?> get rightAlignment => AlignAttribute('right');
|
||||
|
||||
// "attributes":{"align":"justify"}
|
||||
static Attribute<String?> get justifyAlignment => AlignAttribute('justify');
|
||||
|
||||
// "attributes":{"list":"bullet"}
|
||||
static Attribute<String?> get ul => ListAttribute('bullet');
|
||||
|
||||
// "attributes":{"list":"ordered"}
|
||||
static Attribute<String?> get ol => ListAttribute('ordered');
|
||||
|
||||
// "attributes":{"list":"checked"}
|
||||
static Attribute<String?> get checked => ListAttribute('checked');
|
||||
|
||||
// "attributes":{"list":"unchecked"}
|
||||
static Attribute<String?> get unchecked => ListAttribute('unchecked');
|
||||
|
||||
// "attributes":{"indent":1"}
|
||||
static Attribute<int?> get indentL1 => IndentAttribute(level: 1);
|
||||
|
||||
// "attributes":{"indent":2"}
|
||||
static Attribute<int?> get indentL2 => IndentAttribute(level: 2);
|
||||
|
||||
// "attributes":{"indent":3"}
|
||||
static Attribute<int?> get indentL3 => IndentAttribute(level: 3);
|
||||
|
||||
static Attribute<int?> getIndentLevel(int? level) {
|
||||
if (level == 1) {
|
||||
return indentL1;
|
||||
}
|
||||
if (level == 2) {
|
||||
return indentL2;
|
||||
}
|
||||
if (level == 3) {
|
||||
return indentL3;
|
||||
}
|
||||
return IndentAttribute(level: level);
|
||||
}
|
||||
|
||||
bool get isInline => scope == AttributeScope.INLINE;
|
||||
|
||||
bool get isBlockExceptHeader => blockKeysExceptHeader.contains(key);
|
||||
|
||||
Map<String, dynamic> toJson() => <String, dynamic>{key: value};
|
||||
|
||||
static Attribute? fromKeyValue(String key, dynamic value) {
|
||||
final origin = _registry[key];
|
||||
if (origin == null) {
|
||||
return null;
|
||||
}
|
||||
final attribute = clone(origin, value);
|
||||
return attribute;
|
||||
}
|
||||
|
||||
static int getRegistryOrder(Attribute attribute) {
|
||||
var order = 0;
|
||||
for (final attr in _registry.values) {
|
||||
if (attr.key == attribute.key) {
|
||||
break;
|
||||
}
|
||||
order++;
|
||||
}
|
||||
|
||||
return order;
|
||||
}
|
||||
|
||||
static Attribute clone(Attribute origin, dynamic value) {
|
||||
return Attribute(origin.key, origin.scope, value);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! Attribute) return false;
|
||||
final typedOther = other;
|
||||
return key == typedOther.key && scope == typedOther.scope && value == typedOther.value;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => hash3(key, scope, value);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Attribute{key: $key, scope: $scope, value: $value}';
|
||||
}
|
||||
}
|
||||
|
||||
class BoldAttribute extends Attribute<bool> {
|
||||
BoldAttribute() : super('bold', AttributeScope.INLINE, true);
|
||||
}
|
||||
|
||||
class ItalicAttribute extends Attribute<bool> {
|
||||
ItalicAttribute() : super('italic', AttributeScope.INLINE, true);
|
||||
}
|
||||
|
||||
class SmallAttribute extends Attribute<bool> {
|
||||
SmallAttribute() : super('small', AttributeScope.INLINE, true);
|
||||
}
|
||||
|
||||
class UnderlineAttribute extends Attribute<bool> {
|
||||
UnderlineAttribute() : super('underline', AttributeScope.INLINE, true);
|
||||
}
|
||||
|
||||
class StrikeThroughAttribute extends Attribute<bool> {
|
||||
StrikeThroughAttribute() : super('strike', AttributeScope.INLINE, true);
|
||||
}
|
||||
|
||||
class InlineCodeAttribute extends Attribute<bool> {
|
||||
InlineCodeAttribute() : super('code', AttributeScope.INLINE, true);
|
||||
}
|
||||
|
||||
class FontAttribute extends Attribute<String?> {
|
||||
FontAttribute(String? val) : super('font', AttributeScope.INLINE, val);
|
||||
}
|
||||
|
||||
class SizeAttribute extends Attribute<String?> {
|
||||
SizeAttribute(String? val) : super('size', AttributeScope.INLINE, val);
|
||||
}
|
||||
|
||||
class LinkAttribute extends Attribute<String?> {
|
||||
LinkAttribute(String? val) : super('link', AttributeScope.INLINE, val);
|
||||
}
|
||||
|
||||
class ColorAttribute extends Attribute<String?> {
|
||||
ColorAttribute(String? val) : super('color', AttributeScope.INLINE, val);
|
||||
}
|
||||
|
||||
class BackgroundAttribute extends Attribute<String?> {
|
||||
BackgroundAttribute(String? val) : super('background', AttributeScope.INLINE, val);
|
||||
}
|
||||
|
||||
/// This is custom attribute for hint
|
||||
class PlaceholderAttribute extends Attribute<bool> {
|
||||
PlaceholderAttribute() : super('placeholder', AttributeScope.INLINE, true);
|
||||
}
|
||||
|
||||
class HeaderAttribute extends Attribute<int?> {
|
||||
HeaderAttribute({int? level}) : super('header', AttributeScope.BLOCK, level);
|
||||
}
|
||||
|
||||
class IndentAttribute extends Attribute<int?> {
|
||||
IndentAttribute({int? level}) : super('indent', AttributeScope.BLOCK, level);
|
||||
}
|
||||
|
||||
class AlignAttribute extends Attribute<String?> {
|
||||
AlignAttribute(String? val) : super('align', AttributeScope.BLOCK, val);
|
||||
}
|
||||
|
||||
class ListAttribute extends Attribute<String?> {
|
||||
ListAttribute(String? val) : super('list', AttributeScope.BLOCK, val);
|
||||
}
|
||||
|
||||
class CodeBlockAttribute extends Attribute<bool> {
|
||||
CodeBlockAttribute() : super('code-block', AttributeScope.BLOCK, true);
|
||||
}
|
||||
|
||||
class BlockQuoteAttribute extends Attribute<bool> {
|
||||
BlockQuoteAttribute() : super('blockquote', AttributeScope.BLOCK, true);
|
||||
}
|
||||
|
||||
class WidthAttribute extends Attribute<String?> {
|
||||
WidthAttribute(String? val) : super('width', AttributeScope.IGNORE, val);
|
||||
}
|
||||
|
||||
class HeightAttribute extends Attribute<String?> {
|
||||
HeightAttribute(String? val) : super('height', AttributeScope.IGNORE, val);
|
||||
}
|
||||
|
||||
class StyleAttribute extends Attribute<String?> {
|
||||
StyleAttribute(String? val) : super('style', AttributeScope.IGNORE, val);
|
||||
}
|
||||
|
||||
class TokenAttribute extends Attribute<String> {
|
||||
TokenAttribute(String val) : super('token', AttributeScope.IGNORE, val);
|
||||
}
|
@ -1,291 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
import '../quill_delta.dart';
|
||||
import '../rules/rule.dart';
|
||||
import 'attribute.dart';
|
||||
import 'history.dart';
|
||||
import 'nodes/block.dart';
|
||||
import 'nodes/container.dart';
|
||||
import 'nodes/embed.dart';
|
||||
import 'nodes/line.dart';
|
||||
import 'nodes/node.dart';
|
||||
import 'style.dart';
|
||||
|
||||
/// The rich text document
|
||||
class Document {
|
||||
Document() : _delta = Delta()..insert('\n') {
|
||||
_loadDocument(_delta);
|
||||
}
|
||||
|
||||
Document.fromJson(List data) : _delta = _transform(Delta.fromJson(data)) {
|
||||
_loadDocument(_delta);
|
||||
}
|
||||
|
||||
Document.fromDelta(Delta delta) : _delta = delta {
|
||||
_loadDocument(delta);
|
||||
}
|
||||
|
||||
/// The root node of the document tree
|
||||
final Root _root = Root();
|
||||
|
||||
Root get root => _root;
|
||||
|
||||
int get length => _root.length;
|
||||
|
||||
Delta _delta;
|
||||
|
||||
Delta toDelta() => Delta.from(_delta);
|
||||
|
||||
final Rules _rules = Rules.getInstance();
|
||||
|
||||
void setCustomRules(List<Rule> customRules) {
|
||||
_rules.setCustomRules(customRules);
|
||||
}
|
||||
|
||||
final StreamController<Tuple3<Delta, Delta, ChangeSource>> _observer =
|
||||
StreamController.broadcast();
|
||||
|
||||
final History _history = History();
|
||||
|
||||
Stream<Tuple3<Delta, Delta, ChangeSource>> get changes => _observer.stream;
|
||||
|
||||
Delta insert(int index, Object? data, {int replaceLength = 0}) {
|
||||
assert(index >= 0);
|
||||
assert(data is String || data is Embeddable);
|
||||
if (data is Embeddable) {
|
||||
data = data.toJson();
|
||||
} else if ((data as String).isEmpty) {
|
||||
return Delta();
|
||||
}
|
||||
|
||||
final delta = _rules.apply(RuleType.INSERT, this, index,
|
||||
data: data, len: replaceLength);
|
||||
compose(delta, ChangeSource.LOCAL);
|
||||
return delta;
|
||||
}
|
||||
|
||||
Delta delete(int index, int len) {
|
||||
assert(index >= 0 && len > 0);
|
||||
final delta = _rules.apply(RuleType.DELETE, this, index, len: len);
|
||||
if (delta.isNotEmpty) {
|
||||
compose(delta, ChangeSource.LOCAL);
|
||||
}
|
||||
return delta;
|
||||
}
|
||||
|
||||
Delta replace(int index, int len, Object? data) {
|
||||
assert(index >= 0);
|
||||
assert(data is String || data is Embeddable);
|
||||
|
||||
final dataIsNotEmpty = (data is String) ? data.isNotEmpty : true;
|
||||
|
||||
assert(dataIsNotEmpty || len > 0);
|
||||
|
||||
var delta = Delta();
|
||||
|
||||
// We have to insert before applying delete rules
|
||||
// Otherwise delete would be operating on stale document snapshot.
|
||||
if (dataIsNotEmpty) {
|
||||
delta = insert(index, data, replaceLength: len);
|
||||
}
|
||||
|
||||
if (len > 0) {
|
||||
final deleteDelta = delete(index, len);
|
||||
delta = delta.compose(deleteDelta);
|
||||
}
|
||||
|
||||
return delta;
|
||||
}
|
||||
|
||||
Delta format(int index, int len, Attribute? attribute) {
|
||||
assert(index >= 0 && len >= 0 && attribute != null);
|
||||
|
||||
var delta = Delta();
|
||||
|
||||
final formatDelta = _rules.apply(RuleType.FORMAT, this, index,
|
||||
len: len, attribute: attribute);
|
||||
if (formatDelta.isNotEmpty) {
|
||||
compose(formatDelta, ChangeSource.LOCAL);
|
||||
delta = delta.compose(formatDelta);
|
||||
}
|
||||
|
||||
return delta;
|
||||
}
|
||||
|
||||
/// Only attributes applied to all characters within this range are
|
||||
/// included in the result.
|
||||
Style collectStyle(int index, int len) {
|
||||
final res = queryChild(index);
|
||||
return (res.node as Line).collectStyle(res.offset, len);
|
||||
}
|
||||
|
||||
/// Returns all styles for any character within the specified text range.
|
||||
List<Style> collectAllStyles(int index, int len) {
|
||||
final res = queryChild(index);
|
||||
return (res.node as Line).collectAllStyles(res.offset, len);
|
||||
}
|
||||
|
||||
ChildQuery queryChild(int offset) {
|
||||
final res = _root.queryChild(offset, true);
|
||||
if (res.node is Line) {
|
||||
return res;
|
||||
}
|
||||
final block = res.node as Block;
|
||||
return block.queryChild(res.offset, true);
|
||||
}
|
||||
|
||||
void compose(Delta delta, ChangeSource changeSource) {
|
||||
assert(!_observer.isClosed);
|
||||
delta.trim();
|
||||
assert(delta.isNotEmpty);
|
||||
|
||||
var offset = 0;
|
||||
delta = _transform(delta);
|
||||
final originalDelta = toDelta();
|
||||
for (final op in delta.toList()) {
|
||||
final style =
|
||||
op.attributes != null ? Style.fromJson(op.attributes) : null;
|
||||
|
||||
if (op.isInsert) {
|
||||
_root.insert(offset, _normalize(op.data), style);
|
||||
} else if (op.isDelete) {
|
||||
_root.delete(offset, op.length);
|
||||
} else if (op.attributes != null) {
|
||||
_root.retain(offset, op.length, style);
|
||||
}
|
||||
|
||||
if (!op.isDelete) {
|
||||
offset += op.length!;
|
||||
}
|
||||
}
|
||||
try {
|
||||
_delta = _delta.compose(delta);
|
||||
} catch (e) {
|
||||
throw '_delta compose failed';
|
||||
}
|
||||
|
||||
if (_delta != _root.toDelta()) {
|
||||
throw 'Compose failed';
|
||||
}
|
||||
final change = Tuple3(originalDelta, delta, changeSource);
|
||||
_observer.add(change);
|
||||
_history.handleDocChange(change);
|
||||
}
|
||||
|
||||
Tuple2 undo() {
|
||||
return _history.undo(this);
|
||||
}
|
||||
|
||||
Tuple2 redo() {
|
||||
return _history.redo(this);
|
||||
}
|
||||
|
||||
bool get hasUndo => _history.hasUndo;
|
||||
|
||||
bool get hasRedo => _history.hasRedo;
|
||||
|
||||
static Delta _transform(Delta delta) {
|
||||
final res = Delta();
|
||||
final ops = delta.toList();
|
||||
for (var i = 0; i < ops.length; i++) {
|
||||
final op = ops[i];
|
||||
res.push(op);
|
||||
_autoAppendNewlineAfterEmbeddable(i, ops, op, res, 'video');
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
static void _autoAppendNewlineAfterEmbeddable(
|
||||
int i, List<Operation> ops, Operation op, Delta res, String type) {
|
||||
final nextOpIsEmbed = i + 1 < ops.length &&
|
||||
ops[i + 1].isInsert &&
|
||||
ops[i + 1].data is Map &&
|
||||
(ops[i + 1].data as Map).containsKey(type);
|
||||
if (nextOpIsEmbed &&
|
||||
op.data is String &&
|
||||
(op.data as String).isNotEmpty &&
|
||||
!(op.data as String).endsWith('\n')) {
|
||||
res.push(Operation.insert('\n'));
|
||||
}
|
||||
// embed could be image or video
|
||||
final opInsertEmbed =
|
||||
op.isInsert && op.data is Map && (op.data as Map).containsKey(type);
|
||||
final nextOpIsLineBreak = i + 1 < ops.length &&
|
||||
ops[i + 1].isInsert &&
|
||||
ops[i + 1].data is String &&
|
||||
(ops[i + 1].data as String).startsWith('\n');
|
||||
if (opInsertEmbed && (i + 1 == ops.length - 1 || !nextOpIsLineBreak)) {
|
||||
// automatically append '\n' for embeddable
|
||||
res.push(Operation.insert('\n'));
|
||||
}
|
||||
}
|
||||
|
||||
Object _normalize(Object? data) {
|
||||
if (data is String) {
|
||||
return data;
|
||||
}
|
||||
|
||||
if (data is Embeddable) {
|
||||
return data;
|
||||
}
|
||||
return Embeddable.fromJson(data as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
void close() {
|
||||
_observer.close();
|
||||
_history.clear();
|
||||
}
|
||||
|
||||
String toPlainText() => _root.children.map((e) => e.toPlainText()).join();
|
||||
|
||||
void _loadDocument(Delta doc) {
|
||||
if (doc.isEmpty) {
|
||||
throw ArgumentError.value(doc, 'Document Delta cannot be empty.');
|
||||
}
|
||||
|
||||
assert((doc.last.data as String).endsWith('\n'));
|
||||
|
||||
var offset = 0;
|
||||
for (final op in doc.toList()) {
|
||||
if (!op.isInsert) {
|
||||
throw ArgumentError.value(doc,
|
||||
'Document can only contain insert operations but ${op.key} found.');
|
||||
}
|
||||
final style =
|
||||
op.attributes != null ? Style.fromJson(op.attributes) : null;
|
||||
final data = _normalize(op.data);
|
||||
_root.insert(offset, data, style);
|
||||
offset += op.length!;
|
||||
}
|
||||
final node = _root.last;
|
||||
if (node is Line &&
|
||||
node.parent is! Block &&
|
||||
node.style.isEmpty &&
|
||||
_root.childCount > 1) {
|
||||
_root.remove(node);
|
||||
}
|
||||
}
|
||||
|
||||
bool isEmpty() {
|
||||
if (root.children.length != 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final node = root.children.first;
|
||||
if (!node.isLast) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final delta = node.toDelta();
|
||||
return delta.length == 1 &&
|
||||
delta.first.data == '\n' &&
|
||||
delta.first.key == 'insert';
|
||||
}
|
||||
}
|
||||
|
||||
enum ChangeSource {
|
||||
LOCAL,
|
||||
REMOTE,
|
||||
}
|
@ -1,134 +0,0 @@
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
import '../quill_delta.dart';
|
||||
import 'document.dart';
|
||||
|
||||
class History {
|
||||
History({
|
||||
this.ignoreChange = false,
|
||||
this.interval = 400,
|
||||
this.maxStack = 100,
|
||||
this.userOnly = false,
|
||||
this.lastRecorded = 0,
|
||||
});
|
||||
|
||||
final HistoryStack stack = HistoryStack.empty();
|
||||
|
||||
bool get hasUndo => stack.undo.isNotEmpty;
|
||||
|
||||
bool get hasRedo => stack.redo.isNotEmpty;
|
||||
|
||||
/// used for disable redo or undo function
|
||||
bool ignoreChange;
|
||||
|
||||
int lastRecorded;
|
||||
|
||||
/// Collaborative editing's conditions should be true
|
||||
final bool userOnly;
|
||||
|
||||
///max operation count for undo
|
||||
final int maxStack;
|
||||
|
||||
///record delay
|
||||
final int interval;
|
||||
|
||||
void handleDocChange(Tuple3<Delta, Delta, ChangeSource> change) {
|
||||
if (ignoreChange) return;
|
||||
if (!userOnly || change.item3 == ChangeSource.LOCAL) {
|
||||
record(change.item2, change.item1);
|
||||
} else {
|
||||
transform(change.item2);
|
||||
}
|
||||
}
|
||||
|
||||
void clear() {
|
||||
stack.clear();
|
||||
}
|
||||
|
||||
void record(Delta change, Delta before) {
|
||||
if (change.isEmpty) return;
|
||||
stack.redo.clear();
|
||||
var undoDelta = change.invert(before);
|
||||
final timeStamp = DateTime.now().millisecondsSinceEpoch;
|
||||
|
||||
if (lastRecorded + interval > timeStamp && stack.undo.isNotEmpty) {
|
||||
final lastDelta = stack.undo.removeLast();
|
||||
undoDelta = undoDelta.compose(lastDelta);
|
||||
} else {
|
||||
lastRecorded = timeStamp;
|
||||
}
|
||||
|
||||
if (undoDelta.isEmpty) return;
|
||||
stack.undo.add(undoDelta);
|
||||
|
||||
if (stack.undo.length > maxStack) {
|
||||
stack.undo.removeAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
///It will override pre local undo delta,replaced by remote change
|
||||
///
|
||||
void transform(Delta delta) {
|
||||
transformStack(stack.undo, delta);
|
||||
transformStack(stack.redo, delta);
|
||||
}
|
||||
|
||||
void transformStack(List<Delta> stack, Delta delta) {
|
||||
for (var i = stack.length - 1; i >= 0; i -= 1) {
|
||||
final oldDelta = stack[i];
|
||||
stack[i] = delta.transform(oldDelta, true);
|
||||
delta = oldDelta.transform(delta, false);
|
||||
if (stack[i].length == 0) {
|
||||
stack.removeAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Tuple2 _change(Document doc, List<Delta> source, List<Delta> dest) {
|
||||
if (source.isEmpty) {
|
||||
return const Tuple2(false, 0);
|
||||
}
|
||||
final delta = source.removeLast();
|
||||
// look for insert or delete
|
||||
int? len = 0;
|
||||
final ops = delta.toList();
|
||||
for (var i = 0; i < ops.length; i++) {
|
||||
if (ops[i].key == Operation.insertKey) {
|
||||
len = ops[i].length;
|
||||
} else if (ops[i].key == Operation.deleteKey) {
|
||||
len = ops[i].length! * -1;
|
||||
}
|
||||
}
|
||||
final base = Delta.from(doc.toDelta());
|
||||
final inverseDelta = delta.invert(base);
|
||||
dest.add(inverseDelta);
|
||||
lastRecorded = 0;
|
||||
ignoreChange = true;
|
||||
doc.compose(delta, ChangeSource.LOCAL);
|
||||
ignoreChange = false;
|
||||
return Tuple2(true, len);
|
||||
}
|
||||
|
||||
Tuple2 undo(Document doc) {
|
||||
return _change(doc, stack.undo, stack.redo);
|
||||
}
|
||||
|
||||
Tuple2 redo(Document doc) {
|
||||
return _change(doc, stack.redo, stack.undo);
|
||||
}
|
||||
}
|
||||
|
||||
class HistoryStack {
|
||||
HistoryStack.empty()
|
||||
: undo = [],
|
||||
redo = [];
|
||||
|
||||
final List<Delta> undo;
|
||||
final List<Delta> redo;
|
||||
|
||||
void clear() {
|
||||
undo.clear();
|
||||
redo.clear();
|
||||
}
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
import '../../quill_delta.dart';
|
||||
import 'container.dart';
|
||||
import 'line.dart';
|
||||
import 'node.dart';
|
||||
|
||||
/// Represents a group of adjacent [Line]s with the same block style.
|
||||
///
|
||||
/// Block elements are:
|
||||
/// - Blockquote
|
||||
/// - Header
|
||||
/// - Indent
|
||||
/// - List
|
||||
/// - Text Alignment
|
||||
/// - Text Direction
|
||||
/// - Code Block
|
||||
class Block extends Container<Line?> {
|
||||
/// Creates new unmounted [Block].
|
||||
@override
|
||||
Node newInstance() => Block();
|
||||
|
||||
@override
|
||||
Line get defaultChild => Line();
|
||||
|
||||
@override
|
||||
Delta toDelta() {
|
||||
return children
|
||||
.map((child) => child.toDelta())
|
||||
.fold(Delta(), (a, b) => a.concat(b));
|
||||
}
|
||||
|
||||
@override
|
||||
void adjust() {
|
||||
if (isEmpty) {
|
||||
final sibling = previous;
|
||||
unlink();
|
||||
if (sibling != null) {
|
||||
sibling.adjust();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var block = this;
|
||||
final prev = block.previous;
|
||||
// merging it with previous block if style is the same
|
||||
if (!block.isFirst &&
|
||||
block.previous is Block &&
|
||||
prev!.style == block.style) {
|
||||
block
|
||||
..moveChildToNewParent(prev as Container<Node?>?)
|
||||
..unlink();
|
||||
block = prev as Block;
|
||||
}
|
||||
final next = block.next;
|
||||
// merging it with next block if style is the same
|
||||
if (!block.isLast && block.next is Block && next!.style == block.style) {
|
||||
(next as Block).moveChildToNewParent(block);
|
||||
next.unlink();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final block = style.attributes.toString();
|
||||
final buffer = StringBuffer('§ {$block}\n');
|
||||
for (final child in children) {
|
||||
final tree = child.isLast ? '└' : '├';
|
||||
buffer.write(' $tree $child');
|
||||
if (!child.isLast) buffer.writeln();
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
@ -1,160 +0,0 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import '../style.dart';
|
||||
import 'leaf.dart';
|
||||
import 'line.dart';
|
||||
import 'node.dart';
|
||||
|
||||
/// Container can accommodate other nodes.
|
||||
///
|
||||
/// Delegates insert, retain and delete operations to children nodes. For each
|
||||
/// operation container looks for a child at specified index position and
|
||||
/// forwards operation to that child.
|
||||
///
|
||||
/// Most of the operation handling logic is implemented by [Line] and [Text].
|
||||
abstract class Container<T extends Node?> extends Node {
|
||||
final LinkedList<Node> _children = LinkedList<Node>();
|
||||
|
||||
/// List of children.
|
||||
LinkedList<Node> get children => _children;
|
||||
|
||||
/// Returns total number of child nodes in this container.
|
||||
///
|
||||
/// To get text length of this container see [length].
|
||||
int get childCount => _children.length;
|
||||
|
||||
/// Returns the first child [Node].
|
||||
Node get first => _children.first;
|
||||
|
||||
/// Returns the last child [Node].
|
||||
Node get last => _children.last;
|
||||
|
||||
/// Returns `true` if this container has no child nodes.
|
||||
bool get isEmpty => _children.isEmpty;
|
||||
|
||||
/// Returns `true` if this container has at least 1 child.
|
||||
bool get isNotEmpty => _children.isNotEmpty;
|
||||
|
||||
/// Returns an instance of default child for this container node.
|
||||
///
|
||||
/// Always returns fresh instance.
|
||||
T get defaultChild;
|
||||
|
||||
/// Adds [node] to the end of this container children list.
|
||||
void add(T node) {
|
||||
assert(node?.parent == null);
|
||||
node?.parent = this;
|
||||
_children.add(node as Node);
|
||||
}
|
||||
|
||||
/// Adds [node] to the beginning of this container children list.
|
||||
void addFirst(T node) {
|
||||
assert(node?.parent == null);
|
||||
node?.parent = this;
|
||||
_children.addFirst(node as Node);
|
||||
}
|
||||
|
||||
/// Removes [node] from this container.
|
||||
void remove(T node) {
|
||||
assert(node?.parent == this);
|
||||
node?.parent = null;
|
||||
_children.remove(node as Node);
|
||||
}
|
||||
|
||||
/// Moves children of this node to [newParent].
|
||||
void moveChildToNewParent(Container? newParent) {
|
||||
if (isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final last = newParent!.isEmpty ? null : newParent.last as T?;
|
||||
while (isNotEmpty) {
|
||||
final child = first as T;
|
||||
child?.unlink();
|
||||
newParent.add(child);
|
||||
}
|
||||
|
||||
/// In case [newParent] already had children we need to make sure
|
||||
/// combined list is optimized.
|
||||
if (last != null) last.adjust();
|
||||
}
|
||||
|
||||
/// Queries the child [Node] at [offset] in this container.
|
||||
///
|
||||
/// The result may contain the found node or `null` if no node is found
|
||||
/// at specified offset.
|
||||
///
|
||||
/// [ChildQuery.offset] is set to relative offset within returned child node
|
||||
/// which points at the same character position in the document as the
|
||||
/// original [offset].
|
||||
ChildQuery queryChild(int offset, bool inclusive) {
|
||||
if (offset < 0 || offset > length) {
|
||||
return ChildQuery(null, 0);
|
||||
}
|
||||
|
||||
for (final node in children) {
|
||||
final len = node.length;
|
||||
if (offset < len || (inclusive && offset == len && node.isLast)) {
|
||||
return ChildQuery(node, offset);
|
||||
}
|
||||
offset -= len;
|
||||
}
|
||||
return ChildQuery(null, 0);
|
||||
}
|
||||
|
||||
@override
|
||||
String toPlainText() => children.map((child) => child.toPlainText()).join();
|
||||
|
||||
/// Content length of this node's children.
|
||||
///
|
||||
/// To get number of children in this node use [childCount].
|
||||
@override
|
||||
int get length => _children.fold(0, (cur, node) => cur + node.length);
|
||||
|
||||
@override
|
||||
void insert(int index, Object data, Style? style) {
|
||||
assert(index == 0 || (index > 0 && index < length));
|
||||
|
||||
if (isNotEmpty) {
|
||||
final child = queryChild(index, false);
|
||||
child.node!.insert(child.offset, data, style);
|
||||
return;
|
||||
}
|
||||
|
||||
// empty
|
||||
assert(index == 0);
|
||||
final node = defaultChild;
|
||||
add(node);
|
||||
node?.insert(index, data, style);
|
||||
}
|
||||
|
||||
@override
|
||||
void retain(int index, int? length, Style? attributes) {
|
||||
assert(isNotEmpty);
|
||||
final child = queryChild(index, false);
|
||||
child.node!.retain(child.offset, length, attributes);
|
||||
}
|
||||
|
||||
@override
|
||||
void delete(int index, int? length) {
|
||||
assert(isNotEmpty);
|
||||
final child = queryChild(index, false);
|
||||
child.node!.delete(child.offset, length);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => _children.join('\n');
|
||||
}
|
||||
|
||||
/// Result of a child query in a [Container].
|
||||
class ChildQuery {
|
||||
ChildQuery(this.node, this.offset);
|
||||
|
||||
/// The child node if found, otherwise `null`.
|
||||
final Node? node;
|
||||
|
||||
/// Starting offset within the child [node] which points at the same
|
||||
/// character in the document as the original offset passed to
|
||||
/// [Container.queryChild] method.
|
||||
final int offset;
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
/// An object which can be embedded into a Quill document.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [BlockEmbed] which represents a block embed.
|
||||
class Embeddable {
|
||||
const Embeddable(this.type, this.data);
|
||||
|
||||
/// The type of this object.
|
||||
final String type;
|
||||
|
||||
/// The data payload of this object.
|
||||
final dynamic data;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final m = <String, String>{type: data};
|
||||
return m;
|
||||
}
|
||||
|
||||
static Embeddable fromJson(Map<String, dynamic> json) {
|
||||
final m = Map<String, dynamic>.from(json);
|
||||
assert(m.length == 1, 'Embeddable map has one key');
|
||||
|
||||
return BlockEmbed(m.keys.first, m.values.first);
|
||||
}
|
||||
}
|
||||
|
||||
/// An object which occupies an entire line in a document and cannot co-exist
|
||||
/// inline with regular text.
|
||||
///
|
||||
/// There are two built-in embed types supported by Quill documents, however
|
||||
/// the document model itself does not make any assumptions about the types
|
||||
/// of embedded objects and allows users to define their own types.
|
||||
class BlockEmbed extends Embeddable {
|
||||
const BlockEmbed(String type, String data) : super(type, data);
|
||||
|
||||
static const String horizontalRuleType = 'divider';
|
||||
static BlockEmbed horizontalRule = const BlockEmbed(horizontalRuleType, 'hr');
|
||||
|
||||
static const String imageType = 'image';
|
||||
static BlockEmbed image(String imageUrl) => BlockEmbed(imageType, imageUrl);
|
||||
|
||||
static const String videoType = 'video';
|
||||
static BlockEmbed video(String videoUrl) => BlockEmbed(videoType, videoUrl);
|
||||
}
|
@ -1,252 +0,0 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import '../../quill_delta.dart';
|
||||
import '../style.dart';
|
||||
import 'embed.dart';
|
||||
import 'line.dart';
|
||||
import 'node.dart';
|
||||
|
||||
/// A leaf in Quill document tree.
|
||||
abstract class Leaf extends Node {
|
||||
/// Creates a new [Leaf] with specified [data].
|
||||
factory Leaf(Object data) {
|
||||
if (data is Embeddable) {
|
||||
return Embed(data);
|
||||
}
|
||||
final text = data as String;
|
||||
assert(text.isNotEmpty);
|
||||
return Text(text);
|
||||
}
|
||||
|
||||
Leaf.val(Object val) : _value = val;
|
||||
|
||||
/// Contents of this node, either a String if this is a [Text] or an
|
||||
/// [Embed] if this is an [BlockEmbed].
|
||||
Object get value => _value;
|
||||
Object _value;
|
||||
|
||||
@override
|
||||
void applyStyle(Style value) {
|
||||
assert(value.isInline || value.isIgnored || value.isEmpty,
|
||||
'Unable to apply Style to leaf: $value');
|
||||
super.applyStyle(value);
|
||||
}
|
||||
|
||||
@override
|
||||
Line? get parent => super.parent as Line?;
|
||||
|
||||
@override
|
||||
int get length {
|
||||
if (_value is String) {
|
||||
return (_value as String).length;
|
||||
}
|
||||
// return 1 for embedded object
|
||||
return 1;
|
||||
}
|
||||
|
||||
@override
|
||||
Delta toDelta() {
|
||||
final data =
|
||||
_value is Embeddable ? (_value as Embeddable).toJson() : _value;
|
||||
return Delta()..insert(data, style.toJson());
|
||||
}
|
||||
|
||||
@override
|
||||
void insert(int index, Object data, Style? style) {
|
||||
assert(index >= 0 && index <= length);
|
||||
final node = Leaf(data);
|
||||
if (index < length) {
|
||||
splitAt(index)!.insertBefore(node);
|
||||
} else {
|
||||
insertAfter(node);
|
||||
}
|
||||
node.format(style);
|
||||
}
|
||||
|
||||
@override
|
||||
void retain(int index, int? len, Style? style) {
|
||||
if (style == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final local = math.min(length - index, len!);
|
||||
final remain = len - local;
|
||||
final node = _isolate(index, local);
|
||||
|
||||
if (remain > 0) {
|
||||
assert(node.next != null);
|
||||
node.next!.retain(0, remain, style);
|
||||
}
|
||||
node.format(style);
|
||||
}
|
||||
|
||||
@override
|
||||
void delete(int index, int? len) {
|
||||
assert(index < length);
|
||||
|
||||
final local = math.min(length - index, len!);
|
||||
final target = _isolate(index, local);
|
||||
final prev = target.previous as Leaf?;
|
||||
final next = target.next as Leaf?;
|
||||
target.unlink();
|
||||
|
||||
final remain = len - local;
|
||||
if (remain > 0) {
|
||||
assert(next != null);
|
||||
next!.delete(0, remain);
|
||||
}
|
||||
|
||||
if (prev != null) {
|
||||
prev.adjust();
|
||||
}
|
||||
}
|
||||
|
||||
/// Adjust this text node by merging it with adjacent nodes if they share
|
||||
/// the same style.
|
||||
@override
|
||||
void adjust() {
|
||||
if (this is Embed) {
|
||||
// Embed nodes cannot be merged with text nor other embeds (in fact,
|
||||
// there could be no two adjacent embeds on the same line since an
|
||||
// embed occupies an entire line).
|
||||
return;
|
||||
}
|
||||
|
||||
// This is a text node and it can only be merged with other text nodes.
|
||||
var node = this as Text;
|
||||
|
||||
// Merging it with previous node if style is the same.
|
||||
final prev = node.previous;
|
||||
if (!node.isFirst && prev is Text && prev.style == node.style) {
|
||||
prev._value = prev.value + node.value;
|
||||
node.unlink();
|
||||
node = prev;
|
||||
}
|
||||
|
||||
// Merging it with next node if style is the same.
|
||||
final next = node.next;
|
||||
if (!node.isLast && next is Text && next.style == node.style) {
|
||||
node._value = node.value + next.value;
|
||||
next.unlink();
|
||||
}
|
||||
}
|
||||
|
||||
/// Splits this leaf node at [index] and returns new node.
|
||||
///
|
||||
/// If this is the last node in its list and [index] equals this node's
|
||||
/// length then this method returns `null` as there is nothing left to split.
|
||||
/// If there is another leaf node after this one and [index] equals this
|
||||
/// node's length then the next leaf node is returned.
|
||||
///
|
||||
/// If [index] equals to `0` then this node itself is returned unchanged.
|
||||
///
|
||||
/// In case a new node is actually split from this one, it inherits this
|
||||
/// node's style.
|
||||
Leaf? splitAt(int index) {
|
||||
assert(index >= 0 && index <= length);
|
||||
if (index == 0) {
|
||||
return this;
|
||||
}
|
||||
if (index == length) {
|
||||
return isLast ? null : next as Leaf?;
|
||||
}
|
||||
|
||||
assert(this is Text);
|
||||
final text = _value as String;
|
||||
_value = text.substring(0, index);
|
||||
final split = Leaf(text.substring(index))..applyStyle(style);
|
||||
insertAfter(split);
|
||||
return split;
|
||||
}
|
||||
|
||||
/// Cuts a leaf from [index] to the end of this node and returns new node
|
||||
/// in detached state (e.g. [mounted] returns `false`).
|
||||
///
|
||||
/// Splitting logic is identical to one described in [splitAt], meaning this
|
||||
/// method may return `null`.
|
||||
Leaf? cutAt(int index) {
|
||||
assert(index >= 0 && index <= length);
|
||||
final cut = splitAt(index);
|
||||
cut?.unlink();
|
||||
return cut;
|
||||
}
|
||||
|
||||
/// Formats this node and optimizes it with adjacent leaf nodes if needed.
|
||||
void format(Style? style) {
|
||||
if (style != null && style.isNotEmpty) {
|
||||
applyStyle(style);
|
||||
}
|
||||
adjust();
|
||||
}
|
||||
|
||||
/// Isolates a new leaf starting at [index] with specified [length].
|
||||
///
|
||||
/// Splitting logic is identical to one described in [splitAt], with one
|
||||
/// exception that it is required for [index] to always be less than this
|
||||
/// node's length. As a result this method always returns a [LeafNode]
|
||||
/// instance. Returned node may still be the same as this node
|
||||
/// if provided [index] is `0`.
|
||||
Leaf _isolate(int index, int length) {
|
||||
assert(
|
||||
index >= 0 && index < this.length && (index + length <= this.length));
|
||||
final target = splitAt(index)!..splitAt(length);
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
/// A span of formatted text within a line in a Quill document.
|
||||
///
|
||||
/// Text is a leaf node of a document tree.
|
||||
///
|
||||
/// Parent of a text node is always a [Line], and as a consequence text
|
||||
/// node's [value] cannot contain any line-break characters.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Embed], a leaf node representing an embeddable object.
|
||||
/// * [Line], a node representing a line of text.
|
||||
class Text extends Leaf {
|
||||
Text([String text = ''])
|
||||
: assert(!text.contains('\n')),
|
||||
super.val(text);
|
||||
|
||||
@override
|
||||
Node newInstance() => Text(value);
|
||||
|
||||
@override
|
||||
String get value => _value as String;
|
||||
|
||||
@override
|
||||
String toPlainText() => value;
|
||||
}
|
||||
|
||||
/// An embed node inside of a line in a Quill document.
|
||||
///
|
||||
/// Embed node is a leaf node similar to [Text]. It represents an arbitrary
|
||||
/// piece of non-textual content embedded into a document, such as, image,
|
||||
/// horizontal rule, video, or any other object with defined structure,
|
||||
/// like a tweet, for instance.
|
||||
///
|
||||
/// Embed node's length is always `1` character and it is represented with
|
||||
/// unicode object replacement character in the document text.
|
||||
///
|
||||
/// Any inline style can be applied to an embed, however this does not
|
||||
/// necessarily mean the embed will look according to that style. For instance,
|
||||
/// applying "bold" style to an image gives no effect, while adding a "link" to
|
||||
/// an image actually makes the image react to user's action.
|
||||
class Embed extends Leaf {
|
||||
Embed(Embeddable data) : super.val(data);
|
||||
|
||||
static const kObjectReplacementCharacter = '\uFFFC';
|
||||
|
||||
@override
|
||||
Node newInstance() => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Embeddable get value => super.value as Embeddable;
|
||||
|
||||
/// // Embed nodes are represented as unicode object replacement character in
|
||||
// plain text.
|
||||
@override
|
||||
String toPlainText() => kObjectReplacementCharacter;
|
||||
}
|
@ -1,414 +0,0 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import '../../quill_delta.dart';
|
||||
import '../attribute.dart';
|
||||
import '../style.dart';
|
||||
import 'block.dart';
|
||||
import 'container.dart';
|
||||
import 'embed.dart';
|
||||
import 'leaf.dart';
|
||||
import 'node.dart';
|
||||
|
||||
/// A line of rich text in a Quill document.
|
||||
///
|
||||
/// Line serves as a container for [Leaf]s, like [Text] and [Embed].
|
||||
///
|
||||
/// When a line contains an embed, it fully occupies the line, no other embeds
|
||||
/// or text nodes are allowed.
|
||||
class Line extends Container<Leaf?> {
|
||||
@override
|
||||
Leaf get defaultChild => Text();
|
||||
|
||||
@override
|
||||
int get length => super.length + 1;
|
||||
|
||||
/// Returns `true` if this line contains an embedded object.
|
||||
bool get hasEmbed {
|
||||
return children.any((child) => child is Embed);
|
||||
}
|
||||
|
||||
/// Returns next [Line] or `null` if this is the last line in the document.
|
||||
Line? get nextLine {
|
||||
if (!isLast) {
|
||||
return next is Block ? (next as Block).first as Line? : next as Line?;
|
||||
}
|
||||
if (parent is! Block) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (parent!.isLast) {
|
||||
return null;
|
||||
}
|
||||
return parent!.next is Block
|
||||
? (parent!.next as Block).first as Line?
|
||||
: parent!.next as Line?;
|
||||
}
|
||||
|
||||
@override
|
||||
Node newInstance() => Line();
|
||||
|
||||
@override
|
||||
Delta toDelta() {
|
||||
final delta = children
|
||||
.map((child) => child.toDelta())
|
||||
.fold(Delta(), (dynamic a, b) => a.concat(b));
|
||||
var attributes = style;
|
||||
if (parent is Block) {
|
||||
final block = parent as Block;
|
||||
attributes = attributes.mergeAll(block.style);
|
||||
}
|
||||
delta.insert('\n', attributes.toJson());
|
||||
return delta;
|
||||
}
|
||||
|
||||
@override
|
||||
String toPlainText() => '${super.toPlainText()}\n';
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final body = children.join(' → ');
|
||||
final styleString = style.isNotEmpty ? ' $style' : '';
|
||||
return '¶ $body ⏎$styleString';
|
||||
}
|
||||
|
||||
@override
|
||||
void insert(int index, Object data, Style? style) {
|
||||
if (data is Embeddable) {
|
||||
// We do not check whether this line already has any children here as
|
||||
// inserting an embed into a line with other text is acceptable from the
|
||||
// Delta format perspective.
|
||||
// We rely on heuristic rules to ensure that embeds occupy an entire line.
|
||||
_insertSafe(index, data, style);
|
||||
return;
|
||||
}
|
||||
|
||||
final text = data as String;
|
||||
final lineBreak = text.indexOf('\n');
|
||||
if (lineBreak < 0) {
|
||||
_insertSafe(index, text, style);
|
||||
// No need to update line or block format since those attributes can only
|
||||
// be attached to `\n` character and we already know it's not present.
|
||||
return;
|
||||
}
|
||||
|
||||
final prefix = text.substring(0, lineBreak);
|
||||
_insertSafe(index, prefix, style);
|
||||
if (prefix.isNotEmpty) {
|
||||
index += prefix.length;
|
||||
}
|
||||
|
||||
// Next line inherits our format.
|
||||
final nextLine = _getNextLine(index);
|
||||
|
||||
// Reset our format and unwrap from a block if needed.
|
||||
clearStyle();
|
||||
if (parent is Block) {
|
||||
_unwrap();
|
||||
}
|
||||
|
||||
// Now we can apply new format and re-layout.
|
||||
_format(style);
|
||||
|
||||
// Continue with remaining part.
|
||||
final remain = text.substring(lineBreak + 1);
|
||||
nextLine.insert(0, remain, style);
|
||||
}
|
||||
|
||||
@override
|
||||
void retain(int index, int? len, Style? style) {
|
||||
if (style == null) {
|
||||
return;
|
||||
}
|
||||
final thisLength = length;
|
||||
|
||||
final local = math.min(thisLength - index, len!);
|
||||
// If index is at newline character then this is a line/block style update.
|
||||
final isLineFormat = (index + local == thisLength) && local == 1;
|
||||
|
||||
if (isLineFormat) {
|
||||
assert(style.values.every((attr) => attr.scope == AttributeScope.BLOCK),
|
||||
'It is not allowed to apply inline attributes to line itself.');
|
||||
_format(style);
|
||||
} else {
|
||||
// Otherwise forward to children as it's an inline format update.
|
||||
assert(style.values.every((attr) => attr.scope == AttributeScope.INLINE));
|
||||
assert(index + local != thisLength);
|
||||
super.retain(index, local, style);
|
||||
}
|
||||
|
||||
final remain = len - local;
|
||||
if (remain > 0) {
|
||||
assert(nextLine != null);
|
||||
nextLine!.retain(0, remain, style);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void delete(int index, int? len) {
|
||||
final local = math.min(length - index, len!);
|
||||
final isLFDeleted = index + local == length; // Line feed
|
||||
if (isLFDeleted) {
|
||||
// Our newline character deleted with all style information.
|
||||
clearStyle();
|
||||
if (local > 1) {
|
||||
// Exclude newline character from delete range for children.
|
||||
super.delete(index, local - 1);
|
||||
}
|
||||
} else {
|
||||
super.delete(index, local);
|
||||
}
|
||||
|
||||
final remaining = len - local;
|
||||
if (remaining > 0) {
|
||||
assert(nextLine != null);
|
||||
nextLine!.delete(0, remaining);
|
||||
}
|
||||
|
||||
if (isLFDeleted && isNotEmpty) {
|
||||
// Since we lost our line-break and still have child text nodes those must
|
||||
// migrate to the next line.
|
||||
|
||||
// nextLine might have been unmounted since last assert so we need to
|
||||
// check again we still have a line after us.
|
||||
assert(nextLine != null);
|
||||
|
||||
// Move remaining children in this line to the next line so that all
|
||||
// attributes of nextLine are preserved.
|
||||
nextLine!.moveChildToNewParent(this);
|
||||
moveChildToNewParent(nextLine);
|
||||
}
|
||||
|
||||
if (isLFDeleted) {
|
||||
// Now we can remove this line.
|
||||
final block = parent!; // remember reference before un-linking.
|
||||
unlink();
|
||||
block.adjust();
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats this line.
|
||||
void _format(Style? newStyle) {
|
||||
if (newStyle == null || newStyle.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
applyStyle(newStyle);
|
||||
final blockStyle = newStyle.getBlockExceptHeader();
|
||||
if (blockStyle == null) {
|
||||
return;
|
||||
} // No block-level changes
|
||||
|
||||
if (parent is Block) {
|
||||
final parentStyle = (parent as Block).style.getBlocksExceptHeader();
|
||||
// Ensure that we're only unwrapping the block only if we unset a single
|
||||
// block format in the `parentStyle` and there are no more block formats
|
||||
// left to unset.
|
||||
if (blockStyle.value == null &&
|
||||
parentStyle.containsKey(blockStyle.key) &&
|
||||
parentStyle.length == 1) {
|
||||
_unwrap();
|
||||
} else if (!const MapEquality()
|
||||
.equals(newStyle.getBlocksExceptHeader(), parentStyle)) {
|
||||
_unwrap();
|
||||
// Block style now can contain multiple attributes
|
||||
if (newStyle.attributes.keys
|
||||
.any(Attribute.exclusiveBlockKeys.contains)) {
|
||||
parentStyle.removeWhere(
|
||||
(key, attr) => Attribute.exclusiveBlockKeys.contains(key));
|
||||
}
|
||||
parentStyle.removeWhere(
|
||||
(key, attr) => newStyle?.attributes.keys.contains(key) ?? false);
|
||||
final parentStyleToMerge = Style.attr(parentStyle);
|
||||
newStyle = newStyle.mergeAll(parentStyleToMerge);
|
||||
_applyBlockStyles(newStyle);
|
||||
} // else the same style, no-op.
|
||||
} else if (blockStyle.value != null) {
|
||||
// Only wrap with a new block if this is not an unset
|
||||
_applyBlockStyles(newStyle);
|
||||
}
|
||||
}
|
||||
|
||||
void _applyBlockStyles(Style newStyle) {
|
||||
var block = Block();
|
||||
for (final style in newStyle.getBlocksExceptHeader().values) {
|
||||
block = block..applyAttribute(style);
|
||||
}
|
||||
_wrap(block);
|
||||
block.adjust();
|
||||
}
|
||||
|
||||
/// Wraps this line with new parent [block].
|
||||
///
|
||||
/// This line can not be in a [Block] when this method is called.
|
||||
void _wrap(Block block) {
|
||||
assert(parent != null && parent is! Block);
|
||||
insertAfter(block);
|
||||
unlink();
|
||||
block.add(this);
|
||||
}
|
||||
|
||||
/// Unwraps this line from it's parent [Block].
|
||||
///
|
||||
/// This method asserts if current [parent] of this line is not a [Block].
|
||||
void _unwrap() {
|
||||
if (parent is! Block) {
|
||||
throw ArgumentError('Invalid parent');
|
||||
}
|
||||
final block = parent as Block;
|
||||
|
||||
assert(block.children.contains(this));
|
||||
|
||||
if (isFirst) {
|
||||
unlink();
|
||||
block.insertBefore(this);
|
||||
} else if (isLast) {
|
||||
unlink();
|
||||
block.insertAfter(this);
|
||||
} else {
|
||||
final before = block.clone() as Block;
|
||||
block.insertBefore(before);
|
||||
|
||||
var child = block.first as Line;
|
||||
while (child != this) {
|
||||
child.unlink();
|
||||
before.add(child);
|
||||
child = block.first as Line;
|
||||
}
|
||||
unlink();
|
||||
block.insertBefore(this);
|
||||
}
|
||||
block.adjust();
|
||||
}
|
||||
|
||||
Line _getNextLine(int index) {
|
||||
assert(index == 0 || (index > 0 && index < length));
|
||||
|
||||
final line = clone() as Line;
|
||||
insertAfter(line);
|
||||
if (index == length - 1) {
|
||||
return line;
|
||||
}
|
||||
|
||||
final query = queryChild(index, false);
|
||||
while (!query.node!.isLast) {
|
||||
final next = (last as Leaf)..unlink();
|
||||
line.addFirst(next);
|
||||
}
|
||||
final child = query.node as Leaf;
|
||||
final cut = child.splitAt(query.offset);
|
||||
cut?.unlink();
|
||||
line.addFirst(cut);
|
||||
return line;
|
||||
}
|
||||
|
||||
void _insertSafe(int index, Object data, Style? style) {
|
||||
assert(index == 0 || (index > 0 && index < length));
|
||||
|
||||
if (data is String) {
|
||||
assert(!data.contains('\n'));
|
||||
if (data.isEmpty) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (isEmpty) {
|
||||
final child = Leaf(data);
|
||||
add(child);
|
||||
child.format(style);
|
||||
} else {
|
||||
final result = queryChild(index, true);
|
||||
result.node!.insert(result.offset, data, style);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns style for specified text range.
|
||||
///
|
||||
/// Only attributes applied to all characters within this range are
|
||||
/// included in the result. Inline and line level attributes are
|
||||
/// handled separately, e.g.:
|
||||
///
|
||||
/// - line attribute X is included in the result only if it exists for
|
||||
/// every line within this range (partially included lines are counted).
|
||||
/// - inline attribute X is included in the result only if it exists
|
||||
/// for every character within this range (line-break characters excluded).
|
||||
Style collectStyle(int offset, int len) {
|
||||
final local = math.min(length - offset, len);
|
||||
var result = Style();
|
||||
final excluded = <Attribute>{};
|
||||
|
||||
void _handle(Style style) {
|
||||
if (result.isEmpty) {
|
||||
excluded.addAll(style.values);
|
||||
} else {
|
||||
for (final attr in result.values) {
|
||||
if (!style.containsKey(attr.key)) {
|
||||
excluded.add(attr);
|
||||
}
|
||||
}
|
||||
}
|
||||
final remaining = style.removeAll(excluded);
|
||||
result = result.removeAll(excluded);
|
||||
result = result.mergeAll(remaining);
|
||||
}
|
||||
|
||||
final data = queryChild(offset, true);
|
||||
var node = data.node as Leaf?;
|
||||
if (node != null) {
|
||||
result = result.mergeAll(node.style);
|
||||
var pos = node.length - data.offset;
|
||||
while (!node!.isLast && pos < local) {
|
||||
node = node.next as Leaf?;
|
||||
_handle(node!.style);
|
||||
pos += node.length;
|
||||
}
|
||||
}
|
||||
|
||||
result = result.mergeAll(style);
|
||||
if (parent is Block) {
|
||||
final block = parent as Block;
|
||||
result = result.mergeAll(block.style);
|
||||
}
|
||||
|
||||
final remaining = len - local;
|
||||
if (remaining > 0) {
|
||||
final rest = nextLine!.collectStyle(0, remaining);
|
||||
_handle(rest);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Returns all styles for any character within the specified text range.
|
||||
List<Style> collectAllStyles(int offset, int len) {
|
||||
final local = math.min(length - offset, len);
|
||||
final result = <Style>[];
|
||||
|
||||
final data = queryChild(offset, true);
|
||||
var node = data.node as Leaf?;
|
||||
if (node != null) {
|
||||
result.add(node.style);
|
||||
var pos = node.length - data.offset;
|
||||
while (!node!.isLast && pos < local) {
|
||||
node = node.next as Leaf?;
|
||||
result.add(node!.style);
|
||||
pos += node.length;
|
||||
}
|
||||
}
|
||||
|
||||
result.add(style);
|
||||
if (parent is Block) {
|
||||
final block = parent as Block;
|
||||
result.add(block.style);
|
||||
}
|
||||
|
||||
final remaining = len - local;
|
||||
if (remaining > 0) {
|
||||
final rest = nextLine!.collectAllStyles(0, remaining);
|
||||
result.addAll(rest);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
@ -1,134 +0,0 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import '../../quill_delta.dart';
|
||||
import '../attribute.dart';
|
||||
import '../style.dart';
|
||||
import 'container.dart';
|
||||
import 'line.dart';
|
||||
|
||||
/// An abstract node in a document tree.
|
||||
///
|
||||
/// Represents a segment of a Quill document with specified [offset]
|
||||
/// and [length].
|
||||
///
|
||||
/// The [offset] property is relative to [parent]. See also [documentOffset]
|
||||
/// which provides absolute offset of this node within the document.
|
||||
///
|
||||
/// The current parent node is exposed by the [parent] property.
|
||||
abstract class Node extends LinkedListEntry<Node> {
|
||||
/// Current parent of this node. May be null if this node is not mounted.
|
||||
Container? parent;
|
||||
|
||||
Style get style => _style;
|
||||
Style _style = Style();
|
||||
|
||||
/// Returns `true` if this node is the first node in the [parent] list.
|
||||
bool get isFirst => list!.first == this;
|
||||
|
||||
/// Returns `true` if this node is the last node in the [parent] list.
|
||||
bool get isLast => list!.last == this;
|
||||
|
||||
/// Length of this node in characters.
|
||||
int get length;
|
||||
|
||||
Node clone() => newInstance()..applyStyle(style);
|
||||
|
||||
/// Offset in characters of this node relative to [parent] node.
|
||||
///
|
||||
/// To get offset of this node in the document see [documentOffset].
|
||||
int get offset {
|
||||
var offset = 0;
|
||||
|
||||
if (list == null || isFirst) {
|
||||
return offset;
|
||||
}
|
||||
|
||||
var cur = this;
|
||||
do {
|
||||
cur = cur.previous!;
|
||||
offset += cur.length;
|
||||
} while (!cur.isFirst);
|
||||
return offset;
|
||||
}
|
||||
|
||||
/// Offset in characters of this node in the document.
|
||||
int get documentOffset {
|
||||
if (parent == null) {
|
||||
return offset;
|
||||
}
|
||||
final parentOffset = (parent is! Root) ? parent!.documentOffset : 0;
|
||||
return parentOffset + offset;
|
||||
}
|
||||
|
||||
/// Returns `true` if this node contains character at specified [offset] in
|
||||
/// the document.
|
||||
bool containsOffset(int offset) {
|
||||
final o = documentOffset;
|
||||
return o <= offset && offset < o + length;
|
||||
}
|
||||
|
||||
void applyAttribute(Attribute attribute) {
|
||||
_style = _style.merge(attribute);
|
||||
}
|
||||
|
||||
void applyStyle(Style value) {
|
||||
_style = _style.mergeAll(value);
|
||||
}
|
||||
|
||||
void clearStyle() {
|
||||
_style = Style();
|
||||
}
|
||||
|
||||
@override
|
||||
void insertBefore(Node entry) {
|
||||
assert(entry.parent == null && parent != null);
|
||||
entry.parent = parent;
|
||||
super.insertBefore(entry);
|
||||
}
|
||||
|
||||
@override
|
||||
void insertAfter(Node entry) {
|
||||
assert(entry.parent == null && parent != null);
|
||||
entry.parent = parent;
|
||||
super.insertAfter(entry);
|
||||
}
|
||||
|
||||
@override
|
||||
void unlink() {
|
||||
assert(parent != null);
|
||||
parent = null;
|
||||
super.unlink();
|
||||
}
|
||||
|
||||
void adjust() {/* no-op */}
|
||||
|
||||
/// abstract methods begin
|
||||
|
||||
Node newInstance();
|
||||
|
||||
String toPlainText();
|
||||
|
||||
Delta toDelta();
|
||||
|
||||
void insert(int index, Object data, Style? style);
|
||||
|
||||
void retain(int index, int? len, Style? style);
|
||||
|
||||
void delete(int index, int? len);
|
||||
|
||||
/// abstract methods end
|
||||
}
|
||||
|
||||
/// Root node of document tree.
|
||||
class Root extends Container<Container<Node?>> {
|
||||
@override
|
||||
Node newInstance() => Root();
|
||||
|
||||
@override
|
||||
Container<Node?> get defaultChild => Line();
|
||||
|
||||
@override
|
||||
Delta toDelta() => children
|
||||
.map((child) => child.toDelta())
|
||||
.fold(Delta(), (a, b) => a.concat(b));
|
||||
}
|
@ -1,128 +0,0 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:quiver/core.dart';
|
||||
|
||||
import 'attribute.dart';
|
||||
|
||||
/* Collection of style attributes */
|
||||
class Style {
|
||||
Style() : _attributes = <String, Attribute>{};
|
||||
|
||||
Style.attr(this._attributes);
|
||||
|
||||
final Map<String, Attribute> _attributes;
|
||||
|
||||
static Style fromJson(Map<String, dynamic>? attributes) {
|
||||
if (attributes == null) {
|
||||
return Style();
|
||||
}
|
||||
|
||||
final result = attributes.map((key, dynamic value) {
|
||||
final attr = Attribute.fromKeyValue(key, value);
|
||||
return MapEntry<String, Attribute>(
|
||||
key, attr ?? Attribute(key, AttributeScope.IGNORE, value));
|
||||
});
|
||||
return Style.attr(result);
|
||||
}
|
||||
|
||||
Map<String, dynamic>? toJson() => _attributes.isEmpty
|
||||
? null
|
||||
: _attributes.map<String, dynamic>((_, attribute) =>
|
||||
MapEntry<String, dynamic>(attribute.key, attribute.value));
|
||||
|
||||
Iterable<String> get keys => _attributes.keys;
|
||||
|
||||
Iterable<Attribute> get values => _attributes.values.sorted(
|
||||
(a, b) => Attribute.getRegistryOrder(a) - Attribute.getRegistryOrder(b));
|
||||
|
||||
Map<String, Attribute> get attributes => _attributes;
|
||||
|
||||
bool get isEmpty => _attributes.isEmpty;
|
||||
|
||||
bool get isNotEmpty => _attributes.isNotEmpty;
|
||||
|
||||
bool get isInline => isNotEmpty && values.every((item) => item.isInline);
|
||||
|
||||
bool get isIgnored =>
|
||||
isNotEmpty && values.every((item) => item.scope == AttributeScope.IGNORE);
|
||||
|
||||
Attribute get single => _attributes.values.single;
|
||||
|
||||
bool containsKey(String key) => _attributes.containsKey(key);
|
||||
|
||||
Attribute? getBlockExceptHeader() {
|
||||
for (final val in values) {
|
||||
if (val.isBlockExceptHeader && val.value != null) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
for (final val in values) {
|
||||
if (val.isBlockExceptHeader) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, Attribute> getBlocksExceptHeader() {
|
||||
final m = <String, Attribute>{};
|
||||
attributes.forEach((key, value) {
|
||||
if (Attribute.blockKeysExceptHeader.contains(key)) {
|
||||
m[key] = value;
|
||||
}
|
||||
});
|
||||
return m;
|
||||
}
|
||||
|
||||
Style merge(Attribute attribute) {
|
||||
final merged = Map<String, Attribute>.from(_attributes);
|
||||
if (attribute.value == null) {
|
||||
merged.remove(attribute.key);
|
||||
} else {
|
||||
merged[attribute.key] = attribute;
|
||||
}
|
||||
return Style.attr(merged);
|
||||
}
|
||||
|
||||
Style mergeAll(Style other) {
|
||||
var result = Style.attr(_attributes);
|
||||
for (final attribute in other.values) {
|
||||
result = result.merge(attribute);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Style removeAll(Set<Attribute> attributes) {
|
||||
final merged = Map<String, Attribute>.from(_attributes);
|
||||
attributes.map((item) => item.key).forEach(merged.remove);
|
||||
return Style.attr(merged);
|
||||
}
|
||||
|
||||
Style put(Attribute attribute) {
|
||||
final m = Map<String, Attribute>.from(attributes);
|
||||
m[attribute.key] = attribute;
|
||||
return Style.attr(m);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
if (other is! Style) {
|
||||
return false;
|
||||
}
|
||||
final typedOther = other;
|
||||
const eq = MapEquality<String, Attribute>();
|
||||
return eq.equals(_attributes, typedOther._attributes);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
final hashes =
|
||||
_attributes.entries.map((entry) => hash2(entry.key, entry.value));
|
||||
return hashObjects(hashes);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => "{${_attributes.values.join(', ')}}";
|
||||
}
|
@ -1,803 +0,0 @@
|
||||
// Copyright (c) 2018, Anatoly Pulyaevskiy. All rights reserved. Use of this
|
||||
// source code is governed by a BSD-style license that can be found in the
|
||||
// LICENSE file.
|
||||
|
||||
/// Implementation of Quill Delta format in Dart.
|
||||
library quill_delta;
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:diff_match_patch/diff_match_patch.dart' as dmp;
|
||||
import 'package:quiver/core.dart';
|
||||
|
||||
const _attributeEquality = DeepCollectionEquality();
|
||||
const _valueEquality = DeepCollectionEquality();
|
||||
|
||||
/// Decoder function to convert raw `data` object into a user-defined data type.
|
||||
///
|
||||
/// Useful with embedded content.
|
||||
typedef DataDecoder = Object? Function(Object data);
|
||||
|
||||
/// Default data decoder which simply passes through the original value.
|
||||
Object? _passThroughDataDecoder(Object? data) => data;
|
||||
|
||||
/// Operation performed on a rich-text document.
|
||||
class Operation {
|
||||
Operation._(this.key, this.length, this.data, Map? attributes)
|
||||
: assert(_validKeys.contains(key), 'Invalid operation key "$key".'),
|
||||
assert(() {
|
||||
if (key != Operation.insertKey) return true;
|
||||
return data is String ? data.length == length : length == 1;
|
||||
}(), 'Length of insert operation must be equal to the data length.'),
|
||||
_attributes =
|
||||
attributes != null ? Map<String, dynamic>.from(attributes) : null;
|
||||
|
||||
/// Creates operation which deletes [length] of characters.
|
||||
factory Operation.delete(int length) =>
|
||||
Operation._(Operation.deleteKey, length, '', null);
|
||||
|
||||
/// Creates operation which inserts [text] with optional [attributes].
|
||||
factory Operation.insert(dynamic data, [Map<String, dynamic>? attributes]) =>
|
||||
Operation._(Operation.insertKey, data is String ? data.length : 1, data,
|
||||
attributes);
|
||||
|
||||
/// Creates operation which retains [length] of characters and optionally
|
||||
/// applies attributes.
|
||||
factory Operation.retain(int? length, [Map<String, dynamic>? attributes]) =>
|
||||
Operation._(Operation.retainKey, length, '', attributes);
|
||||
|
||||
/// Key of insert operations.
|
||||
static const String insertKey = 'insert';
|
||||
|
||||
/// Key of delete operations.
|
||||
static const String deleteKey = 'delete';
|
||||
|
||||
/// Key of retain operations.
|
||||
static const String retainKey = 'retain';
|
||||
|
||||
/// Key of attributes collection.
|
||||
static const String attributesKey = 'attributes';
|
||||
|
||||
static const List<String> _validKeys = [insertKey, deleteKey, retainKey];
|
||||
|
||||
/// Key of this operation, can be "insert", "delete" or "retain".
|
||||
final String key;
|
||||
|
||||
/// Length of this operation.
|
||||
final int? length;
|
||||
|
||||
/// Payload of "insert" operation, for other types is set to empty string.
|
||||
final Object? data;
|
||||
|
||||
/// Rich-text attributes set by this operation, can be `null`.
|
||||
Map<String, dynamic>? get attributes =>
|
||||
_attributes == null ? null : Map<String, dynamic>.from(_attributes!);
|
||||
final Map<String, dynamic>? _attributes;
|
||||
|
||||
/// Creates new [Operation] from JSON payload.
|
||||
///
|
||||
/// If `dataDecoder` parameter is not null then it is used to additionally
|
||||
/// decode the operation's data object. Only applied to insert operations.
|
||||
static Operation fromJson(Map data, {DataDecoder? dataDecoder}) {
|
||||
dataDecoder ??= _passThroughDataDecoder;
|
||||
final map = Map<String, dynamic>.from(data);
|
||||
if (map.containsKey(Operation.insertKey)) {
|
||||
final data = dataDecoder(map[Operation.insertKey]);
|
||||
final dataLength = data is String ? data.length : 1;
|
||||
return Operation._(
|
||||
Operation.insertKey, dataLength, data, map[Operation.attributesKey]);
|
||||
} else if (map.containsKey(Operation.deleteKey)) {
|
||||
final int? length = map[Operation.deleteKey];
|
||||
return Operation._(Operation.deleteKey, length, '', null);
|
||||
} else if (map.containsKey(Operation.retainKey)) {
|
||||
final int? length = map[Operation.retainKey];
|
||||
return Operation._(
|
||||
Operation.retainKey, length, '', map[Operation.attributesKey]);
|
||||
}
|
||||
throw ArgumentError.value(data, 'Invalid data for Delta operation.');
|
||||
}
|
||||
|
||||
/// Returns JSON-serializable representation of this operation.
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = {key: value};
|
||||
if (_attributes != null) json[Operation.attributesKey] = attributes;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns value of this operation.
|
||||
///
|
||||
/// For insert operations this returns text, for delete and retain - length.
|
||||
dynamic get value => (key == Operation.insertKey) ? data : length;
|
||||
|
||||
/// Returns `true` if this is a delete operation.
|
||||
bool get isDelete => key == Operation.deleteKey;
|
||||
|
||||
/// Returns `true` if this is an insert operation.
|
||||
bool get isInsert => key == Operation.insertKey;
|
||||
|
||||
/// Returns `true` if this is a retain operation.
|
||||
bool get isRetain => key == Operation.retainKey;
|
||||
|
||||
/// Returns `true` if this operation has no attributes, e.g. is plain text.
|
||||
bool get isPlain => _attributes == null || _attributes!.isEmpty;
|
||||
|
||||
/// Returns `true` if this operation sets at least one attribute.
|
||||
bool get isNotPlain => !isPlain;
|
||||
|
||||
/// Returns `true` is this operation is empty.
|
||||
///
|
||||
/// An operation is considered empty if its [length] is equal to `0`.
|
||||
bool get isEmpty => length == 0;
|
||||
|
||||
/// Returns `true` is this operation is not empty.
|
||||
bool get isNotEmpty => length! > 0;
|
||||
|
||||
@override
|
||||
bool operator ==(other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! Operation) return false;
|
||||
final typedOther = other;
|
||||
return key == typedOther.key &&
|
||||
length == typedOther.length &&
|
||||
_valueEquality.equals(data, typedOther.data) &&
|
||||
hasSameAttributes(typedOther);
|
||||
}
|
||||
|
||||
/// Returns `true` if this operation has attribute specified by [name].
|
||||
bool hasAttribute(String name) =>
|
||||
isNotPlain && _attributes!.containsKey(name);
|
||||
|
||||
/// Returns `true` if [other] operation has the same attributes as this one.
|
||||
bool hasSameAttributes(Operation other) {
|
||||
return _attributeEquality.equals(_attributes, other._attributes);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
if (_attributes != null && _attributes!.isNotEmpty) {
|
||||
final attrsHash =
|
||||
hashObjects(_attributes!.entries.map((e) => hash2(e.key, e.value)));
|
||||
return hash3(key, value, attrsHash);
|
||||
}
|
||||
return hash2(key, value);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final attr = attributes == null ? '' : ' + $attributes';
|
||||
final text = isInsert
|
||||
? (data is String
|
||||
? (data as String).replaceAll('\n', '⏎')
|
||||
: data.toString())
|
||||
: '$length';
|
||||
return '$key⟨ $text ⟩$attr';
|
||||
}
|
||||
}
|
||||
|
||||
/// Delta represents a document or a modification of a document as a sequence of
|
||||
/// insert, delete and retain operations.
|
||||
///
|
||||
/// Delta consisting of only "insert" operations is usually referred to as
|
||||
/// "document delta". When delta includes also "retain" or "delete" operations
|
||||
/// it is a "change delta".
|
||||
class Delta {
|
||||
/// Creates new empty [Delta].
|
||||
factory Delta() => Delta._(<Operation>[]);
|
||||
|
||||
Delta._(List<Operation> operations) : _operations = operations;
|
||||
|
||||
/// Creates new [Delta] from [other].
|
||||
factory Delta.from(Delta other) =>
|
||||
Delta._(List<Operation>.from(other._operations));
|
||||
|
||||
// Placeholder char for embed in diff()
|
||||
static final String _kNullCharacter = String.fromCharCode(0);
|
||||
|
||||
/// Transforms two attribute sets.
|
||||
static Map<String, dynamic>? transformAttributes(
|
||||
Map<String, dynamic>? a, Map<String, dynamic>? b, bool priority) {
|
||||
if (a == null) return b;
|
||||
if (b == null) return null;
|
||||
|
||||
if (!priority) return b;
|
||||
|
||||
final result = b.keys.fold<Map<String, dynamic>>({}, (attributes, key) {
|
||||
if (!a.containsKey(key)) attributes[key] = b[key];
|
||||
return attributes;
|
||||
});
|
||||
|
||||
return result.isEmpty ? null : result;
|
||||
}
|
||||
|
||||
/// Composes two attribute sets.
|
||||
static Map<String, dynamic>? composeAttributes(
|
||||
Map<String, dynamic>? a, Map<String, dynamic>? b,
|
||||
{bool keepNull = false}) {
|
||||
a ??= const {};
|
||||
b ??= const {};
|
||||
|
||||
final result = Map<String, dynamic>.from(a)..addAll(b);
|
||||
final keys = result.keys.toList(growable: false);
|
||||
|
||||
if (!keepNull) {
|
||||
for (final key in keys) {
|
||||
if (result[key] == null) result.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
return result.isEmpty ? null : result;
|
||||
}
|
||||
|
||||
///get anti-attr result base on base
|
||||
static Map<String, dynamic> invertAttributes(
|
||||
Map<String, dynamic>? attr, Map<String, dynamic>? base) {
|
||||
attr ??= const {};
|
||||
base ??= const {};
|
||||
|
||||
final baseInverted = base.keys.fold({}, (dynamic memo, key) {
|
||||
if (base![key] != attr![key] && attr.containsKey(key)) {
|
||||
memo[key] = base[key];
|
||||
}
|
||||
return memo;
|
||||
});
|
||||
|
||||
final inverted =
|
||||
Map<String, dynamic>.from(attr.keys.fold(baseInverted, (memo, key) {
|
||||
if (base![key] != attr![key] && !base.containsKey(key)) {
|
||||
memo[key] = null;
|
||||
}
|
||||
return memo;
|
||||
}));
|
||||
return inverted;
|
||||
}
|
||||
|
||||
/// Returns diff between two attribute sets
|
||||
static Map<String, dynamic>? diffAttributes(
|
||||
Map<String, dynamic>? a, Map<String, dynamic>? b) {
|
||||
a ??= const {};
|
||||
b ??= const {};
|
||||
|
||||
final attributes = <String, dynamic>{};
|
||||
(a.keys.toList()..addAll(b.keys)).forEach((key) {
|
||||
if (a![key] != b![key]) {
|
||||
attributes[key] = b.containsKey(key) ? b[key] : null;
|
||||
}
|
||||
});
|
||||
|
||||
return attributes.keys.isNotEmpty ? attributes : null;
|
||||
}
|
||||
|
||||
final List<Operation> _operations;
|
||||
|
||||
int _modificationCount = 0;
|
||||
|
||||
/// Creates [Delta] from de-serialized JSON representation.
|
||||
///
|
||||
/// If `dataDecoder` parameter is not null then it is used to additionally
|
||||
/// decode the operation's data object. Only applied to insert operations.
|
||||
static Delta fromJson(List data, {DataDecoder? dataDecoder}) {
|
||||
return Delta._(data
|
||||
.map((op) => Operation.fromJson(op, dataDecoder: dataDecoder))
|
||||
.toList());
|
||||
}
|
||||
|
||||
/// Returns list of operations in this delta.
|
||||
List<Operation> toList() => List.from(_operations);
|
||||
|
||||
/// Returns JSON-serializable version of this delta.
|
||||
List toJson() => toList().map((operation) => operation.toJson()).toList();
|
||||
|
||||
/// Returns `true` if this delta is empty.
|
||||
bool get isEmpty => _operations.isEmpty;
|
||||
|
||||
/// Returns `true` if this delta is not empty.
|
||||
bool get isNotEmpty => _operations.isNotEmpty;
|
||||
|
||||
/// Returns number of operations in this delta.
|
||||
int get length => _operations.length;
|
||||
|
||||
/// Returns [Operation] at specified [index] in this delta.
|
||||
Operation operator [](int index) => _operations[index];
|
||||
|
||||
/// Returns [Operation] at specified [index] in this delta.
|
||||
Operation elementAt(int index) => _operations.elementAt(index);
|
||||
|
||||
/// Returns the first [Operation] in this delta.
|
||||
Operation get first => _operations.first;
|
||||
|
||||
/// Returns the last [Operation] in this delta.
|
||||
Operation get last => _operations.last;
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! Delta) return false;
|
||||
final typedOther = other;
|
||||
const comparator = ListEquality<Operation>(DefaultEquality<Operation>());
|
||||
return comparator.equals(_operations, typedOther._operations);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => hashObjects(_operations);
|
||||
|
||||
/// Retain [count] of characters from current position.
|
||||
void retain(int count, [Map<String, dynamic>? attributes]) {
|
||||
assert(count >= 0);
|
||||
if (count == 0) return; // no-op
|
||||
push(Operation.retain(count, attributes));
|
||||
}
|
||||
|
||||
/// Insert [data] at current position.
|
||||
void insert(dynamic data, [Map<String, dynamic>? attributes]) {
|
||||
if (data is String && data.isEmpty) return; // no-op
|
||||
push(Operation.insert(data, attributes));
|
||||
}
|
||||
|
||||
/// Delete [count] characters from current position.
|
||||
void delete(int count) {
|
||||
assert(count >= 0);
|
||||
if (count == 0) return;
|
||||
push(Operation.delete(count));
|
||||
}
|
||||
|
||||
void _mergeWithTail(Operation operation) {
|
||||
assert(isNotEmpty);
|
||||
assert(last.key == operation.key);
|
||||
assert(operation.data is String && last.data is String);
|
||||
|
||||
final length = operation.length! + last.length!;
|
||||
final lastText = last.data as String;
|
||||
final opText = operation.data as String;
|
||||
final resultText = lastText + opText;
|
||||
final index = _operations.length;
|
||||
_operations.replaceRange(index - 1, index, [
|
||||
Operation._(operation.key, length, resultText, operation.attributes),
|
||||
]);
|
||||
}
|
||||
|
||||
/// Pushes new operation into this delta.
|
||||
///
|
||||
/// Performs compaction by composing [operation] with current tail operation
|
||||
/// of this delta, when possible. For instance, if current tail is
|
||||
/// `insert('abc')` and pushed operation is `insert('123')` then existing
|
||||
/// tail is replaced with `insert('abc123')` - a compound result of the two
|
||||
/// operations.
|
||||
void push(Operation operation) {
|
||||
if (operation.isEmpty) return;
|
||||
|
||||
var index = _operations.length;
|
||||
final lastOp = _operations.isNotEmpty ? _operations.last : null;
|
||||
if (lastOp != null) {
|
||||
if (lastOp.isDelete && operation.isDelete) {
|
||||
_mergeWithTail(operation);
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastOp.isDelete && operation.isInsert) {
|
||||
index -= 1; // Always insert before deleting
|
||||
final nLastOp = (index > 0) ? _operations.elementAt(index - 1) : null;
|
||||
if (nLastOp == null) {
|
||||
_operations.insert(0, operation);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastOp.isInsert && operation.isInsert) {
|
||||
if (lastOp.hasSameAttributes(operation) &&
|
||||
operation.data is String &&
|
||||
lastOp.data is String) {
|
||||
_mergeWithTail(operation);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastOp.isRetain && operation.isRetain) {
|
||||
if (lastOp.hasSameAttributes(operation)) {
|
||||
_mergeWithTail(operation);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (index == _operations.length) {
|
||||
_operations.add(operation);
|
||||
} else {
|
||||
final opAtIndex = _operations.elementAt(index);
|
||||
_operations.replaceRange(index, index + 1, [operation, opAtIndex]);
|
||||
}
|
||||
_modificationCount++;
|
||||
}
|
||||
|
||||
/// Composes next operation from [thisIter] and [otherIter].
|
||||
///
|
||||
/// Returns new operation or `null` if operations from [thisIter] and
|
||||
/// [otherIter] nullify each other. For instance, for the pair `insert('abc')`
|
||||
/// and `delete(3)` composition result would be empty string.
|
||||
Operation? _composeOperation(
|
||||
DeltaIterator thisIter, DeltaIterator otherIter) {
|
||||
if (otherIter.isNextInsert) return otherIter.next();
|
||||
if (thisIter.isNextDelete) return thisIter.next();
|
||||
|
||||
final length = math.min(thisIter.peekLength(), otherIter.peekLength());
|
||||
final thisOp = thisIter.next(length);
|
||||
final otherOp = otherIter.next(length);
|
||||
assert(thisOp.length == otherOp.length);
|
||||
|
||||
if (otherOp.isRetain) {
|
||||
final attributes = composeAttributes(
|
||||
thisOp.attributes,
|
||||
otherOp.attributes,
|
||||
keepNull: thisOp.isRetain,
|
||||
);
|
||||
if (thisOp.isRetain) {
|
||||
return Operation.retain(thisOp.length, attributes);
|
||||
} else if (thisOp.isInsert) {
|
||||
return Operation.insert(thisOp.data, attributes);
|
||||
} else {
|
||||
throw StateError('Unreachable');
|
||||
}
|
||||
} else {
|
||||
// otherOp == delete && thisOp in [retain, insert]
|
||||
assert(otherOp.isDelete);
|
||||
if (thisOp.isRetain) return otherOp;
|
||||
assert(thisOp.isInsert);
|
||||
// otherOp(delete) + thisOp(insert) => null
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Composes this delta with [other] and returns new [Delta].
|
||||
///
|
||||
/// It is not required for this and [other] delta to represent a document
|
||||
/// delta (consisting only of insert operations).
|
||||
Delta compose(Delta other) {
|
||||
final result = Delta();
|
||||
final thisIter = DeltaIterator(this);
|
||||
final otherIter = DeltaIterator(other);
|
||||
|
||||
while (thisIter.hasNext || otherIter.hasNext) {
|
||||
final newOp = _composeOperation(thisIter, otherIter);
|
||||
if (newOp != null) result.push(newOp);
|
||||
}
|
||||
return result..trim();
|
||||
}
|
||||
|
||||
/// Returns a new lazy Iterable with elements that are created by calling
|
||||
/// f on each element of this Iterable in iteration order.
|
||||
///
|
||||
/// Convenience method
|
||||
Iterable<T> map<T>(T Function(Operation) f) {
|
||||
return _operations.map<T>(f);
|
||||
}
|
||||
|
||||
/// Returns a [Delta] containing differences between 2 [Delta]s.
|
||||
/// If [cleanupSemantic] is `true` (default), applies the following:
|
||||
///
|
||||
/// The diff of "mouse" and "sofas" is
|
||||
/// [delete(1), insert("s"), retain(1),
|
||||
/// delete("u"), insert("fa"), retain(1), delete(1)].
|
||||
/// While this is the optimum diff, it is difficult for humans to understand.
|
||||
/// Semantic cleanup rewrites the diff,
|
||||
/// expanding it into a more intelligible format.
|
||||
/// The above example would become: [(-1, "mouse"), (1, "sofas")].
|
||||
/// (source: https://github.com/google/diff-match-patch/wiki/API)
|
||||
///
|
||||
/// Useful when one wishes to display difference between 2 documents
|
||||
Delta diff(Delta other, {bool cleanupSemantic = true}) {
|
||||
if (_operations.equals(other._operations)) {
|
||||
return Delta();
|
||||
}
|
||||
final stringThis = map((op) {
|
||||
if (op.isInsert) {
|
||||
return op.data is String ? op.data : _kNullCharacter;
|
||||
}
|
||||
final prep = this == other ? 'on' : 'with';
|
||||
throw ArgumentError('diff() call $prep non-document');
|
||||
}).join();
|
||||
final stringOther = other.map((op) {
|
||||
if (op.isInsert) {
|
||||
return op.data is String ? op.data : _kNullCharacter;
|
||||
}
|
||||
final prep = this == other ? 'on' : 'with';
|
||||
throw ArgumentError('diff() call $prep non-document');
|
||||
}).join();
|
||||
|
||||
final retDelta = Delta();
|
||||
final diffResult = dmp.diff(stringThis, stringOther);
|
||||
if (cleanupSemantic) {
|
||||
dmp.DiffMatchPatch().diffCleanupSemantic(diffResult);
|
||||
}
|
||||
|
||||
final thisIter = DeltaIterator(this);
|
||||
final otherIter = DeltaIterator(other);
|
||||
|
||||
diffResult.forEach((component) {
|
||||
var length = component.text.length;
|
||||
while (length > 0) {
|
||||
var opLength = 0;
|
||||
switch (component.operation) {
|
||||
case dmp.DIFF_INSERT:
|
||||
opLength = math.min(otherIter.peekLength(), length);
|
||||
retDelta.push(otherIter.next(opLength));
|
||||
break;
|
||||
case dmp.DIFF_DELETE:
|
||||
opLength = math.min(length, thisIter.peekLength());
|
||||
thisIter.next(opLength);
|
||||
retDelta.delete(opLength);
|
||||
break;
|
||||
case dmp.DIFF_EQUAL:
|
||||
opLength = math.min(
|
||||
math.min(thisIter.peekLength(), otherIter.peekLength()),
|
||||
length,
|
||||
);
|
||||
final thisOp = thisIter.next(opLength);
|
||||
final otherOp = otherIter.next(opLength);
|
||||
if (thisOp.data == otherOp.data) {
|
||||
retDelta.retain(
|
||||
opLength,
|
||||
diffAttributes(thisOp.attributes, otherOp.attributes),
|
||||
);
|
||||
} else {
|
||||
retDelta
|
||||
..push(otherOp)
|
||||
..delete(opLength);
|
||||
}
|
||||
break;
|
||||
}
|
||||
length -= opLength;
|
||||
}
|
||||
});
|
||||
return retDelta..trim();
|
||||
}
|
||||
|
||||
/// Transforms next operation from [otherIter] against next operation in
|
||||
/// [thisIter].
|
||||
///
|
||||
/// Returns `null` if both operations nullify each other.
|
||||
Operation? _transformOperation(
|
||||
DeltaIterator thisIter, DeltaIterator otherIter, bool priority) {
|
||||
if (thisIter.isNextInsert && (priority || !otherIter.isNextInsert)) {
|
||||
return Operation.retain(thisIter.next().length);
|
||||
} else if (otherIter.isNextInsert) {
|
||||
return otherIter.next();
|
||||
}
|
||||
|
||||
final length = math.min(thisIter.peekLength(), otherIter.peekLength());
|
||||
final thisOp = thisIter.next(length);
|
||||
final otherOp = otherIter.next(length);
|
||||
assert(thisOp.length == otherOp.length);
|
||||
|
||||
// At this point only delete and retain operations are possible.
|
||||
if (thisOp.isDelete) {
|
||||
// otherOp is either delete or retain, so they nullify each other.
|
||||
return null;
|
||||
} else if (otherOp.isDelete) {
|
||||
return otherOp;
|
||||
} else {
|
||||
// Retain otherOp which is either retain or insert.
|
||||
return Operation.retain(
|
||||
length,
|
||||
transformAttributes(thisOp.attributes, otherOp.attributes, priority),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transforms [other] delta against operations in this delta.
|
||||
Delta transform(Delta other, bool priority) {
|
||||
final result = Delta();
|
||||
final thisIter = DeltaIterator(this);
|
||||
final otherIter = DeltaIterator(other);
|
||||
|
||||
while (thisIter.hasNext || otherIter.hasNext) {
|
||||
final newOp = _transformOperation(thisIter, otherIter, priority);
|
||||
if (newOp != null) result.push(newOp);
|
||||
}
|
||||
return result..trim();
|
||||
}
|
||||
|
||||
/// Removes trailing retain operation with empty attributes, if present.
|
||||
void trim() {
|
||||
if (isNotEmpty) {
|
||||
final last = _operations.last;
|
||||
if (last.isRetain && last.isPlain) _operations.removeLast();
|
||||
}
|
||||
}
|
||||
|
||||
/// Concatenates [other] with this delta and returns the result.
|
||||
Delta concat(Delta other) {
|
||||
final result = Delta.from(this);
|
||||
if (other.isNotEmpty) {
|
||||
// In case first operation of other can be merged with last operation in
|
||||
// our list.
|
||||
result.push(other._operations.first);
|
||||
result._operations.addAll(other._operations.sublist(1));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Inverts this delta against [base].
|
||||
///
|
||||
/// Returns new delta which negates effect of this delta when applied to
|
||||
/// [base]. This is an equivalent of "undo" operation on deltas.
|
||||
Delta invert(Delta base) {
|
||||
final inverted = Delta();
|
||||
if (base.isEmpty) return inverted;
|
||||
|
||||
var baseIndex = 0;
|
||||
for (final op in _operations) {
|
||||
if (op.isInsert) {
|
||||
inverted.delete(op.length!);
|
||||
} else if (op.isRetain && op.isPlain) {
|
||||
inverted.retain(op.length!);
|
||||
baseIndex += op.length!;
|
||||
} else if (op.isDelete || (op.isRetain && op.isNotPlain)) {
|
||||
final length = op.length!;
|
||||
final sliceDelta = base.slice(baseIndex, baseIndex + length);
|
||||
sliceDelta.toList().forEach((baseOp) {
|
||||
if (op.isDelete) {
|
||||
inverted.push(baseOp);
|
||||
} else if (op.isRetain && op.isNotPlain) {
|
||||
final invertAttr =
|
||||
invertAttributes(op.attributes, baseOp.attributes);
|
||||
inverted.retain(
|
||||
baseOp.length!, invertAttr.isEmpty ? null : invertAttr);
|
||||
}
|
||||
});
|
||||
baseIndex += length;
|
||||
} else {
|
||||
throw StateError('Unreachable');
|
||||
}
|
||||
}
|
||||
inverted.trim();
|
||||
return inverted;
|
||||
}
|
||||
|
||||
/// Returns slice of this delta from [start] index (inclusive) to [end]
|
||||
/// (exclusive).
|
||||
Delta slice(int start, [int? end]) {
|
||||
final delta = Delta();
|
||||
var index = 0;
|
||||
final opIterator = DeltaIterator(this);
|
||||
|
||||
final actualEnd = end ?? DeltaIterator.maxLength;
|
||||
|
||||
while (index < actualEnd && opIterator.hasNext) {
|
||||
Operation op;
|
||||
if (index < start) {
|
||||
op = opIterator.next(start - index);
|
||||
} else {
|
||||
op = opIterator.next(actualEnd - index);
|
||||
delta.push(op);
|
||||
}
|
||||
index += op.length!;
|
||||
}
|
||||
return delta;
|
||||
}
|
||||
|
||||
/// Transforms [index] against this delta.
|
||||
///
|
||||
/// Any "delete" operation before specified [index] shifts it backward, as
|
||||
/// well as any "insert" operation shifts it forward.
|
||||
///
|
||||
/// The [force] argument is used to resolve scenarios when there is an
|
||||
/// insert operation at the same position as [index]. If [force] is set to
|
||||
/// `true` (default) then position is forced to shift forward, otherwise
|
||||
/// position stays at the same index. In other words setting [force] to
|
||||
/// `false` gives higher priority to the transformed position.
|
||||
///
|
||||
/// Useful to adjust caret or selection positions.
|
||||
int transformPosition(int index, {bool force = true}) {
|
||||
final iter = DeltaIterator(this);
|
||||
var offset = 0;
|
||||
while (iter.hasNext && offset <= index) {
|
||||
final op = iter.next();
|
||||
if (op.isDelete) {
|
||||
index -= math.min(op.length!, index - offset);
|
||||
continue;
|
||||
} else if (op.isInsert && (offset < index || force)) {
|
||||
index += op.length!;
|
||||
}
|
||||
offset += op.length!;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => _operations.join('\n');
|
||||
}
|
||||
|
||||
/// Specialized iterator for [Delta]s.
|
||||
class DeltaIterator {
|
||||
DeltaIterator(this.delta) : _modificationCount = delta._modificationCount;
|
||||
|
||||
static const int maxLength = 1073741824;
|
||||
|
||||
final Delta delta;
|
||||
final int _modificationCount;
|
||||
int _index = 0;
|
||||
int _offset = 0;
|
||||
|
||||
bool get isNextInsert => nextOperationKey == Operation.insertKey;
|
||||
|
||||
bool get isNextDelete => nextOperationKey == Operation.deleteKey;
|
||||
|
||||
bool get isNextRetain => nextOperationKey == Operation.retainKey;
|
||||
|
||||
String? get nextOperationKey {
|
||||
if (_index < delta.length) {
|
||||
return delta.elementAt(_index).key;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
bool get hasNext => peekLength() < maxLength;
|
||||
|
||||
/// Returns length of next operation without consuming it.
|
||||
///
|
||||
/// Returns [maxLength] if there is no more operations left to iterate.
|
||||
int peekLength() {
|
||||
if (_index < delta.length) {
|
||||
final operation = delta._operations[_index];
|
||||
return operation.length! - _offset;
|
||||
}
|
||||
return maxLength;
|
||||
}
|
||||
|
||||
/// Consumes and returns next operation.
|
||||
///
|
||||
/// Optional [length] specifies maximum length of operation to return. Note
|
||||
/// that actual length of returned operation may be less than specified value.
|
||||
///
|
||||
/// If this iterator reached the end of the Delta then returns a retain
|
||||
/// operation with its length set to [maxLength].
|
||||
// TODO: Note that we used double.infinity as the default value
|
||||
// for length here
|
||||
// but this can now cause a type error since operation length is
|
||||
// expected to be an int. Changing default length to [maxLength] is
|
||||
// a workaround to avoid breaking changes.
|
||||
Operation next([int length = maxLength]) {
|
||||
if (_modificationCount != delta._modificationCount) {
|
||||
throw ConcurrentModificationError(delta);
|
||||
}
|
||||
|
||||
if (_index < delta.length) {
|
||||
final op = delta.elementAt(_index);
|
||||
final opKey = op.key;
|
||||
final opAttributes = op.attributes;
|
||||
final _currentOffset = _offset;
|
||||
final actualLength = math.min(op.length! - _currentOffset, length);
|
||||
if (actualLength == op.length! - _currentOffset) {
|
||||
_index++;
|
||||
_offset = 0;
|
||||
} else {
|
||||
_offset += actualLength;
|
||||
}
|
||||
final opData = op.isInsert && op.data is String
|
||||
? (op.data as String)
|
||||
.substring(_currentOffset, _currentOffset + actualLength)
|
||||
: op.data;
|
||||
final opIsNotEmpty =
|
||||
opData is String ? opData.isNotEmpty : true; // embeds are never empty
|
||||
final opLength = opData is String ? opData.length : 1;
|
||||
final opActualLength = opIsNotEmpty ? opLength : actualLength;
|
||||
return Operation._(opKey, opActualLength, opData, opAttributes);
|
||||
}
|
||||
return Operation.retain(length);
|
||||
}
|
||||
|
||||
/// Skips [length] characters in source delta.
|
||||
///
|
||||
/// Returns last skipped operation, or `null` if there was nothing to skip.
|
||||
Operation? skip(int length) {
|
||||
var skipped = 0;
|
||||
Operation? op;
|
||||
while (skipped < length && hasNext) {
|
||||
final opLength = peekLength();
|
||||
final skip = math.min(length - skipped, opLength);
|
||||
op = next(skip);
|
||||
skipped += op.length!;
|
||||
}
|
||||
return op;
|
||||
}
|
||||
}
|
@ -1,126 +0,0 @@
|
||||
import '../documents/attribute.dart';
|
||||
import '../quill_delta.dart';
|
||||
import 'rule.dart';
|
||||
|
||||
abstract class DeleteRule extends Rule {
|
||||
const DeleteRule();
|
||||
|
||||
@override
|
||||
RuleType get type => RuleType.DELETE;
|
||||
|
||||
@override
|
||||
void validateArgs(int? len, Object? data, Attribute? attribute) {
|
||||
assert(len != null);
|
||||
assert(data == null);
|
||||
assert(attribute == null);
|
||||
}
|
||||
}
|
||||
|
||||
class CatchAllDeleteRule extends DeleteRule {
|
||||
const CatchAllDeleteRule();
|
||||
|
||||
@override
|
||||
Delta applyRule(Delta document, int index,
|
||||
{int? len, Object? data, Attribute? attribute}) {
|
||||
return Delta()
|
||||
..retain(index)
|
||||
..delete(len!);
|
||||
}
|
||||
}
|
||||
|
||||
class PreserveLineStyleOnMergeRule extends DeleteRule {
|
||||
const PreserveLineStyleOnMergeRule();
|
||||
|
||||
@override
|
||||
Delta? applyRule(Delta document, int index,
|
||||
{int? len, Object? data, Attribute? attribute}) {
|
||||
final itr = DeltaIterator(document)..skip(index);
|
||||
var op = itr.next(1);
|
||||
if (op.data != '\n') {
|
||||
return null;
|
||||
}
|
||||
|
||||
final isNotPlain = op.isNotPlain;
|
||||
final attrs = op.attributes;
|
||||
|
||||
itr.skip(len! - 1);
|
||||
final delta = Delta()
|
||||
..retain(index)
|
||||
..delete(len);
|
||||
|
||||
while (itr.hasNext) {
|
||||
op = itr.next();
|
||||
final text = op.data is String ? (op.data as String?)! : '';
|
||||
final lineBreak = text.indexOf('\n');
|
||||
if (lineBreak == -1) {
|
||||
delta.retain(op.length!);
|
||||
continue;
|
||||
}
|
||||
|
||||
var attributes = op.attributes == null
|
||||
? null
|
||||
: op.attributes!.map<String, dynamic>(
|
||||
(key, dynamic value) => MapEntry<String, dynamic>(key, null));
|
||||
|
||||
if (isNotPlain) {
|
||||
attributes ??= <String, dynamic>{};
|
||||
attributes.addAll(attrs!);
|
||||
}
|
||||
delta
|
||||
..retain(lineBreak)
|
||||
..retain(1, attributes);
|
||||
break;
|
||||
}
|
||||
return delta;
|
||||
}
|
||||
}
|
||||
|
||||
class EnsureEmbedLineRule extends DeleteRule {
|
||||
const EnsureEmbedLineRule();
|
||||
|
||||
@override
|
||||
Delta? applyRule(Delta document, int index,
|
||||
{int? len, Object? data, Attribute? attribute}) {
|
||||
final itr = DeltaIterator(document);
|
||||
|
||||
var op = itr.skip(index);
|
||||
int? indexDelta = 0, lengthDelta = 0, remain = len;
|
||||
var embedFound = op != null && op.data is! String;
|
||||
final hasLineBreakBefore =
|
||||
!embedFound && (op == null || (op.data as String).endsWith('\n'));
|
||||
if (embedFound) {
|
||||
var candidate = itr.next(1);
|
||||
if (remain != null) {
|
||||
remain--;
|
||||
if (candidate.data == '\n') {
|
||||
indexDelta++;
|
||||
lengthDelta--;
|
||||
|
||||
candidate = itr.next(1);
|
||||
remain--;
|
||||
if (candidate.data == '\n') {
|
||||
lengthDelta++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
op = itr.skip(remain!);
|
||||
if (op != null &&
|
||||
(op.data is String ? op.data as String? : '')!.endsWith('\n')) {
|
||||
final candidate = itr.next(1);
|
||||
if (candidate.data is! String && !hasLineBreakBefore) {
|
||||
embedFound = true;
|
||||
lengthDelta--;
|
||||
}
|
||||
}
|
||||
|
||||
if (!embedFound) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Delta()
|
||||
..retain(index + indexDelta)
|
||||
..delete(len! + lengthDelta);
|
||||
}
|
||||
}
|
@ -1,161 +0,0 @@
|
||||
import '../documents/attribute.dart';
|
||||
import '../quill_delta.dart';
|
||||
import 'rule.dart';
|
||||
|
||||
abstract class FormatRule extends Rule {
|
||||
const FormatRule();
|
||||
|
||||
@override
|
||||
RuleType get type => RuleType.FORMAT;
|
||||
|
||||
@override
|
||||
void validateArgs(int? len, Object? data, Attribute? attribute) {
|
||||
assert(len != null);
|
||||
assert(data == null);
|
||||
assert(attribute != null);
|
||||
}
|
||||
}
|
||||
|
||||
class ResolveLineFormatRule extends FormatRule {
|
||||
const ResolveLineFormatRule();
|
||||
|
||||
@override
|
||||
Delta? applyRule(Delta document, int index,
|
||||
{int? len, Object? data, Attribute? attribute}) {
|
||||
if (attribute!.scope != AttributeScope.BLOCK) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var delta = Delta()..retain(index);
|
||||
final itr = DeltaIterator(document)..skip(index);
|
||||
Operation op;
|
||||
for (var cur = 0; cur < len! && itr.hasNext; cur += op.length!) {
|
||||
op = itr.next(len - cur);
|
||||
if (op.data is! String || !(op.data as String).contains('\n')) {
|
||||
delta.retain(op.length!);
|
||||
continue;
|
||||
}
|
||||
final text = op.data as String;
|
||||
final tmp = Delta();
|
||||
var offset = 0;
|
||||
|
||||
// Enforce Block Format exclusivity by rule
|
||||
final removedBlocks = Attribute.exclusiveBlockKeys.contains(attribute.key)
|
||||
? op.attributes?.keys
|
||||
.where((key) =>
|
||||
Attribute.exclusiveBlockKeys.contains(key) &&
|
||||
attribute.key != key &&
|
||||
attribute.value != null)
|
||||
.map((key) => MapEntry<String, dynamic>(key, null)) ??
|
||||
[]
|
||||
: <MapEntry<String, dynamic>>[];
|
||||
|
||||
for (var lineBreak = text.indexOf('\n');
|
||||
lineBreak >= 0;
|
||||
lineBreak = text.indexOf('\n', offset)) {
|
||||
tmp
|
||||
..retain(lineBreak - offset)
|
||||
..retain(1, attribute.toJson()..addEntries(removedBlocks));
|
||||
offset = lineBreak + 1;
|
||||
}
|
||||
tmp.retain(text.length - offset);
|
||||
delta = delta.concat(tmp);
|
||||
}
|
||||
|
||||
while (itr.hasNext) {
|
||||
op = itr.next();
|
||||
final text = op.data is String ? (op.data as String?)! : '';
|
||||
final lineBreak = text.indexOf('\n');
|
||||
if (lineBreak < 0) {
|
||||
delta.retain(op.length!);
|
||||
continue;
|
||||
}
|
||||
// Enforce Block Format exclusivity by rule
|
||||
final removedBlocks = Attribute.exclusiveBlockKeys.contains(attribute.key)
|
||||
? op.attributes?.keys
|
||||
.where((key) =>
|
||||
Attribute.exclusiveBlockKeys.contains(key) &&
|
||||
attribute.key != key &&
|
||||
attribute.value != null)
|
||||
.map((key) => MapEntry<String, dynamic>(key, null)) ??
|
||||
[]
|
||||
: <MapEntry<String, dynamic>>[];
|
||||
delta
|
||||
..retain(lineBreak)
|
||||
..retain(1, attribute.toJson()..addEntries(removedBlocks));
|
||||
break;
|
||||
}
|
||||
return delta;
|
||||
}
|
||||
}
|
||||
|
||||
class FormatLinkAtCaretPositionRule extends FormatRule {
|
||||
const FormatLinkAtCaretPositionRule();
|
||||
|
||||
@override
|
||||
Delta? applyRule(Delta document, int index,
|
||||
{int? len, Object? data, Attribute? attribute}) {
|
||||
if (attribute!.key != Attribute.link.key || len! > 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final delta = Delta();
|
||||
final itr = DeltaIterator(document);
|
||||
final before = itr.skip(index), after = itr.next();
|
||||
int? beg = index, retain = 0;
|
||||
if (before != null && before.hasAttribute(attribute.key)) {
|
||||
beg -= before.length!;
|
||||
retain = before.length;
|
||||
}
|
||||
if (after.hasAttribute(attribute.key)) {
|
||||
if (retain != null) retain += after.length!;
|
||||
}
|
||||
if (retain == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
delta
|
||||
..retain(beg)
|
||||
..retain(retain!, attribute.toJson());
|
||||
return delta;
|
||||
}
|
||||
}
|
||||
|
||||
class ResolveInlineFormatRule extends FormatRule {
|
||||
const ResolveInlineFormatRule();
|
||||
|
||||
@override
|
||||
Delta? applyRule(Delta document, int index,
|
||||
{int? len, Object? data, Attribute? attribute}) {
|
||||
if (attribute!.scope != AttributeScope.INLINE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final delta = Delta()..retain(index);
|
||||
final itr = DeltaIterator(document)..skip(index);
|
||||
|
||||
Operation op;
|
||||
for (var cur = 0; cur < len! && itr.hasNext; cur += op.length!) {
|
||||
op = itr.next(len - cur);
|
||||
final text = op.data is String ? (op.data as String?)! : '';
|
||||
var lineBreak = text.indexOf('\n');
|
||||
if (lineBreak < 0) {
|
||||
delta.retain(op.length!, attribute.toJson());
|
||||
continue;
|
||||
}
|
||||
var pos = 0;
|
||||
while (lineBreak >= 0) {
|
||||
delta
|
||||
..retain(lineBreak - pos, attribute.toJson())
|
||||
..retain(1);
|
||||
pos = lineBreak + 1;
|
||||
lineBreak = text.indexOf('\n', pos);
|
||||
}
|
||||
if (pos < op.length!) {
|
||||
delta.retain(op.length! - pos, attribute.toJson());
|
||||
}
|
||||
}
|
||||
|
||||
return delta;
|
||||
}
|
||||
}
|
@ -1,385 +0,0 @@
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
import '../documents/attribute.dart';
|
||||
import '../documents/style.dart';
|
||||
import '../quill_delta.dart';
|
||||
import 'rule.dart';
|
||||
|
||||
abstract class InsertRule extends Rule {
|
||||
const InsertRule();
|
||||
|
||||
@override
|
||||
RuleType get type => RuleType.INSERT;
|
||||
|
||||
@override
|
||||
void validateArgs(int? len, Object? data, Attribute? attribute) {
|
||||
assert(data != null);
|
||||
assert(attribute == null);
|
||||
}
|
||||
}
|
||||
|
||||
class PreserveLineStyleOnSplitRule extends InsertRule {
|
||||
const PreserveLineStyleOnSplitRule();
|
||||
|
||||
@override
|
||||
Delta? applyRule(Delta document, int index,
|
||||
{int? len, Object? data, Attribute? attribute}) {
|
||||
if (data is! String || data != '\n') {
|
||||
return null;
|
||||
}
|
||||
|
||||
final itr = DeltaIterator(document);
|
||||
final before = itr.skip(index);
|
||||
if (before == null ||
|
||||
before.data is! String ||
|
||||
(before.data as String).endsWith('\n')) {
|
||||
return null;
|
||||
}
|
||||
final after = itr.next();
|
||||
if (after.data is! String || (after.data as String).startsWith('\n')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final text = after.data as String;
|
||||
|
||||
final delta = Delta()..retain(index + (len ?? 0));
|
||||
if (text.contains('\n')) {
|
||||
assert(after.isPlain);
|
||||
delta.insert('\n');
|
||||
return delta;
|
||||
}
|
||||
final nextNewLine = _getNextNewLine(itr);
|
||||
final attributes = nextNewLine.item1?.attributes;
|
||||
|
||||
return delta..insert('\n', attributes);
|
||||
}
|
||||
}
|
||||
|
||||
/// Preserves block style when user inserts text containing newlines.
|
||||
///
|
||||
/// This rule handles:
|
||||
///
|
||||
/// * inserting a new line in a block
|
||||
/// * pasting text containing multiple lines of text in a block
|
||||
///
|
||||
/// This rule may also be activated for changes triggered by auto-correct.
|
||||
class PreserveBlockStyleOnInsertRule extends InsertRule {
|
||||
const PreserveBlockStyleOnInsertRule();
|
||||
|
||||
@override
|
||||
Delta? applyRule(Delta document, int index,
|
||||
{int? len, Object? data, Attribute? attribute}) {
|
||||
if (data is! String || !data.contains('\n')) {
|
||||
// Only interested in text containing at least one newline character.
|
||||
return null;
|
||||
}
|
||||
|
||||
final itr = DeltaIterator(document)..skip(index);
|
||||
|
||||
// Look for the next newline.
|
||||
final nextNewLine = _getNextNewLine(itr);
|
||||
final lineStyle =
|
||||
Style.fromJson(nextNewLine.item1?.attributes ?? <String, dynamic>{});
|
||||
|
||||
final blockStyle = lineStyle.getBlocksExceptHeader();
|
||||
// Are we currently in a block? If not then ignore.
|
||||
if (blockStyle.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, dynamic>? resetStyle;
|
||||
// If current line had heading style applied to it we'll need to move this
|
||||
// style to the newly inserted line before it and reset style of the
|
||||
// original line.
|
||||
if (lineStyle.containsKey(Attribute.header.key)) {
|
||||
resetStyle = Attribute.header.toJson();
|
||||
}
|
||||
|
||||
// Go over each inserted line and ensure block style is applied.
|
||||
final lines = data.split('\n');
|
||||
final delta = Delta()..retain(index + (len ?? 0));
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
final line = lines[i];
|
||||
if (line.isNotEmpty) {
|
||||
delta.insert(line);
|
||||
}
|
||||
if (i == 0) {
|
||||
// The first line should inherit the lineStyle entirely.
|
||||
delta.insert('\n', lineStyle.toJson());
|
||||
} else if (i < lines.length - 1) {
|
||||
// we don't want to insert a newline after the last chunk of text, so -1
|
||||
delta.insert('\n', blockStyle);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset style of the original newline character if needed.
|
||||
if (resetStyle != null) {
|
||||
delta
|
||||
..retain(nextNewLine.item2!)
|
||||
..retain((nextNewLine.item1!.data as String).indexOf('\n'))
|
||||
..retain(1, resetStyle);
|
||||
}
|
||||
|
||||
return delta;
|
||||
}
|
||||
}
|
||||
|
||||
/// Heuristic rule to exit current block when user inserts two consecutive
|
||||
/// newlines.
|
||||
///
|
||||
/// This rule is only applied when the cursor is on the last line of a block.
|
||||
/// When the cursor is in the middle of a block we allow adding empty lines
|
||||
/// and preserving the block's style.
|
||||
class AutoExitBlockRule extends InsertRule {
|
||||
const AutoExitBlockRule();
|
||||
|
||||
bool _isEmptyLine(Operation? before, Operation? after) {
|
||||
if (before == null) {
|
||||
return true;
|
||||
}
|
||||
return before.data is String &&
|
||||
(before.data as String).endsWith('\n') &&
|
||||
after!.data is String &&
|
||||
(after.data as String).startsWith('\n');
|
||||
}
|
||||
|
||||
@override
|
||||
Delta? applyRule(Delta document, int index,
|
||||
{int? len, Object? data, Attribute? attribute}) {
|
||||
if (data is! String || data != '\n') {
|
||||
return null;
|
||||
}
|
||||
|
||||
final itr = DeltaIterator(document);
|
||||
final prev = itr.skip(index), cur = itr.next();
|
||||
final blockStyle = Style.fromJson(cur.attributes).getBlockExceptHeader();
|
||||
// We are not in a block, ignore.
|
||||
if (cur.isPlain || blockStyle == null) {
|
||||
return null;
|
||||
}
|
||||
// We are not on an empty line, ignore.
|
||||
if (!_isEmptyLine(prev, cur)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// We are on an empty line. Now we need to determine if we are on the
|
||||
// last line of a block.
|
||||
// First check if `cur` length is greater than 1, this would indicate
|
||||
// that it contains multiple newline characters which share the same style.
|
||||
// This would mean we are not on the last line yet.
|
||||
// `cur.value as String` is safe since we already called isEmptyLine and
|
||||
// know it contains a newline
|
||||
if ((cur.value as String).length > 1) {
|
||||
// We are not on the last line of this block, ignore.
|
||||
return null;
|
||||
}
|
||||
|
||||
// Keep looking for the next newline character to see if it shares the same
|
||||
// block style as `cur`.
|
||||
final nextNewLine = _getNextNewLine(itr);
|
||||
if (nextNewLine.item1 != null &&
|
||||
nextNewLine.item1!.attributes != null &&
|
||||
Style.fromJson(nextNewLine.item1!.attributes).getBlockExceptHeader() ==
|
||||
blockStyle) {
|
||||
// We are not at the end of this block, ignore.
|
||||
return null;
|
||||
}
|
||||
|
||||
// Here we now know that the line after `cur` is not in the same block
|
||||
// therefore we can exit this block.
|
||||
final attributes = cur.attributes ?? <String, dynamic>{};
|
||||
final k =
|
||||
attributes.keys.firstWhere(Attribute.blockKeysExceptHeader.contains);
|
||||
attributes[k] = null;
|
||||
// retain(1) should be '\n', set it with no attribute
|
||||
return Delta()
|
||||
..retain(index + (len ?? 0))
|
||||
..retain(1, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
class ResetLineFormatOnNewLineRule extends InsertRule {
|
||||
const ResetLineFormatOnNewLineRule();
|
||||
|
||||
@override
|
||||
Delta? applyRule(Delta document, int index,
|
||||
{int? len, Object? data, Attribute? attribute}) {
|
||||
if (data is! String || data != '\n') {
|
||||
return null;
|
||||
}
|
||||
|
||||
final itr = DeltaIterator(document)..skip(index);
|
||||
final cur = itr.next();
|
||||
if (cur.data is! String || !(cur.data as String).startsWith('\n')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, dynamic>? resetStyle;
|
||||
if (cur.attributes != null &&
|
||||
cur.attributes!.containsKey(Attribute.header.key)) {
|
||||
resetStyle = Attribute.header.toJson();
|
||||
}
|
||||
return Delta()
|
||||
..retain(index + (len ?? 0))
|
||||
..insert('\n', cur.attributes)
|
||||
..retain(1, resetStyle)
|
||||
..trim();
|
||||
}
|
||||
}
|
||||
|
||||
class InsertEmbedsRule extends InsertRule {
|
||||
const InsertEmbedsRule();
|
||||
|
||||
@override
|
||||
Delta? applyRule(Delta document, int index,
|
||||
{int? len, Object? data, Attribute? attribute}) {
|
||||
if (data is String) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final delta = Delta()..retain(index + (len ?? 0));
|
||||
final itr = DeltaIterator(document);
|
||||
final prev = itr.skip(index), cur = itr.next();
|
||||
|
||||
final textBefore = prev?.data is String ? prev!.data as String? : '';
|
||||
final textAfter = cur.data is String ? (cur.data as String?)! : '';
|
||||
|
||||
final isNewlineBefore = prev == null || textBefore!.endsWith('\n');
|
||||
final isNewlineAfter = textAfter.startsWith('\n');
|
||||
|
||||
if (isNewlineBefore && isNewlineAfter) {
|
||||
return delta..insert(data);
|
||||
}
|
||||
|
||||
Map<String, dynamic>? lineStyle;
|
||||
if (textAfter.contains('\n')) {
|
||||
lineStyle = cur.attributes;
|
||||
} else {
|
||||
while (itr.hasNext) {
|
||||
final op = itr.next();
|
||||
if ((op.data is String ? op.data as String? : '')!.contains('\n')) {
|
||||
lineStyle = op.attributes;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isNewlineBefore) {
|
||||
delta.insert('\n', lineStyle);
|
||||
}
|
||||
delta.insert(data);
|
||||
if (!isNewlineAfter) {
|
||||
delta.insert('\n');
|
||||
}
|
||||
return delta;
|
||||
}
|
||||
}
|
||||
|
||||
class AutoFormatLinksRule extends InsertRule {
|
||||
const AutoFormatLinksRule();
|
||||
|
||||
@override
|
||||
Delta? applyRule(Delta document, int index,
|
||||
{int? len, Object? data, Attribute? attribute}) {
|
||||
if (data is! String || data != ' ') {
|
||||
return null;
|
||||
}
|
||||
|
||||
final itr = DeltaIterator(document);
|
||||
final prev = itr.skip(index);
|
||||
if (prev == null || prev.data is! String) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final cand = (prev.data as String).split('\n').last.split(' ').last;
|
||||
final link = Uri.parse(cand);
|
||||
if (!['https', 'http'].contains(link.scheme)) {
|
||||
return null;
|
||||
}
|
||||
final attributes = prev.attributes ?? <String, dynamic>{};
|
||||
|
||||
if (attributes.containsKey(Attribute.link.key)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
attributes.addAll(LinkAttribute(link.toString()).toJson());
|
||||
return Delta()
|
||||
..retain(index + (len ?? 0) - cand.length)
|
||||
..retain(cand.length, attributes)
|
||||
..insert(data, prev.attributes);
|
||||
} on FormatException {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PreserveInlineStylesRule extends InsertRule {
|
||||
const PreserveInlineStylesRule();
|
||||
|
||||
@override
|
||||
Delta? applyRule(Delta document, int index,
|
||||
{int? len, Object? data, Attribute? attribute}) {
|
||||
if (data is! String || data.contains('\n')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final itr = DeltaIterator(document);
|
||||
final prev = itr.skip(index);
|
||||
if (prev == null ||
|
||||
prev.data is! String ||
|
||||
(prev.data as String).contains('\n')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final attributes = prev.attributes;
|
||||
final text = data;
|
||||
if (attributes == null || !attributes.containsKey(Attribute.link.key)) {
|
||||
return Delta()
|
||||
..retain(index + (len ?? 0))
|
||||
..insert(text, attributes);
|
||||
}
|
||||
|
||||
attributes.remove(Attribute.link.key);
|
||||
final delta = Delta()
|
||||
..retain(index + (len ?? 0))
|
||||
..insert(text, attributes.isEmpty ? null : attributes);
|
||||
final next = itr.next();
|
||||
|
||||
final nextAttributes = next.attributes ?? const <String, dynamic>{};
|
||||
if (!nextAttributes.containsKey(Attribute.link.key)) {
|
||||
return delta;
|
||||
}
|
||||
if (attributes[Attribute.link.key] == nextAttributes[Attribute.link.key]) {
|
||||
return Delta()
|
||||
..retain(index + (len ?? 0))
|
||||
..insert(text, attributes);
|
||||
}
|
||||
return delta;
|
||||
}
|
||||
}
|
||||
|
||||
class CatchAllInsertRule extends InsertRule {
|
||||
const CatchAllInsertRule();
|
||||
|
||||
@override
|
||||
Delta applyRule(Delta document, int index,
|
||||
{int? len, Object? data, Attribute? attribute}) {
|
||||
return Delta()
|
||||
..retain(index + (len ?? 0))
|
||||
..insert(data);
|
||||
}
|
||||
}
|
||||
|
||||
Tuple2<Operation?, int?> _getNextNewLine(DeltaIterator iterator) {
|
||||
Operation op;
|
||||
for (var skipped = 0; iterator.hasNext; skipped += op.length!) {
|
||||
op = iterator.next();
|
||||
final lineBreak =
|
||||
(op.data is String ? op.data as String? : '')!.indexOf('\n');
|
||||
if (lineBreak >= 0) {
|
||||
return Tuple2(op, skipped);
|
||||
}
|
||||
}
|
||||
return const Tuple2(null, null);
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
import '../documents/attribute.dart';
|
||||
import '../documents/document.dart';
|
||||
import '../quill_delta.dart';
|
||||
import 'delete.dart';
|
||||
import 'format.dart';
|
||||
import 'insert.dart';
|
||||
|
||||
enum RuleType { INSERT, DELETE, FORMAT }
|
||||
|
||||
abstract class Rule {
|
||||
const Rule();
|
||||
|
||||
Delta? apply(Delta document, int index,
|
||||
{int? len, Object? data, Attribute? attribute}) {
|
||||
validateArgs(len, data, attribute);
|
||||
return applyRule(document, index,
|
||||
len: len, data: data, attribute: attribute);
|
||||
}
|
||||
|
||||
void validateArgs(int? len, Object? data, Attribute? attribute);
|
||||
|
||||
Delta? applyRule(Delta document, int index,
|
||||
{int? len, Object? data, Attribute? attribute});
|
||||
|
||||
RuleType get type;
|
||||
}
|
||||
|
||||
class Rules {
|
||||
Rules(this._rules);
|
||||
|
||||
List<Rule> _customRules = [];
|
||||
|
||||
final List<Rule> _rules;
|
||||
static final Rules _instance = Rules([
|
||||
const FormatLinkAtCaretPositionRule(),
|
||||
const ResolveLineFormatRule(),
|
||||
const ResolveInlineFormatRule(),
|
||||
const InsertEmbedsRule(),
|
||||
const AutoExitBlockRule(),
|
||||
const PreserveBlockStyleOnInsertRule(),
|
||||
const PreserveLineStyleOnSplitRule(),
|
||||
const ResetLineFormatOnNewLineRule(),
|
||||
const AutoFormatLinksRule(),
|
||||
const PreserveInlineStylesRule(),
|
||||
const CatchAllInsertRule(),
|
||||
const EnsureEmbedLineRule(),
|
||||
const PreserveLineStyleOnMergeRule(),
|
||||
const CatchAllDeleteRule(),
|
||||
]);
|
||||
|
||||
static Rules getInstance() => _instance;
|
||||
|
||||
void setCustomRules(List<Rule> customRules) {
|
||||
_customRules = customRules;
|
||||
}
|
||||
|
||||
Delta apply(RuleType ruleType, Document document, int index,
|
||||
{int? len, Object? data, Attribute? attribute}) {
|
||||
final delta = document.toDelta();
|
||||
for (final rule in _customRules + _rules) {
|
||||
if (rule.type != ruleType) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
final result = rule.apply(delta, index,
|
||||
len: len, data: data, attribute: attribute);
|
||||
if (result != null) {
|
||||
return result..trim();
|
||||
}
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
throw 'Apply rules failed';
|
||||
}
|
||||
}
|
@ -1,125 +0,0 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Color stringToColor(String? s) {
|
||||
switch (s) {
|
||||
case 'transparent':
|
||||
return Colors.transparent;
|
||||
case 'black':
|
||||
return Colors.black;
|
||||
case 'black12':
|
||||
return Colors.black12;
|
||||
case 'black26':
|
||||
return Colors.black26;
|
||||
case 'black38':
|
||||
return Colors.black38;
|
||||
case 'black45':
|
||||
return Colors.black45;
|
||||
case 'black54':
|
||||
return Colors.black54;
|
||||
case 'black87':
|
||||
return Colors.black87;
|
||||
case 'white':
|
||||
return Colors.white;
|
||||
case 'white10':
|
||||
return Colors.white10;
|
||||
case 'white12':
|
||||
return Colors.white12;
|
||||
case 'white24':
|
||||
return Colors.white24;
|
||||
case 'white30':
|
||||
return Colors.white30;
|
||||
case 'white38':
|
||||
return Colors.white38;
|
||||
case 'white54':
|
||||
return Colors.white54;
|
||||
case 'white60':
|
||||
return Colors.white60;
|
||||
case 'white70':
|
||||
return Colors.white70;
|
||||
case 'red':
|
||||
return Colors.red;
|
||||
case 'redAccent':
|
||||
return Colors.redAccent;
|
||||
case 'amber':
|
||||
return Colors.amber;
|
||||
case 'amberAccent':
|
||||
return Colors.amberAccent;
|
||||
case 'yellow':
|
||||
return Colors.yellow;
|
||||
case 'yellowAccent':
|
||||
return Colors.yellowAccent;
|
||||
case 'teal':
|
||||
return Colors.teal;
|
||||
case 'tealAccent':
|
||||
return Colors.tealAccent;
|
||||
case 'purple':
|
||||
return Colors.purple;
|
||||
case 'purpleAccent':
|
||||
return Colors.purpleAccent;
|
||||
case 'pink':
|
||||
return Colors.pink;
|
||||
case 'pinkAccent':
|
||||
return Colors.pinkAccent;
|
||||
case 'orange':
|
||||
return Colors.orange;
|
||||
case 'orangeAccent':
|
||||
return Colors.orangeAccent;
|
||||
case 'deepOrange':
|
||||
return Colors.deepOrange;
|
||||
case 'deepOrangeAccent':
|
||||
return Colors.deepOrangeAccent;
|
||||
case 'indigo':
|
||||
return Colors.indigo;
|
||||
case 'indigoAccent':
|
||||
return Colors.indigoAccent;
|
||||
case 'lime':
|
||||
return Colors.lime;
|
||||
case 'limeAccent':
|
||||
return Colors.limeAccent;
|
||||
case 'grey':
|
||||
return Colors.grey;
|
||||
case 'blueGrey':
|
||||
return Colors.blueGrey;
|
||||
case 'green':
|
||||
return Colors.green;
|
||||
case 'greenAccent':
|
||||
return Colors.greenAccent;
|
||||
case 'lightGreen':
|
||||
return Colors.lightGreen;
|
||||
case 'lightGreenAccent':
|
||||
return Colors.lightGreenAccent;
|
||||
case 'blue':
|
||||
return Colors.blue;
|
||||
case 'blueAccent':
|
||||
return Colors.blueAccent;
|
||||
case 'lightBlue':
|
||||
return Colors.lightBlue;
|
||||
case 'lightBlueAccent':
|
||||
return Colors.lightBlueAccent;
|
||||
case 'cyan':
|
||||
return Colors.cyan;
|
||||
case 'cyanAccent':
|
||||
return Colors.cyanAccent;
|
||||
case 'brown':
|
||||
return Colors.brown;
|
||||
}
|
||||
|
||||
if (s!.startsWith('rgba')) {
|
||||
s = s.substring(5); // trim left 'rgba('
|
||||
s = s.substring(0, s.length - 1); // trim right ')'
|
||||
final arr = s.split(',').map((e) => e.trim()).toList();
|
||||
return Color.fromRGBO(int.parse(arr[0]), int.parse(arr[1]),
|
||||
int.parse(arr[2]), double.parse(arr[3]));
|
||||
}
|
||||
|
||||
if (!s.startsWith('#')) {
|
||||
throw 'Color code not supported';
|
||||
}
|
||||
|
||||
var hex = s.replaceFirst('#', '');
|
||||
hex = hex.length == 6 ? 'ff$hex' : hex;
|
||||
final val = int.parse(hex, radix: 16);
|
||||
return Color(val);
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import '../models/quill_delta.dart';
|
||||
|
||||
const Set<int> WHITE_SPACE = {
|
||||
0x9,
|
||||
0xA,
|
||||
0xB,
|
||||
0xC,
|
||||
0xD,
|
||||
0x1C,
|
||||
0x1D,
|
||||
0x1E,
|
||||
0x1F,
|
||||
0x20,
|
||||
0xA0,
|
||||
0x1680,
|
||||
0x2000,
|
||||
0x2001,
|
||||
0x2002,
|
||||
0x2003,
|
||||
0x2004,
|
||||
0x2005,
|
||||
0x2006,
|
||||
0x2007,
|
||||
0x2008,
|
||||
0x2009,
|
||||
0x200A,
|
||||
0x202F,
|
||||
0x205F,
|
||||
0x3000
|
||||
};
|
||||
|
||||
// Diff between two texts - old text and new text
|
||||
class Diff {
|
||||
Diff(this.start, this.deleted, this.inserted);
|
||||
|
||||
// Start index in old text at which changes begin.
|
||||
final int start;
|
||||
|
||||
/// The deleted text
|
||||
final String deleted;
|
||||
|
||||
// The inserted text
|
||||
final String inserted;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Diff[$start, "$deleted", "$inserted"]';
|
||||
}
|
||||
}
|
||||
|
||||
/* Get diff operation between old text and new text */
|
||||
Diff getDiff(String oldText, String newText, int cursorPosition) {
|
||||
var end = oldText.length;
|
||||
final delta = newText.length - end;
|
||||
for (final limit = math.max(0, cursorPosition - delta);
|
||||
end > limit && oldText[end - 1] == newText[end + delta - 1];
|
||||
end--) {}
|
||||
var start = 0;
|
||||
for (final startLimit = cursorPosition - math.max(0, delta);
|
||||
start < startLimit && oldText[start] == newText[start];
|
||||
start++) {}
|
||||
final deleted = (start >= end) ? '' : oldText.substring(start, end);
|
||||
final inserted = newText.substring(start, end + delta);
|
||||
return Diff(start, deleted, inserted);
|
||||
}
|
||||
|
||||
int getPositionDelta(Delta user, Delta actual) {
|
||||
if (actual.isEmpty) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
final userItr = DeltaIterator(user);
|
||||
final actualItr = DeltaIterator(actual);
|
||||
var diff = 0;
|
||||
while (userItr.hasNext || actualItr.hasNext) {
|
||||
final length = math.min(userItr.peekLength(), actualItr.peekLength());
|
||||
final userOperation = userItr.next(length);
|
||||
final actualOperation = actualItr.next(length);
|
||||
if (userOperation.length != actualOperation.length) {
|
||||
throw 'userOp ${userOperation.length} does not match actualOp '
|
||||
'${actualOperation.length}';
|
||||
}
|
||||
if (userOperation.key == actualOperation.key) {
|
||||
continue;
|
||||
} else if (userOperation.isInsert && actualOperation.isRetain) {
|
||||
diff -= userOperation.length!;
|
||||
} else if (userOperation.isDelete && actualOperation.isRetain) {
|
||||
diff += userOperation.length!;
|
||||
} else if (userOperation.isRetain && actualOperation.isInsert) {
|
||||
String? operationTxt = '';
|
||||
if (actualOperation.data is String) {
|
||||
operationTxt = actualOperation.data as String?;
|
||||
}
|
||||
if (operationTxt!.startsWith('\n')) {
|
||||
continue;
|
||||
}
|
||||
diff += actualOperation.length!;
|
||||
}
|
||||
}
|
||||
return diff;
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
enum MediaPickSetting {
|
||||
Gallery,
|
||||
Link,
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
Map<String, String> parseKeyValuePairs(String s, Set<String> targetKeys) {
|
||||
final result = <String, String>{};
|
||||
final pairs = s.split(';');
|
||||
for (final pair in pairs) {
|
||||
final _index = pair.indexOf(':');
|
||||
if (_index < 0) {
|
||||
continue;
|
||||
}
|
||||
final _key = pair.substring(0, _index).trim();
|
||||
if (targetKeys.contains(_key)) {
|
||||
result[_key] = pair.substring(_index + 1).trim();
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
@ -1,122 +0,0 @@
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
import '../models/documents/nodes/container.dart';
|
||||
|
||||
abstract class RenderContentProxyBox implements RenderBox {
|
||||
double getPreferredLineHeight();
|
||||
|
||||
Offset getOffsetForCaret(TextPosition position, Rect? caretPrototype);
|
||||
|
||||
TextPosition getPositionForOffset(Offset offset);
|
||||
|
||||
double? getFullHeightForCaret(TextPosition position);
|
||||
|
||||
TextRange getWordBoundary(TextPosition position);
|
||||
|
||||
List<TextBox> getBoxesForSelection(TextSelection textSelection);
|
||||
}
|
||||
|
||||
/// Base class for render boxes of editable content.
|
||||
///
|
||||
/// Implementations of this class usually work as a wrapper around
|
||||
/// regular (non-editable) render boxes which implement
|
||||
/// [RenderContentProxyBox].
|
||||
abstract class RenderEditableBox extends RenderBox {
|
||||
/// The document node represented by this render box.
|
||||
Container getContainer();
|
||||
|
||||
/// Returns preferred line height at specified `position` in text.
|
||||
///
|
||||
/// The `position` parameter must be relative to the [node]'s content.
|
||||
double preferredLineHeight(TextPosition position);
|
||||
|
||||
/// Returns the offset at which to paint the caret.
|
||||
///
|
||||
/// The `position` parameter must be relative to the [node]'s content.
|
||||
///
|
||||
/// Valid only after [layout].
|
||||
Offset getOffsetForCaret(TextPosition position);
|
||||
|
||||
/// Returns the position within the text for the given pixel offset.
|
||||
///
|
||||
/// The `offset` parameter must be local to this box coordinate system.
|
||||
///
|
||||
/// Valid only after [layout].
|
||||
TextPosition getPositionForOffset(Offset offset);
|
||||
|
||||
/// Returns the position relative to the [node] content
|
||||
///
|
||||
/// The `position` must be within the [node] content
|
||||
TextPosition globalToLocalPosition(TextPosition position);
|
||||
|
||||
/// Returns the position within the text which is on the line above the given
|
||||
/// `position`.
|
||||
///
|
||||
/// The `position` parameter must be relative to the [node] content.
|
||||
///
|
||||
/// Primarily used with multi-line or soft-wrapping text.
|
||||
///
|
||||
/// Can return `null` which indicates that the `position` is at the topmost
|
||||
/// line in the text already.
|
||||
TextPosition? getPositionAbove(TextPosition position);
|
||||
|
||||
/// Returns the position within the text which is on the line below the given
|
||||
/// `position`.
|
||||
///
|
||||
/// The `position` parameter must be relative to the [node] content.
|
||||
///
|
||||
/// Primarily used with multi-line or soft-wrapping text.
|
||||
///
|
||||
/// Can return `null` which indicates that the `position` is at the bottommost
|
||||
/// line in the text already.
|
||||
TextPosition? getPositionBelow(TextPosition position);
|
||||
|
||||
/// Returns the text range of the word at the given offset. Characters not
|
||||
/// part of a word, such as spaces, symbols, and punctuation, have word breaks
|
||||
/// on both sides. In such cases, this method will return a text range that
|
||||
/// contains the given text position.
|
||||
///
|
||||
/// Word boundaries are defined more precisely in Unicode Standard Annex #29
|
||||
/// <http://www.unicode.org/reports/tr29/#Word_Boundaries>.
|
||||
///
|
||||
/// The `position` parameter must be relative to the [node]'s content.
|
||||
///
|
||||
/// Valid only after [layout].
|
||||
TextRange getWordBoundary(TextPosition position);
|
||||
|
||||
/// Returns the text range of the line at the given offset.
|
||||
///
|
||||
/// The newline, if any, is included in the range.
|
||||
///
|
||||
/// The `position` parameter must be relative to the [node]'s content.
|
||||
///
|
||||
/// Valid only after [layout].
|
||||
TextRange getLineBoundary(TextPosition position);
|
||||
|
||||
/// Returns a list of rects that bound the given selection.
|
||||
///
|
||||
/// A given selection might have more than one rect if this text painter
|
||||
/// contains bidirectional text because logically contiguous text might not be
|
||||
/// visually contiguous.
|
||||
///
|
||||
/// Valid only after [layout].
|
||||
// List<TextBox> getBoxesForSelection(TextSelection selection);
|
||||
|
||||
/// Returns a point for the base selection handle used on touch-oriented
|
||||
/// devices.
|
||||
///
|
||||
/// The `selection` parameter is expected to be in local offsets to this
|
||||
/// render object's [node].
|
||||
TextSelectionPoint getBaseEndpointForSelection(TextSelection textSelection);
|
||||
|
||||
/// Returns a point for the extent selection handle used on touch-oriented
|
||||
/// devices.
|
||||
///
|
||||
/// The `selection` parameter is expected to be in local offsets to this
|
||||
/// render object's [node].
|
||||
TextSelectionPoint getExtentEndpointForSelection(TextSelection textSelection);
|
||||
|
||||
/// Returns the [Rect] in local coordinates for the caret at the given text
|
||||
/// position.
|
||||
Rect getLocalRectForCaret(TextPosition position);
|
||||
}
|
@ -1,255 +0,0 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
import '../models/documents/attribute.dart';
|
||||
import '../models/documents/document.dart';
|
||||
import '../models/documents/nodes/embed.dart';
|
||||
import '../models/documents/style.dart';
|
||||
import '../models/quill_delta.dart';
|
||||
import '../utils/diff_delta.dart';
|
||||
|
||||
class QuillController extends ChangeNotifier {
|
||||
QuillController({
|
||||
required this.document,
|
||||
required TextSelection selection,
|
||||
bool keepStyleOnNewLine = false,
|
||||
}) : _selection = selection,
|
||||
_keepStyleOnNewLine = keepStyleOnNewLine;
|
||||
|
||||
factory QuillController.basic() {
|
||||
return QuillController(
|
||||
document: Document(),
|
||||
selection: const TextSelection.collapsed(offset: 0),
|
||||
);
|
||||
}
|
||||
|
||||
/// Document managed by this controller.
|
||||
final Document document;
|
||||
|
||||
/// Tells whether to keep or reset the [toggledStyle]
|
||||
/// when user adds a new line.
|
||||
final bool _keepStyleOnNewLine;
|
||||
|
||||
/// Currently selected text within the [document].
|
||||
TextSelection get selection => _selection;
|
||||
TextSelection _selection;
|
||||
|
||||
/// Store any styles attribute that got toggled by the tap of a button
|
||||
/// and that has not been applied yet.
|
||||
/// It gets reset after each format action within the [document].
|
||||
Style toggledStyle = Style();
|
||||
|
||||
bool ignoreFocusOnTextChange = false;
|
||||
|
||||
/// True when this [QuillController] instance has been disposed.
|
||||
///
|
||||
/// A safety mechanism to ensure that listeners don't crash when adding,
|
||||
/// removing or listeners to this instance.
|
||||
bool _isDisposed = false;
|
||||
|
||||
// item1: Document state before [change].
|
||||
//
|
||||
// item2: Change delta applied to the document.
|
||||
//
|
||||
// item3: The source of this change.
|
||||
Stream<Tuple3<Delta, Delta, ChangeSource>> get changes => document.changes;
|
||||
|
||||
TextEditingValue get plainTextEditingValue => TextEditingValue(
|
||||
text: document.toPlainText(),
|
||||
selection: selection,
|
||||
);
|
||||
|
||||
/// Only attributes applied to all characters within this range are
|
||||
/// included in the result.
|
||||
Style getSelectionStyle() {
|
||||
return document
|
||||
.collectStyle(selection.start, selection.end - selection.start)
|
||||
.mergeAll(toggledStyle);
|
||||
}
|
||||
|
||||
/// Returns all styles for any character within the specified text range.
|
||||
List<Style> getAllSelectionStyles() {
|
||||
final styles = document.collectAllStyles(
|
||||
selection.start, selection.end - selection.start)
|
||||
..add(toggledStyle);
|
||||
return styles;
|
||||
}
|
||||
|
||||
void undo() {
|
||||
final tup = document.undo();
|
||||
if (tup.item1) {
|
||||
_handleHistoryChange(tup.item2);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleHistoryChange(int? len) {
|
||||
if (len! != 0) {
|
||||
// if (this.selection.extentOffset >= document.length) {
|
||||
// // cursor exceeds the length of document, position it in the end
|
||||
// updateSelection(
|
||||
// TextSelection.collapsed(offset: document.length), ChangeSource.LOCAL);
|
||||
updateSelection(
|
||||
TextSelection.collapsed(offset: selection.baseOffset + len),
|
||||
ChangeSource.LOCAL);
|
||||
} else {
|
||||
// no need to move cursor
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void redo() {
|
||||
final tup = document.redo();
|
||||
if (tup.item1) {
|
||||
_handleHistoryChange(tup.item2);
|
||||
}
|
||||
}
|
||||
|
||||
bool get hasUndo => document.hasUndo;
|
||||
|
||||
bool get hasRedo => document.hasRedo;
|
||||
|
||||
void replaceText(
|
||||
int index, int len, Object? data, TextSelection? textSelection,
|
||||
{bool ignoreFocus = false}) {
|
||||
assert(data is String || data is Embeddable);
|
||||
|
||||
Delta? delta;
|
||||
if (len > 0 || data is! String || data.isNotEmpty) {
|
||||
delta = document.replace(index, len, data);
|
||||
var shouldRetainDelta = toggledStyle.isNotEmpty &&
|
||||
delta.isNotEmpty &&
|
||||
delta.length <= 2 &&
|
||||
delta.last.isInsert;
|
||||
if (shouldRetainDelta &&
|
||||
toggledStyle.isNotEmpty &&
|
||||
delta.length == 2 &&
|
||||
delta.last.data == '\n') {
|
||||
// if all attributes are inline, shouldRetainDelta should be false
|
||||
final anyAttributeNotInline =
|
||||
toggledStyle.values.any((attr) => !attr.isInline);
|
||||
if (!anyAttributeNotInline) {
|
||||
shouldRetainDelta = false;
|
||||
}
|
||||
}
|
||||
if (shouldRetainDelta) {
|
||||
final retainDelta = Delta()
|
||||
..retain(index)
|
||||
..retain(data is String ? data.length : 1, toggledStyle.toJson());
|
||||
document.compose(retainDelta, ChangeSource.LOCAL);
|
||||
}
|
||||
}
|
||||
|
||||
if (_keepStyleOnNewLine) {
|
||||
final style = getSelectionStyle();
|
||||
final notInlineStyle = style.attributes.values.where((s) => !s.isInline);
|
||||
toggledStyle = style.removeAll(notInlineStyle.toSet());
|
||||
} else {
|
||||
toggledStyle = Style();
|
||||
}
|
||||
|
||||
if (textSelection != null) {
|
||||
if (delta == null || delta.isEmpty) {
|
||||
_updateSelection(textSelection, ChangeSource.LOCAL);
|
||||
} else {
|
||||
final user = Delta()
|
||||
..retain(index)
|
||||
..insert(data)
|
||||
..delete(len);
|
||||
final positionDelta = getPositionDelta(user, delta);
|
||||
_updateSelection(
|
||||
textSelection.copyWith(
|
||||
baseOffset: textSelection.baseOffset + positionDelta,
|
||||
extentOffset: textSelection.extentOffset + positionDelta,
|
||||
),
|
||||
ChangeSource.LOCAL,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (ignoreFocus) {
|
||||
ignoreFocusOnTextChange = true;
|
||||
}
|
||||
notifyListeners();
|
||||
ignoreFocusOnTextChange = false;
|
||||
}
|
||||
|
||||
void formatText(int index, int len, Attribute? attribute) {
|
||||
if (len == 0 &&
|
||||
attribute!.isInline &&
|
||||
attribute.key != Attribute.link.key) {
|
||||
toggledStyle = toggledStyle.put(attribute);
|
||||
}
|
||||
|
||||
final change = document.format(index, len, attribute);
|
||||
final adjustedSelection = selection.copyWith(
|
||||
baseOffset: change.transformPosition(selection.baseOffset),
|
||||
extentOffset: change.transformPosition(selection.extentOffset));
|
||||
if (selection != adjustedSelection) {
|
||||
_updateSelection(adjustedSelection, ChangeSource.LOCAL);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void formatSelection(Attribute? attribute) {
|
||||
formatText(selection.start, selection.end - selection.start, attribute);
|
||||
}
|
||||
|
||||
void updateSelection(TextSelection textSelection, ChangeSource source) {
|
||||
_updateSelection(textSelection, source);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void compose(Delta delta, TextSelection textSelection, ChangeSource source) {
|
||||
if (delta.isNotEmpty) {
|
||||
document.compose(delta, source);
|
||||
}
|
||||
|
||||
textSelection = selection.copyWith(
|
||||
baseOffset: delta.transformPosition(selection.baseOffset, force: false),
|
||||
extentOffset:
|
||||
delta.transformPosition(selection.extentOffset, force: false));
|
||||
if (selection != textSelection) {
|
||||
_updateSelection(textSelection, source);
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void addListener(VoidCallback listener) {
|
||||
// By using `_isDisposed`, make sure that `addListener` won't be called on a
|
||||
// disposed `ChangeListener`
|
||||
if (!_isDisposed) {
|
||||
super.addListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void removeListener(VoidCallback listener) {
|
||||
// By using `_isDisposed`, make sure that `removeListener` won't be called
|
||||
// on a disposed `ChangeListener`
|
||||
if (!_isDisposed) {
|
||||
super.removeListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (!_isDisposed) {
|
||||
document.close();
|
||||
}
|
||||
|
||||
_isDisposed = true;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _updateSelection(TextSelection textSelection, ChangeSource source) {
|
||||
_selection = textSelection;
|
||||
final end = document.length - 1;
|
||||
_selection = selection.copyWith(
|
||||
baseOffset: math.min(selection.baseOffset, end),
|
||||
extentOffset: math.min(selection.extentOffset, end));
|
||||
}
|
||||
}
|
@ -1,341 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'box.dart';
|
||||
|
||||
/// Style properties of editing cursor.
|
||||
class CursorStyle {
|
||||
const CursorStyle({
|
||||
required this.color,
|
||||
required this.backgroundColor,
|
||||
this.width = 1.0,
|
||||
this.height,
|
||||
this.radius,
|
||||
this.offset,
|
||||
this.opacityAnimates = false,
|
||||
this.paintAboveText = false,
|
||||
});
|
||||
|
||||
/// The color to use when painting the cursor.
|
||||
final Color color;
|
||||
|
||||
/// The color to use when painting the background cursor aligned with the text
|
||||
/// while rendering the floating cursor.
|
||||
final Color backgroundColor;
|
||||
|
||||
/// How thick the cursor will be.
|
||||
///
|
||||
/// The cursor will draw under the text. The cursor width will extend
|
||||
/// to the right of the boundary between characters for left-to-right text
|
||||
/// and to the left for right-to-left text. This corresponds to extending
|
||||
/// downstream relative to the selected position. Negative values may be used
|
||||
/// to reverse this behavior.
|
||||
final double width;
|
||||
|
||||
/// How tall the cursor will be.
|
||||
///
|
||||
/// By default, the cursor height is set to the preferred line height of the
|
||||
/// text.
|
||||
final double? height;
|
||||
|
||||
/// How rounded the corners of the cursor should be.
|
||||
///
|
||||
/// By default, the cursor has no radius.
|
||||
final Radius? radius;
|
||||
|
||||
/// The offset that is used, in pixels, when painting the cursor on screen.
|
||||
///
|
||||
/// By default, the cursor position should be set to an offset of
|
||||
/// (-[cursorWidth] * 0.5, 0.0) on iOS platforms and (0, 0) on Android
|
||||
/// platforms. The origin from where the offset is applied to is the arbitrary
|
||||
/// location where the cursor ends up being rendered from by default.
|
||||
final Offset? offset;
|
||||
|
||||
/// Whether the cursor will animate from fully transparent to fully opaque
|
||||
/// during each cursor blink.
|
||||
///
|
||||
/// By default, the cursor opacity will animate on iOS platforms and will not
|
||||
/// animate on Android platforms.
|
||||
final bool opacityAnimates;
|
||||
|
||||
/// If the cursor should be painted on top of the text or underneath it.
|
||||
///
|
||||
/// By default, the cursor should be painted on top for iOS platforms and
|
||||
/// underneath for Android platforms.
|
||||
final bool paintAboveText;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is CursorStyle &&
|
||||
runtimeType == other.runtimeType &&
|
||||
color == other.color &&
|
||||
backgroundColor == other.backgroundColor &&
|
||||
width == other.width &&
|
||||
height == other.height &&
|
||||
radius == other.radius &&
|
||||
offset == other.offset &&
|
||||
opacityAnimates == other.opacityAnimates &&
|
||||
paintAboveText == other.paintAboveText;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
color.hashCode ^
|
||||
backgroundColor.hashCode ^
|
||||
width.hashCode ^
|
||||
height.hashCode ^
|
||||
radius.hashCode ^
|
||||
offset.hashCode ^
|
||||
opacityAnimates.hashCode ^
|
||||
paintAboveText.hashCode;
|
||||
}
|
||||
|
||||
/// Controls the cursor of an editable widget.
|
||||
///
|
||||
/// This class is a [ChangeNotifier] and allows to listen for updates on the
|
||||
/// cursor [style].
|
||||
class CursorCont extends ChangeNotifier {
|
||||
CursorCont({
|
||||
required this.show,
|
||||
required CursorStyle style,
|
||||
required TickerProvider tickerProvider,
|
||||
}) : _style = style,
|
||||
blink = ValueNotifier(false),
|
||||
color = ValueNotifier(style.color) {
|
||||
_blinkOpacityController =
|
||||
AnimationController(vsync: tickerProvider, duration: _fadeDuration);
|
||||
_blinkOpacityController.addListener(_onColorTick);
|
||||
}
|
||||
|
||||
// The time it takes for the cursor to fade from fully opaque to fully
|
||||
// transparent and vice versa. A full cursor blink, from transparent to opaque
|
||||
// to transparent, is twice this duration.
|
||||
static const Duration _blinkHalfPeriod = Duration(milliseconds: 500);
|
||||
|
||||
// The time the cursor is static in opacity before animating to become
|
||||
// transparent.
|
||||
static const Duration _blinkWaitForStart = Duration(milliseconds: 150);
|
||||
|
||||
// This value is an eyeball estimation of the time it takes for the iOS cursor
|
||||
// to ease in and out.
|
||||
static const Duration _fadeDuration = Duration(milliseconds: 250);
|
||||
|
||||
final ValueNotifier<bool> show;
|
||||
final ValueNotifier<Color> color;
|
||||
final ValueNotifier<bool> blink;
|
||||
|
||||
late final AnimationController _blinkOpacityController;
|
||||
|
||||
Timer? _cursorTimer;
|
||||
bool _targetCursorVisibility = false;
|
||||
|
||||
CursorStyle _style;
|
||||
CursorStyle get style => _style;
|
||||
set style(CursorStyle value) {
|
||||
if (_style == value) return;
|
||||
_style = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// True when this [CursorCont] instance has been disposed.
|
||||
///
|
||||
/// A safety mechanism to prevent the value of a disposed controller from
|
||||
/// getting set.
|
||||
bool _isDisposed = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_blinkOpacityController.removeListener(_onColorTick);
|
||||
stopCursorTimer();
|
||||
|
||||
_isDisposed = true;
|
||||
_blinkOpacityController.dispose();
|
||||
show.dispose();
|
||||
blink.dispose();
|
||||
color.dispose();
|
||||
assert(_cursorTimer == null);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _cursorTick(Timer timer) {
|
||||
_targetCursorVisibility = !_targetCursorVisibility;
|
||||
final targetOpacity = _targetCursorVisibility ? 1.0 : 0.0;
|
||||
if (style.opacityAnimates) {
|
||||
// If we want to show the cursor, we will animate the opacity to the value
|
||||
// of 1.0, and likewise if we want to make it disappear, to 0.0. An easing
|
||||
// curve is used for the animation to mimic the aesthetics of the native
|
||||
// iOS cursor.
|
||||
//
|
||||
// These values and curves have been obtained through eyeballing, so are
|
||||
// likely not exactly the same as the values for native iOS.
|
||||
_blinkOpacityController.animateTo(targetOpacity, curve: Curves.easeOut);
|
||||
} else {
|
||||
_blinkOpacityController.value = targetOpacity;
|
||||
}
|
||||
}
|
||||
|
||||
void _waitForStart(Timer timer) {
|
||||
_cursorTimer?.cancel();
|
||||
_cursorTimer = Timer.periodic(_blinkHalfPeriod, _cursorTick);
|
||||
}
|
||||
|
||||
void startCursorTimer() {
|
||||
if (_isDisposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
_targetCursorVisibility = true;
|
||||
_blinkOpacityController.value = 1.0;
|
||||
|
||||
if (style.opacityAnimates) {
|
||||
_cursorTimer = Timer.periodic(_blinkWaitForStart, _waitForStart);
|
||||
} else {
|
||||
_cursorTimer = Timer.periodic(_blinkHalfPeriod, _cursorTick);
|
||||
}
|
||||
}
|
||||
|
||||
void stopCursorTimer({bool resetCharTicks = true}) {
|
||||
_cursorTimer?.cancel();
|
||||
_cursorTimer = null;
|
||||
_targetCursorVisibility = false;
|
||||
_blinkOpacityController.value = 0.0;
|
||||
|
||||
if (style.opacityAnimates) {
|
||||
_blinkOpacityController
|
||||
..stop()
|
||||
..value = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
void startOrStopCursorTimerIfNeeded(bool hasFocus, TextSelection selection) {
|
||||
if (show.value &&
|
||||
_cursorTimer == null &&
|
||||
hasFocus &&
|
||||
selection.isCollapsed) {
|
||||
startCursorTimer();
|
||||
} else if (_cursorTimer != null && (!hasFocus || !selection.isCollapsed)) {
|
||||
stopCursorTimer();
|
||||
}
|
||||
}
|
||||
|
||||
void _onColorTick() {
|
||||
color.value = _style.color.withOpacity(_blinkOpacityController.value);
|
||||
blink.value = show.value && _blinkOpacityController.value > 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Paints the editing cursor.
|
||||
class CursorPainter {
|
||||
CursorPainter(
|
||||
this.editable,
|
||||
this.style,
|
||||
this.prototype,
|
||||
this.color,
|
||||
this.devicePixelRatio,
|
||||
);
|
||||
|
||||
final RenderContentProxyBox? editable;
|
||||
final CursorStyle style;
|
||||
final Rect prototype;
|
||||
final Color color;
|
||||
final double devicePixelRatio;
|
||||
|
||||
/// Paints cursor on [canvas] at specified [position].
|
||||
/// [offset] is global top left (x, y) of text line
|
||||
/// [position] is relative (x) in text line
|
||||
void paint(
|
||||
Canvas canvas, Offset offset, TextPosition position, bool lineHasEmbed) {
|
||||
// relative (x, y) to global offset
|
||||
var relativeCaretOffset = editable!.getOffsetForCaret(position, prototype);
|
||||
if (lineHasEmbed && relativeCaretOffset == Offset.zero) {
|
||||
relativeCaretOffset = editable!.getOffsetForCaret(
|
||||
TextPosition(
|
||||
offset: position.offset - 1, affinity: position.affinity),
|
||||
prototype);
|
||||
// Hardcoded 6 as estimate of the width of a character
|
||||
relativeCaretOffset =
|
||||
Offset(relativeCaretOffset.dx + 6, relativeCaretOffset.dy);
|
||||
}
|
||||
|
||||
final caretOffset = relativeCaretOffset + offset;
|
||||
var caretRect = prototype.shift(caretOffset);
|
||||
if (style.offset != null) {
|
||||
caretRect = caretRect.shift(style.offset!);
|
||||
}
|
||||
|
||||
if (caretRect.left < 0.0) {
|
||||
// For iOS the cursor may get clipped by the scroll view when
|
||||
// it's located at a beginning of a line. We ensure that this
|
||||
// does not happen here. This may result in the cursor being painted
|
||||
// closer to the character on the right, but it's arguably better
|
||||
// then painting clipped cursor (or even cursor completely hidden).
|
||||
caretRect = caretRect.shift(Offset(-caretRect.left, 0));
|
||||
}
|
||||
|
||||
final caretHeight = editable!.getFullHeightForCaret(position);
|
||||
if (caretHeight != null) {
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
// Override the height to take the full height of the glyph at the
|
||||
// TextPosition when not on iOS. iOS has special handling that
|
||||
// creates a taller caret.
|
||||
caretRect = Rect.fromLTWH(
|
||||
caretRect.left,
|
||||
caretRect.top - 2.0,
|
||||
caretRect.width,
|
||||
caretHeight,
|
||||
);
|
||||
break;
|
||||
case TargetPlatform.iOS:
|
||||
case TargetPlatform.macOS:
|
||||
// Center the caret vertically along the text.
|
||||
caretRect = Rect.fromLTWH(
|
||||
caretRect.left,
|
||||
caretRect.top + (caretHeight - caretRect.height) / 2,
|
||||
caretRect.width,
|
||||
caretRect.height,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
final pixelPerfectOffset = _getPixelPerfectCursorOffset(caretRect);
|
||||
if (!pixelPerfectOffset.isFinite) {
|
||||
return;
|
||||
}
|
||||
caretRect = caretRect.shift(pixelPerfectOffset);
|
||||
|
||||
final paint = Paint()..color = color;
|
||||
if (style.radius == null) {
|
||||
canvas.drawRect(caretRect, paint);
|
||||
} else {
|
||||
final caretRRect = RRect.fromRectAndRadius(caretRect, style.radius!);
|
||||
canvas.drawRRect(caretRRect, paint);
|
||||
}
|
||||
}
|
||||
|
||||
Offset _getPixelPerfectCursorOffset(
|
||||
Rect caretRect,
|
||||
) {
|
||||
final caretPosition = editable!.localToGlobal(caretRect.topLeft);
|
||||
final pixelMultiple = 1.0 / devicePixelRatio;
|
||||
|
||||
final pixelPerfectOffsetX = caretPosition.dx.isFinite
|
||||
? (caretPosition.dx / pixelMultiple).round() * pixelMultiple -
|
||||
caretPosition.dx
|
||||
: caretPosition.dx;
|
||||
final pixelPerfectOffsetY = caretPosition.dy.isFinite
|
||||
? (caretPosition.dy / pixelMultiple).round() * pixelMultiple -
|
||||
caretPosition.dy
|
||||
: caretPosition.dy;
|
||||
|
||||
return Offset(pixelPerfectOffsetX, pixelPerfectOffsetY);
|
||||
}
|
||||
}
|
@ -1,235 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class QuillStyles extends InheritedWidget {
|
||||
const QuillStyles({
|
||||
required this.data,
|
||||
required Widget child,
|
||||
Key? key,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
final DefaultStyles data;
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(QuillStyles oldWidget) {
|
||||
return data != oldWidget.data;
|
||||
}
|
||||
|
||||
static DefaultStyles? getStyles(BuildContext context, bool nullOk) {
|
||||
final widget = context.dependOnInheritedWidgetOfExactType<QuillStyles>();
|
||||
if (widget == null && nullOk) {
|
||||
return null;
|
||||
}
|
||||
assert(widget != null);
|
||||
return widget!.data;
|
||||
}
|
||||
}
|
||||
|
||||
class DefaultTextBlockStyle {
|
||||
DefaultTextBlockStyle(
|
||||
this.style,
|
||||
this.verticalSpacing,
|
||||
this.lineSpacing,
|
||||
this.decoration,
|
||||
);
|
||||
|
||||
final TextStyle style;
|
||||
|
||||
final Tuple2<double, double> verticalSpacing;
|
||||
|
||||
final Tuple2<double, double> lineSpacing;
|
||||
|
||||
final BoxDecoration? decoration;
|
||||
}
|
||||
|
||||
class DefaultStyles {
|
||||
DefaultStyles({
|
||||
this.h1,
|
||||
this.h2,
|
||||
this.h3,
|
||||
this.paragraph,
|
||||
this.bold,
|
||||
this.italic,
|
||||
this.small,
|
||||
this.underline,
|
||||
this.strikeThrough,
|
||||
this.inlineCode,
|
||||
this.link,
|
||||
this.color,
|
||||
this.placeHolder,
|
||||
this.lists,
|
||||
this.quote,
|
||||
this.code,
|
||||
this.indent,
|
||||
this.align,
|
||||
this.leading,
|
||||
this.sizeSmall,
|
||||
this.sizeLarge,
|
||||
this.sizeHuge,
|
||||
});
|
||||
|
||||
final DefaultTextBlockStyle? h1;
|
||||
final DefaultTextBlockStyle? h2;
|
||||
final DefaultTextBlockStyle? h3;
|
||||
final DefaultTextBlockStyle? paragraph;
|
||||
final TextStyle? bold;
|
||||
final TextStyle? italic;
|
||||
final TextStyle? small;
|
||||
final TextStyle? underline;
|
||||
final TextStyle? strikeThrough;
|
||||
final TextStyle? inlineCode;
|
||||
final TextStyle? sizeSmall; // 'small'
|
||||
final TextStyle? sizeLarge; // 'large'
|
||||
final TextStyle? sizeHuge; // 'huge'
|
||||
final TextStyle? link;
|
||||
final Color? color;
|
||||
final DefaultTextBlockStyle? placeHolder;
|
||||
final DefaultTextBlockStyle? lists;
|
||||
final DefaultTextBlockStyle? quote;
|
||||
final DefaultTextBlockStyle? code;
|
||||
final DefaultTextBlockStyle? indent;
|
||||
final DefaultTextBlockStyle? align;
|
||||
final DefaultTextBlockStyle? leading;
|
||||
|
||||
static DefaultStyles getInstance(BuildContext context) {
|
||||
final themeData = Theme.of(context);
|
||||
final defaultTextStyle = DefaultTextStyle.of(context);
|
||||
final baseStyle = defaultTextStyle.style.copyWith(
|
||||
fontSize: 16,
|
||||
height: 1.3,
|
||||
);
|
||||
const baseSpacing = Tuple2<double, double>(6, 0);
|
||||
String fontFamily;
|
||||
switch (themeData.platform) {
|
||||
case TargetPlatform.iOS:
|
||||
case TargetPlatform.macOS:
|
||||
fontFamily = 'Menlo';
|
||||
break;
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.windows:
|
||||
case TargetPlatform.linux:
|
||||
fontFamily = 'Roboto Mono';
|
||||
break;
|
||||
default:
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
return DefaultStyles(
|
||||
h1: DefaultTextBlockStyle(
|
||||
defaultTextStyle.style.copyWith(
|
||||
fontSize: 34,
|
||||
color: defaultTextStyle.style.color!.withOpacity(0.70),
|
||||
height: 1.15,
|
||||
fontWeight: FontWeight.w300,
|
||||
),
|
||||
const Tuple2(16, 0),
|
||||
const Tuple2(0, 0),
|
||||
null),
|
||||
h2: DefaultTextBlockStyle(
|
||||
defaultTextStyle.style.copyWith(
|
||||
fontSize: 24,
|
||||
color: defaultTextStyle.style.color!.withOpacity(0.70),
|
||||
height: 1.15,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
const Tuple2(8, 0),
|
||||
const Tuple2(0, 0),
|
||||
null),
|
||||
h3: DefaultTextBlockStyle(
|
||||
defaultTextStyle.style.copyWith(
|
||||
fontSize: 20,
|
||||
color: defaultTextStyle.style.color!.withOpacity(0.70),
|
||||
height: 1.25,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
const Tuple2(8, 0),
|
||||
const Tuple2(0, 0),
|
||||
null),
|
||||
paragraph: DefaultTextBlockStyle(
|
||||
baseStyle, const Tuple2(0, 0), const Tuple2(0, 0), null),
|
||||
bold: const TextStyle(fontWeight: FontWeight.bold),
|
||||
italic: const TextStyle(fontStyle: FontStyle.italic),
|
||||
small: const TextStyle(fontSize: 12, color: Colors.black45),
|
||||
underline: const TextStyle(decoration: TextDecoration.underline),
|
||||
strikeThrough: const TextStyle(decoration: TextDecoration.lineThrough),
|
||||
inlineCode: TextStyle(
|
||||
color: Colors.blue.shade900.withOpacity(0.9),
|
||||
fontFamily: fontFamily,
|
||||
fontSize: 13,
|
||||
),
|
||||
link: TextStyle(
|
||||
color: themeData.colorScheme.secondary,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
placeHolder: DefaultTextBlockStyle(
|
||||
defaultTextStyle.style.copyWith(
|
||||
fontSize: 20,
|
||||
height: 1.5,
|
||||
color: Colors.grey.withOpacity(0.6),
|
||||
),
|
||||
const Tuple2(0, 0),
|
||||
const Tuple2(0, 0),
|
||||
null),
|
||||
lists: DefaultTextBlockStyle(
|
||||
baseStyle, baseSpacing, const Tuple2(0, 6), null),
|
||||
quote: DefaultTextBlockStyle(
|
||||
TextStyle(color: baseStyle.color!.withOpacity(0.6)),
|
||||
baseSpacing,
|
||||
const Tuple2(6, 2),
|
||||
BoxDecoration(
|
||||
border: Border(
|
||||
left: BorderSide(width: 4, color: Colors.grey.shade300),
|
||||
),
|
||||
)),
|
||||
code: DefaultTextBlockStyle(
|
||||
TextStyle(
|
||||
color: Colors.blue.shade900.withOpacity(0.9),
|
||||
fontFamily: fontFamily,
|
||||
fontSize: 13,
|
||||
height: 1.15,
|
||||
),
|
||||
baseSpacing,
|
||||
const Tuple2(0, 0),
|
||||
BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
)),
|
||||
indent: DefaultTextBlockStyle(
|
||||
baseStyle, baseSpacing, const Tuple2(0, 6), null),
|
||||
align: DefaultTextBlockStyle(
|
||||
baseStyle, const Tuple2(0, 0), const Tuple2(0, 0), null),
|
||||
leading: DefaultTextBlockStyle(
|
||||
baseStyle, const Tuple2(0, 0), const Tuple2(0, 0), null),
|
||||
sizeSmall: const TextStyle(fontSize: 10),
|
||||
sizeLarge: const TextStyle(fontSize: 18),
|
||||
sizeHuge: const TextStyle(fontSize: 22));
|
||||
}
|
||||
|
||||
DefaultStyles merge(DefaultStyles other) {
|
||||
return DefaultStyles(
|
||||
h1: other.h1 ?? h1,
|
||||
h2: other.h2 ?? h2,
|
||||
h3: other.h3 ?? h3,
|
||||
paragraph: other.paragraph ?? paragraph,
|
||||
bold: other.bold ?? bold,
|
||||
italic: other.italic ?? italic,
|
||||
small: other.small ?? small,
|
||||
underline: other.underline ?? underline,
|
||||
strikeThrough: other.strikeThrough ?? strikeThrough,
|
||||
inlineCode: other.inlineCode ?? inlineCode,
|
||||
link: other.link ?? link,
|
||||
color: other.color ?? color,
|
||||
placeHolder: other.placeHolder ?? placeHolder,
|
||||
lists: other.lists ?? lists,
|
||||
quote: other.quote ?? quote,
|
||||
code: other.code ?? code,
|
||||
indent: other.indent ?? indent,
|
||||
align: other.align ?? align,
|
||||
leading: other.leading ?? leading,
|
||||
sizeSmall: other.sizeSmall ?? sizeSmall,
|
||||
sizeLarge: other.sizeLarge ?? sizeLarge,
|
||||
sizeHuge: other.sizeHuge ?? sizeHuge);
|
||||
}
|
||||
}
|
@ -1,152 +0,0 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import '../../flutter_quill.dart';
|
||||
|
||||
import '../models/documents/nodes/leaf.dart';
|
||||
import 'editor.dart';
|
||||
import 'text_selection.dart';
|
||||
|
||||
typedef EmbedBuilder = Widget Function(
|
||||
BuildContext context, Embed node, bool readOnly);
|
||||
|
||||
typedef CustomStyleBuilder = TextStyle Function(Attribute attribute);
|
||||
|
||||
abstract class EditorTextSelectionGestureDetectorBuilderDelegate {
|
||||
GlobalKey<EditorState> getEditableTextKey();
|
||||
|
||||
bool getForcePressEnabled();
|
||||
|
||||
bool getSelectionEnabled();
|
||||
}
|
||||
|
||||
class EditorTextSelectionGestureDetectorBuilder {
|
||||
EditorTextSelectionGestureDetectorBuilder(this.delegate);
|
||||
|
||||
final EditorTextSelectionGestureDetectorBuilderDelegate delegate;
|
||||
bool shouldShowSelectionToolbar = true;
|
||||
|
||||
EditorState? getEditor() {
|
||||
return delegate.getEditableTextKey().currentState;
|
||||
}
|
||||
|
||||
RenderEditor? getRenderEditor() {
|
||||
return getEditor()!.getRenderEditor();
|
||||
}
|
||||
|
||||
void onTapDown(TapDownDetails details) {
|
||||
getRenderEditor()!.handleTapDown(details);
|
||||
|
||||
final kind = details.kind;
|
||||
shouldShowSelectionToolbar = kind == null ||
|
||||
kind == PointerDeviceKind.touch ||
|
||||
kind == PointerDeviceKind.stylus;
|
||||
}
|
||||
|
||||
void onForcePressStart(ForcePressDetails details) {
|
||||
assert(delegate.getForcePressEnabled());
|
||||
shouldShowSelectionToolbar = true;
|
||||
if (delegate.getSelectionEnabled()) {
|
||||
getRenderEditor()!.selectWordsInRange(
|
||||
details.globalPosition,
|
||||
null,
|
||||
SelectionChangedCause.forcePress,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void onForcePressEnd(ForcePressDetails details) {
|
||||
assert(delegate.getForcePressEnabled());
|
||||
getRenderEditor()!.selectWordsInRange(
|
||||
details.globalPosition,
|
||||
null,
|
||||
SelectionChangedCause.forcePress,
|
||||
);
|
||||
if (shouldShowSelectionToolbar) {
|
||||
getEditor()!.showToolbar();
|
||||
}
|
||||
}
|
||||
|
||||
void onSingleTapUp(TapUpDetails details) {
|
||||
if (delegate.getSelectionEnabled()) {
|
||||
getRenderEditor()!.selectWordEdge(SelectionChangedCause.tap);
|
||||
}
|
||||
}
|
||||
|
||||
void onSingleTapCancel() {}
|
||||
|
||||
void onSingleLongTapStart(LongPressStartDetails details) {
|
||||
if (delegate.getSelectionEnabled()) {
|
||||
getRenderEditor()!.selectPositionAt(
|
||||
details.globalPosition,
|
||||
null,
|
||||
SelectionChangedCause.longPress,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) {
|
||||
if (delegate.getSelectionEnabled()) {
|
||||
getRenderEditor()!.selectPositionAt(
|
||||
details.globalPosition,
|
||||
null,
|
||||
SelectionChangedCause.longPress,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void onSingleLongTapEnd(LongPressEndDetails details) {
|
||||
if (shouldShowSelectionToolbar) {
|
||||
getEditor()!.showToolbar();
|
||||
}
|
||||
}
|
||||
|
||||
void onDoubleTapDown(TapDownDetails details) {
|
||||
if (delegate.getSelectionEnabled()) {
|
||||
getRenderEditor()!.selectWord(SelectionChangedCause.tap);
|
||||
if (shouldShowSelectionToolbar) {
|
||||
getEditor()!.showToolbar();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void onDragSelectionStart(DragStartDetails details) {
|
||||
getRenderEditor()!.selectPositionAt(
|
||||
details.globalPosition,
|
||||
null,
|
||||
SelectionChangedCause.drag,
|
||||
);
|
||||
}
|
||||
|
||||
void onDragSelectionUpdate(
|
||||
DragStartDetails startDetails, DragUpdateDetails updateDetails) {
|
||||
getRenderEditor()!.selectPositionAt(
|
||||
startDetails.globalPosition,
|
||||
updateDetails.globalPosition,
|
||||
SelectionChangedCause.drag,
|
||||
);
|
||||
}
|
||||
|
||||
void onDragSelectionEnd(DragEndDetails details) {}
|
||||
|
||||
Widget build(HitTestBehavior behavior, Widget child) {
|
||||
return EditorTextSelectionGestureDetector(
|
||||
onTapDown: onTapDown,
|
||||
onForcePressStart:
|
||||
delegate.getForcePressEnabled() ? onForcePressStart : null,
|
||||
onForcePressEnd: delegate.getForcePressEnabled() ? onForcePressEnd : null,
|
||||
onSingleTapUp: onSingleTapUp,
|
||||
onSingleTapCancel: onSingleTapCancel,
|
||||
onSingleLongTapStart: onSingleLongTapStart,
|
||||
onSingleLongTapMoveUpdate: onSingleLongTapMoveUpdate,
|
||||
onSingleLongTapEnd: onSingleLongTapEnd,
|
||||
onDoubleTapDown: onDoubleTapDown,
|
||||
onDragSelectionStart: onDragSelectionStart,
|
||||
onDragSelectionUpdate: onDragSelectionUpdate,
|
||||
onDragSelectionEnd: onDragSelectionEnd,
|
||||
behavior: behavior,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
|
||||
class ImageTapWrapper extends StatelessWidget {
|
||||
const ImageTapWrapper({
|
||||
this.imageProvider,
|
||||
});
|
||||
|
||||
final ImageProvider? imageProvider;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
constraints: BoxConstraints.expand(
|
||||
height: MediaQuery.of(context).size.height,
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTapDown: (_) {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: PhotoView(
|
||||
imageProvider: imageProvider,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,129 +0,0 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
//fixme workaround flutter MacOS issue https://github.com/flutter/flutter/issues/75595
|
||||
extension _LogicalKeyboardKeyCaseExt on LogicalKeyboardKey {
|
||||
static const _kUpperToLowerDist = 0x20;
|
||||
static final _kLowerCaseA = LogicalKeyboardKey.keyA.keyId;
|
||||
static final _kLowerCaseZ = LogicalKeyboardKey.keyZ.keyId;
|
||||
|
||||
LogicalKeyboardKey toUpperCase() {
|
||||
if (keyId < _kLowerCaseA || keyId > _kLowerCaseZ) return this;
|
||||
return LogicalKeyboardKey(keyId - _kUpperToLowerDist);
|
||||
}
|
||||
}
|
||||
|
||||
enum InputShortcut { CUT, COPY, PASTE, SELECT_ALL, UNDO, REDO }
|
||||
|
||||
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 KeyboardEventHandler {
|
||||
KeyboardEventHandler(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.keyZ.toUpperCase(),
|
||||
LogicalKeyboardKey.keyZ,
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
final isShortcutModifierPressed =
|
||||
isMacOS ? event.isMetaPressed : event.isControlPressed;
|
||||
|
||||
if (_moveKeys.contains(key)) {
|
||||
onCursorMove(
|
||||
key,
|
||||
isMacOS ? event.isAltPressed : event.isControlPressed,
|
||||
isMacOS ? event.isMetaPressed : event.isAltPressed,
|
||||
event.isShiftPressed);
|
||||
} else if (isShortcutModifierPressed && (_shortcutKeys.contains(key))) {
|
||||
if (key == LogicalKeyboardKey.keyZ ||
|
||||
key == LogicalKeyboardKey.keyZ.toUpperCase()) {
|
||||
onShortcut(
|
||||
event.isShiftPressed ? InputShortcut.REDO : InputShortcut.UNDO);
|
||||
} else {
|
||||
onShortcut(_keyToShortcut[key]);
|
||||
}
|
||||
} else if (key == LogicalKeyboardKey.delete) {
|
||||
onDelete(true);
|
||||
} else if (key == LogicalKeyboardKey.backspace) {
|
||||
onDelete(false);
|
||||
} else {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
}
|