├── example ├── linux │ ├── .gitignore │ ├── main.cc │ ├── flutter │ │ ├── generated_plugin_registrant.h │ │ ├── generated_plugin_registrant.cc │ │ ├── generated_plugins.cmake │ │ └── CMakeLists.txt │ ├── my_application.h │ ├── my_application.cc │ └── CMakeLists.txt ├── ios │ ├── Runner │ │ ├── Runner-Bridging-Header.h │ │ ├── AppDelegate.h │ │ ├── Assets.xcassets │ │ │ ├── LaunchImage.imageset │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ ├── README.md │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ │ └── Contents.json │ │ ├── main.m │ │ ├── AppDelegate.swift │ │ ├── AppDelegate.m │ │ ├── Info.plist │ │ └── Base.lproj │ │ │ ├── Main.storyboard │ │ │ └── LaunchScreen.storyboard │ ├── Flutter │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── AppFrameworkInfo.plist │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ ├── Runner.xcodeproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── .gitignore │ └── Podfile ├── web │ ├── favicon.png │ ├── icons │ │ ├── Icon-192.png │ │ └── Icon-512.png │ ├── manifest.json │ └── index.html ├── android │ ├── gradle.properties │ ├── app │ │ ├── src │ │ │ ├── main │ │ │ │ ├── res │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── drawable │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── drawable-v21 │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── values │ │ │ │ │ │ └── styles.xml │ │ │ │ │ └── values-night │ │ │ │ │ │ └── styles.xml │ │ │ │ ├── kotlin │ │ │ │ │ └── com │ │ │ │ │ │ └── example │ │ │ │ │ │ └── example │ │ │ │ │ │ └── MainActivity.kt │ │ │ │ └── AndroidManifest.xml │ │ │ ├── debug │ │ │ │ └── AndroidManifest.xml │ │ │ └── profile │ │ │ │ └── AndroidManifest.xml │ │ └── build.gradle │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── .gitignore │ ├── settings.gradle │ └── build.gradle ├── macos │ ├── Runner │ │ ├── Configs │ │ │ ├── Debug.xcconfig │ │ │ ├── Release.xcconfig │ │ │ ├── Warnings.xcconfig │ │ │ └── AppInfo.xcconfig │ │ ├── Assets.xcassets │ │ │ └── AppIcon.appiconset │ │ │ │ ├── app_icon_128.png │ │ │ │ ├── app_icon_16.png │ │ │ │ ├── app_icon_256.png │ │ │ │ ├── app_icon_32.png │ │ │ │ ├── app_icon_512.png │ │ │ │ ├── app_icon_64.png │ │ │ │ ├── app_icon_1024.png │ │ │ │ └── Contents.json │ │ ├── AppDelegate.swift │ │ ├── Release.entitlements │ │ ├── MainFlutterWindow.swift │ │ ├── DebugProfile.entitlements │ │ └── Info.plist │ ├── .gitignore │ ├── Flutter │ │ ├── Flutter-Debug.xcconfig │ │ ├── Flutter-Release.xcconfig │ │ └── GeneratedPluginRegistrant.swift │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── Runner.xcodeproj │ │ ├── project.xcworkspace │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ └── Podfile ├── lib │ ├── universal_ui │ │ ├── fake_ui.dart │ │ ├── real_ui.dart │ │ └── universal_ui.dart │ ├── widgets │ │ ├── responsive_widget.dart │ │ └── demo_scaffold.dart │ ├── main.dart │ └── pages │ │ └── read_only_page.dart ├── windows │ ├── runner │ │ ├── resources │ │ │ └── app_icon.ico │ │ ├── resource.h │ │ ├── CMakeLists.txt │ │ ├── utils.h │ │ ├── runner.exe.manifest │ │ ├── run_loop.h │ │ ├── flutter_window.h │ │ ├── main.cpp │ │ ├── utils.cpp │ │ ├── flutter_window.cpp │ │ ├── run_loop.cpp │ │ ├── Runner.rc │ │ └── win32_window.h │ ├── flutter │ │ ├── generated_plugin_registrant.h │ │ ├── generated_plugin_registrant.cc │ │ ├── generated_plugins.cmake │ │ └── CMakeLists.txt │ ├── .gitignore │ └── CMakeLists.txt ├── .metadata ├── README.md ├── .gitignore ├── test │ └── widget_test.dart └── pubspec.yaml ├── lib ├── src │ ├── widgets │ │ ├── style_widgets │ │ │ ├── style_widgets.dart │ │ │ ├── bullet_point.dart │ │ │ ├── checkbox_point.dart │ │ │ └── number_point.dart │ │ ├── float_cursor.dart │ │ ├── toolbar │ │ │ ├── quill_icon_button.dart │ │ │ ├── clear_format_button.dart │ │ │ ├── indent_button.dart │ │ │ ├── history_button.dart │ │ │ ├── toggle_check_list_button.dart │ │ │ ├── image_button.dart │ │ │ ├── video_button.dart │ │ │ ├── quill_dropdown_button.dart │ │ │ ├── arrow_indicated_button_list.dart │ │ │ ├── camera_button.dart │ │ │ ├── select_header_style_button.dart │ │ │ └── toggle_style_button.dart │ │ ├── embeds │ │ │ ├── youtube_video_app.dart │ │ │ ├── video_app.dart │ │ │ └── image.dart │ │ └── keyboard_listener.dart │ ├── models │ │ ├── themes │ │ │ ├── quill_dialog_theme.dart │ │ │ └── quill_icon_theme.dart │ │ ├── documents │ │ │ ├── nodes │ │ │ │ ├── embeddable.dart │ │ │ │ ├── block.dart │ │ │ │ └── node.dart │ │ │ ├── history.dart │ │ │ └── style.dart │ │ └── rules │ │ │ └── rule.dart │ └── utils │ │ ├── platform.dart │ │ ├── string.dart │ │ ├── delta.dart │ │ └── color.dart └── flutter_quill.dart ├── .metadata ├── .github └── ISSUE_TEMPLATE │ └── issue-template.md ├── LICENSE ├── analysis_options.yaml ├── .gitignore └── pubspec.yaml /example/linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /example/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appflowy/flutter-quill/HEAD/example/web/favicon.png -------------------------------------------------------------------------------- /example/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appflowy/flutter-quill/HEAD/example/web/icons/Icon-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appflowy/flutter-quill/HEAD/example/web/icons/Icon-512.png -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /example/lib/universal_ui/fake_ui.dart: -------------------------------------------------------------------------------- 1 | class PlatformViewRegistry { 2 | static void registerViewFactory(String viewId, dynamic cb) {} 3 | } 4 | -------------------------------------------------------------------------------- /example/macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/xcuserdata/ 7 | Podfile.lock -------------------------------------------------------------------------------- /example/windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appflowy/flutter-quill/HEAD/example/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /lib/src/widgets/style_widgets/style_widgets.dart: -------------------------------------------------------------------------------- 1 | export 'bullet_point.dart'; 2 | export 'checkbox_point.dart'; 3 | export 'number_point.dart'; 4 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appflowy/flutter-quill/HEAD/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appflowy/flutter-quill/HEAD/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appflowy/flutter-quill/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appflowy/flutter-quill/HEAD/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appflowy/flutter-quill/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appflowy/flutter-quill/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appflowy/flutter-quill/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appflowy/flutter-quill/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appflowy/flutter-quill/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appflowy/flutter-quill/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appflowy/flutter-quill/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appflowy/flutter-quill/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appflowy/flutter-quill/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appflowy/flutter-quill/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appflowy/flutter-quill/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appflowy/flutter-quill/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appflowy/flutter-quill/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appflowy/flutter-quill/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appflowy/flutter-quill/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appflowy/flutter-quill/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appflowy/flutter-quill/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appflowy/flutter-quill/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appflowy/flutter-quill/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appflowy/flutter-quill/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appflowy/flutter-quill/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appflowy/flutter-quill/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appflowy/flutter-quill/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appflowy/flutter-quill/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/example/example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.app 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appflowy/flutter-quill/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appflowy/flutter-quill/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/linux/main.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | int main(int argc, char** argv) { 4 | g_autoptr(MyApplication) app = my_application_new(); 5 | return g_application_run(G_APPLICATION(app), argc, argv); 6 | } 7 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/lib/universal_ui/real_ui.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui' as ui; 2 | 3 | class PlatformViewRegistry { 4 | static void registerViewFactory(String viewId, dynamic cb) { 5 | ui.platformViewRegistry.registerViewFactory(viewId, cb); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /example/ios/Runner/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char* argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @NSApplicationMain 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip 7 | -------------------------------------------------------------------------------- /example/macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 84f3d28555368a70270e9ac8390a9441df95e752 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 84f3d28555368a70270e9ac8390a9441df95e752 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/linux/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 6 | #define GENERATED_PLUGIN_REGISTRANT_ 7 | 8 | #include 9 | 10 | // Registers Flutter plugins. 11 | void fl_register_plugins(FlPluginRegistry* registry); 12 | 13 | #endif // GENERATED_PLUGIN_REGISTRANT_ 14 | -------------------------------------------------------------------------------- /example/windows/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 6 | #define GENERATED_PLUGIN_REGISTRANT_ 7 | 8 | #include 9 | 10 | // Registers Flutter plugins. 11 | void RegisterPlugins(flutter::PluginRegistry* registry); 12 | 13 | #endif // GENERATED_PLUGIN_REGISTRANT_ 14 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/windows/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | #include "generated_plugin_registrant.h" 6 | 7 | #include 8 | 9 | void RegisterPlugins(flutter::PluginRegistry* registry) { 10 | UrlLauncherWindowsRegisterWithRegistrar( 11 | registry->GetRegistrarForPlugin("UrlLauncherWindows")); 12 | } 13 | -------------------------------------------------------------------------------- /example/windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral/ 2 | 3 | # Visual Studio user-specific files. 4 | *.suo 5 | *.user 6 | *.userosscache 7 | *.sln.docstates 8 | 9 | # Visual Studio build-related files. 10 | x64/ 11 | x86/ 12 | 13 | # Visual Studio cache files 14 | # files ending in .cache can be ignored 15 | *.[Cc]ache 16 | # but keep track of directories ending in .cache 17 | !*.[Cc]ache/ 18 | -------------------------------------------------------------------------------- /example/macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import path_provider_macos 9 | import url_launcher_macos 10 | 11 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 12 | PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) 13 | UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) 14 | } 15 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | #import "GeneratedPluginRegistrant.h" 3 | 4 | @implementation AppDelegate 5 | 6 | - (BOOL)application:(UIApplication *)application 7 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 8 | [GeneratedPluginRegistrant registerWithRegistry:self]; 9 | // Override point for customization after application launch. 10 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 11 | } 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /example/linux/my_application.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_MY_APPLICATION_H_ 2 | #define FLUTTER_MY_APPLICATION_H_ 3 | 4 | #include 5 | 6 | G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, 7 | GtkApplication) 8 | 9 | /** 10 | * my_application_new: 11 | * 12 | * Creates a new Flutter-based application. 13 | * 14 | * Returns: a new #MyApplication. 15 | */ 16 | MyApplication* my_application_new(); 17 | 18 | #endif // FLUTTER_MY_APPLICATION_H_ 19 | -------------------------------------------------------------------------------- /example/macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | class MainFlutterWindow: NSWindow { 5 | override func awakeFromNib() { 6 | let flutterViewController = FlutterViewController.init() 7 | let windowFrame = self.frame 8 | self.contentViewController = flutterViewController 9 | self.setFrame(windowFrame, display: true) 10 | 11 | RegisterGeneratedPlugins(registry: flutterViewController) 12 | 13 | super.awakeFromNib() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example/linux/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | #include "generated_plugin_registrant.h" 6 | 7 | #include 8 | 9 | void fl_register_plugins(FlPluginRegistry* registry) { 10 | g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = 11 | fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); 12 | url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); 13 | } 14 | -------------------------------------------------------------------------------- /example/macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | com.apple.security.network.server 10 | 11 | com.apple.security.network.client 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/windows/runner/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by Runner.rc 4 | // 5 | #define IDI_APP_ICON 101 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 102 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/models/themes/quill_dialog_theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class QuillDialogTheme { 4 | QuillDialogTheme( 5 | {this.labelTextStyle, this.inputTextStyle, this.dialogBackgroundColor}); 6 | 7 | ///The text style to use for the label shown in the link-input dialog 8 | final TextStyle? labelTextStyle; 9 | 10 | ///The text style to use for the input text shown in the link-input dialog 11 | final TextStyle? inputTextStyle; 12 | 13 | ///The background color for the [LinkDialog()] 14 | final Color? dialogBackgroundColor; 15 | } 16 | -------------------------------------------------------------------------------- /example/linux/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | url_launcher_linux 7 | ) 8 | 9 | set(PLUGIN_BUNDLED_LIBRARIES) 10 | 11 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 12 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) 13 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 14 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 15 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 16 | endforeach(plugin) 17 | -------------------------------------------------------------------------------- /example/windows/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | url_launcher_windows 7 | ) 8 | 9 | set(PLUGIN_BUNDLED_LIBRARIES) 10 | 11 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 12 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 13 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 14 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 15 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 16 | endforeach(plugin) 17 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # app 2 | 3 | demo app 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.dev/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue template 3 | about: Common things to fill 4 | title: "[Web] or [Mobile] or [Desktop]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | My issue is about [Web] 11 | My issue is about [Mobile] 12 | My issue is about [Desktop] 13 | 14 | I have tried running `example` directory successfully before creating an issue here. 15 | 16 | Please note that we are using latest flutter version in stable channel on branch master. If you are using beta or master channel, or you are not using latest flutter version in stable channel, you may experience error. 17 | 18 | -------------------------------------------------------------------------------- /lib/src/widgets/style_widgets/bullet_point.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class QuillBulletPoint extends StatelessWidget { 4 | const QuillBulletPoint({ 5 | required this.style, 6 | required this.width, 7 | Key? key, 8 | }) : super(key: key); 9 | 10 | final TextStyle style; 11 | final double width; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Container( 16 | alignment: AlignmentDirectional.topEnd, 17 | width: width, 18 | padding: const EdgeInsetsDirectional.only(end: 13), 19 | child: Text('•', style: style), 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /example/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "short_name": "app", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = app 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = com.example.app 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2021 com.example. All rights reserved. 15 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /example/windows/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15) 2 | project(runner LANGUAGES CXX) 3 | 4 | add_executable(${BINARY_NAME} WIN32 5 | "flutter_window.cpp" 6 | "main.cpp" 7 | "run_loop.cpp" 8 | "utils.cpp" 9 | "win32_window.cpp" 10 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 11 | "Runner.rc" 12 | "runner.exe.manifest" 13 | ) 14 | apply_standard_settings(${BINARY_NAME}) 15 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 16 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) 17 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 18 | add_dependencies(${BINARY_NAME} flutter_assemble) 19 | -------------------------------------------------------------------------------- /lib/flutter_quill.dart: -------------------------------------------------------------------------------- 1 | library flutter_quill; 2 | 3 | export 'src/models/documents/attribute.dart'; 4 | export 'src/models/documents/document.dart'; 5 | export 'src/models/documents/nodes/embeddable.dart'; 6 | export 'src/models/documents/nodes/leaf.dart'; 7 | export 'src/models/quill_delta.dart'; 8 | export 'src/models/themes/quill_dialog_theme.dart'; 9 | export 'src/models/themes/quill_icon_theme.dart'; 10 | export 'src/widgets/controller.dart'; 11 | export 'src/widgets/default_styles.dart'; 12 | export 'src/widgets/editor.dart'; 13 | export 'src/widgets/link.dart' show LinkActionPickerDelegate, LinkMenuAction; 14 | export 'src/widgets/style_widgets/style_widgets.dart'; 15 | export 'src/widgets/toolbar.dart'; 16 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.6.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:4.1.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /example/windows/runner/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_UTILS_H_ 2 | #define RUNNER_UTILS_H_ 3 | 4 | #include 5 | #include 6 | 7 | // Creates a console for the process, and redirects stdout and stderr to 8 | // it for both the runner and the Flutter library. 9 | void CreateAndAttachConsole(); 10 | 11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string 12 | // encoded in UTF-8. Returns an empty std::string on failure. 13 | std::string Utf8FromUtf16(const wchar_t* utf16_string); 14 | 15 | // Gets the command line arguments passed in as a std::vector, 16 | // encoded in UTF-8. Returns an empty std::vector on failure. 17 | std::vector GetCommandLineArguments(); 18 | 19 | #endif // RUNNER_UTILS_H_ 20 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | pubspec.lock -------------------------------------------------------------------------------- /lib/src/utils/platform.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | 3 | bool isMobile([TargetPlatform? targetPlatform]) { 4 | targetPlatform ??= defaultTargetPlatform; 5 | return {TargetPlatform.iOS, TargetPlatform.android}.contains(targetPlatform); 6 | } 7 | 8 | bool isDesktop([TargetPlatform? targetPlatform]) { 9 | targetPlatform ??= defaultTargetPlatform; 10 | return {TargetPlatform.macOS, TargetPlatform.linux, TargetPlatform.windows} 11 | .contains(targetPlatform); 12 | } 13 | 14 | bool isKeyboardOS([TargetPlatform? targetPlatform]) { 15 | targetPlatform ??= defaultTargetPlatform; 16 | return isDesktop(targetPlatform) || targetPlatform == TargetPlatform.fuchsia; 17 | } 18 | 19 | bool isAppleOS([TargetPlatform? targetPlatform]) { 20 | targetPlatform ??= defaultTargetPlatform; 21 | return { 22 | TargetPlatform.macOS, 23 | TargetPlatform.iOS, 24 | }.contains(targetPlatform); 25 | } 26 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 9.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /lib/src/widgets/float_cursor.dart: -------------------------------------------------------------------------------- 1 | // The corner radius of the floating cursor in pixels. 2 | import 'dart:ui'; 3 | 4 | import 'cursor.dart'; 5 | 6 | const Radius _kFloatingCaretRadius = Radius.circular(1); 7 | 8 | /// Floating painter responsible for painting the floating cursor when 9 | /// floating mode is activated 10 | class FloatingCursorPainter { 11 | FloatingCursorPainter({ 12 | required this.floatingCursorRect, 13 | required this.style, 14 | }); 15 | 16 | CursorStyle style; 17 | 18 | Rect? floatingCursorRect; 19 | 20 | final Paint floatingCursorPaint = Paint(); 21 | 22 | void paint(Canvas canvas) { 23 | final floatingCursorRect = this.floatingCursorRect; 24 | final floatingCursorColor = style.color.withOpacity(0.75); 25 | if (floatingCursorRect == null) return; 26 | canvas.drawRRect( 27 | RRect.fromRectAndRadius(floatingCursorRect, _kFloatingCaretRadius), 28 | floatingCursorPaint..color = floatingCursorColor, 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/src/models/themes/quill_icon_theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class QuillIconTheme { 4 | const QuillIconTheme({ 5 | this.iconSelectedColor, 6 | this.iconUnselectedColor, 7 | this.iconSelectedFillColor, 8 | this.iconUnselectedFillColor, 9 | this.disabledIconColor, 10 | this.disabledIconFillColor, 11 | }); 12 | 13 | ///The color to use for selected icons in the toolbar 14 | final Color? iconSelectedColor; 15 | 16 | ///The color to use for unselected icons in the toolbar 17 | final Color? iconUnselectedColor; 18 | 19 | ///The fill color to use for the selected icons in the toolbar 20 | final Color? iconSelectedFillColor; 21 | 22 | ///The fill color to use for the unselected icons in the toolbar 23 | final Color? iconUnselectedFillColor; 24 | 25 | ///The color to use for disabled icons in the toolbar 26 | final Color? disabledIconColor; 27 | 28 | ///The fill color to use for disabled icons in the toolbar 29 | final Color? disabledIconFillColor; 30 | } 31 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Xin Yao 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:app/main.dart'; 9 | import 'package:flutter/material.dart'; 10 | import 'package:flutter_test/flutter_test.dart'; 11 | 12 | void main() { 13 | testWidgets('Counter increments smoke test', (tester) async { 14 | // Build our app and trigger a frame. 15 | await tester.pumpWidget(MyApp()); 16 | 17 | // Verify that our counter starts at 0. 18 | expect(find.text('0'), findsOneWidget); 19 | expect(find.text('1'), findsNothing); 20 | 21 | // Tap the '+' icon and trigger a frame. 22 | await tester.tap(find.byIcon(Icons.add)); 23 | await tester.pump(); 24 | 25 | // Verify that our counter has incremented. 26 | expect(find.text('0'), findsNothing); 27 | expect(find.text('1'), findsOneWidget); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/widgets/toolbar/quill_icon_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class QuillIconButton extends StatelessWidget { 4 | const QuillIconButton({ 5 | required this.onPressed, 6 | this.icon, 7 | this.size = 40, 8 | this.fillColor, 9 | this.hoverElevation = 1, 10 | this.highlightElevation = 1, 11 | Key? key, 12 | }) : super(key: key); 13 | 14 | final VoidCallback? onPressed; 15 | final Widget? icon; 16 | final double size; 17 | final Color? fillColor; 18 | final double hoverElevation; 19 | final double highlightElevation; 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | return ConstrainedBox( 24 | constraints: BoxConstraints.tightFor(width: size, height: size), 25 | child: RawMaterialButton( 26 | visualDensity: VisualDensity.compact, 27 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)), 28 | fillColor: fillColor, 29 | elevation: 0, 30 | hoverElevation: hoverElevation, 31 | highlightElevation: hoverElevation, 32 | onPressed: onPressed, 33 | child: icon, 34 | ), 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:pedantic/analysis_options.yaml 2 | 3 | analyzer: 4 | errors: 5 | undefined_prefixed_name: ignore 6 | unsafe_html: ignore 7 | linter: 8 | rules: 9 | - always_declare_return_types 10 | - always_put_required_named_parameters_first 11 | - annotate_overrides 12 | - avoid_empty_else 13 | - avoid_escaping_inner_quotes 14 | - avoid_print 15 | - avoid_redundant_argument_values 16 | - avoid_types_on_closure_parameters 17 | - avoid_void_async 18 | - cascade_invocations 19 | - directives_ordering 20 | - lines_longer_than_80_chars 21 | - omit_local_variable_types 22 | - prefer_const_constructors 23 | - prefer_const_constructors_in_immutables 24 | - prefer_const_declarations 25 | - prefer_final_fields 26 | - prefer_final_in_for_each 27 | - prefer_final_locals 28 | - prefer_initializing_formals 29 | - prefer_int_literals 30 | - prefer_interpolation_to_compose_strings 31 | - prefer_relative_imports 32 | - prefer_single_quotes 33 | - sort_constructors_first 34 | - sort_unnamed_constructors_first 35 | - unnecessary_lambdas 36 | - unnecessary_parenthesis 37 | - unnecessary_string_interpolations 38 | -------------------------------------------------------------------------------- /example/macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | $(PRODUCT_COPYRIGHT) 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /example/windows/runner/run_loop.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_RUN_LOOP_H_ 2 | #define RUNNER_RUN_LOOP_H_ 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | // A runloop that will service events for Flutter instances as well 10 | // as native messages. 11 | class RunLoop { 12 | public: 13 | RunLoop(); 14 | ~RunLoop(); 15 | 16 | // Prevent copying 17 | RunLoop(RunLoop const&) = delete; 18 | RunLoop& operator=(RunLoop const&) = delete; 19 | 20 | // Runs the run loop until the application quits. 21 | void Run(); 22 | 23 | // Registers the given Flutter instance for event servicing. 24 | void RegisterFlutterInstance( 25 | flutter::FlutterEngine* flutter_instance); 26 | 27 | // Unregisters the given Flutter instance from event servicing. 28 | void UnregisterFlutterInstance( 29 | flutter::FlutterEngine* flutter_instance); 30 | 31 | private: 32 | using TimePoint = std::chrono::steady_clock::time_point; 33 | 34 | // Processes all currently pending messages for registered Flutter instances. 35 | TimePoint ProcessFlutterMessages(); 36 | 37 | std::set flutter_instances_; 38 | }; 39 | 40 | #endif // RUNNER_RUN_LOOP_H_ 41 | -------------------------------------------------------------------------------- /example/windows/runner/flutter_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_FLUTTER_WINDOW_H_ 2 | #define RUNNER_FLUTTER_WINDOW_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "run_loop.h" 10 | #include "win32_window.h" 11 | 12 | // A window that does nothing but host a Flutter view. 13 | class FlutterWindow : public Win32Window { 14 | public: 15 | // Creates a new FlutterWindow driven by the |run_loop|, hosting a 16 | // Flutter view running |project|. 17 | explicit FlutterWindow(RunLoop* run_loop, 18 | const flutter::DartProject& project); 19 | virtual ~FlutterWindow(); 20 | 21 | protected: 22 | // Win32Window: 23 | bool OnCreate() override; 24 | void OnDestroy() override; 25 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, 26 | LPARAM const lparam) noexcept override; 27 | 28 | private: 29 | // The run loop driving events for this window. 30 | RunLoop* run_loop_; 31 | 32 | // The project to run. 33 | flutter::DartProject project_; 34 | 35 | // The Flutter instance hosted by this window. 36 | std::unique_ptr flutter_controller_; 37 | }; 38 | 39 | #endif // RUNNER_FLUTTER_WINDOW_H_ 40 | -------------------------------------------------------------------------------- /example/lib/widgets/responsive_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ResponsiveWidget extends StatelessWidget { 4 | const ResponsiveWidget({ 5 | required this.largeScreen, 6 | this.mediumScreen, 7 | this.smallScreen, 8 | Key? key, 9 | }) : super(key: key); 10 | 11 | final Widget largeScreen; 12 | final Widget? mediumScreen; 13 | final Widget? smallScreen; 14 | 15 | static bool isSmallScreen(BuildContext context) { 16 | return MediaQuery.of(context).size.width < 800; 17 | } 18 | 19 | static bool isLargeScreen(BuildContext context) { 20 | return MediaQuery.of(context).size.width > 1200; 21 | } 22 | 23 | static bool isMediumScreen(BuildContext context) { 24 | return MediaQuery.of(context).size.width >= 800 && 25 | MediaQuery.of(context).size.width <= 1200; 26 | } 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | return LayoutBuilder( 31 | builder: (context, constraints) { 32 | if (constraints.maxWidth > 1200) { 33 | return largeScreen; 34 | } else if (constraints.maxWidth <= 1200 && 35 | constraints.maxWidth >= 800) { 36 | return mediumScreen ?? largeScreen; 37 | } else { 38 | return smallScreen ?? largeScreen; 39 | } 40 | }, 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/src/utils/string.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | 3 | Map parseKeyValuePairs(String s, Set targetKeys) { 4 | final result = {}; 5 | final pairs = s.split(';'); 6 | for (final pair in pairs) { 7 | final _index = pair.indexOf(':'); 8 | if (_index < 0) { 9 | continue; 10 | } 11 | final _key = pair.substring(0, _index).trim(); 12 | if (targetKeys.contains(_key)) { 13 | result[_key] = pair.substring(_index + 1).trim(); 14 | } 15 | } 16 | 17 | return result; 18 | } 19 | 20 | Alignment getAlignment(String? s) { 21 | const _defaultAlignment = Alignment.center; 22 | if (s == null) { 23 | return _defaultAlignment; 24 | } 25 | 26 | final _index = [ 27 | 'topLeft', 28 | 'topCenter', 29 | 'topRight', 30 | 'centerLeft', 31 | 'center', 32 | 'centerRight', 33 | 'bottomLeft', 34 | 'bottomCenter', 35 | 'bottomRight' 36 | ].indexOf(s); 37 | if (_index < 0) { 38 | return _defaultAlignment; 39 | } 40 | 41 | return [ 42 | Alignment.topLeft, 43 | Alignment.topCenter, 44 | Alignment.topRight, 45 | Alignment.centerLeft, 46 | Alignment.center, 47 | Alignment.centerRight, 48 | Alignment.bottomLeft, 49 | Alignment.bottomCenter, 50 | Alignment.bottomRight 51 | ][_index]; 52 | } 53 | -------------------------------------------------------------------------------- /lib/src/models/documents/nodes/embeddable.dart: -------------------------------------------------------------------------------- 1 | /// An object which can be embedded into a Quill document. 2 | /// 3 | /// See also: 4 | /// 5 | /// * [BlockEmbed] which represents a block embed. 6 | class Embeddable { 7 | const Embeddable(this.type, this.data); 8 | 9 | /// The type of this object. 10 | final String type; 11 | 12 | /// The data payload of this object. 13 | final dynamic data; 14 | 15 | Map toJson() { 16 | final m = {type: data}; 17 | return m; 18 | } 19 | 20 | static Embeddable fromJson(Map json) { 21 | final m = Map.from(json); 22 | assert(m.length == 1, 'Embeddable map must only have one key'); 23 | 24 | return BlockEmbed(m.keys.first, m.values.first); 25 | } 26 | } 27 | 28 | /// There are two built-in embed types supported by Quill documents, however 29 | /// the document model itself does not make any assumptions about the types 30 | /// of embedded objects and allows users to define their own types. 31 | class BlockEmbed extends Embeddable { 32 | const BlockEmbed(String type, String data) : super(type, data); 33 | 34 | static const String imageType = 'image'; 35 | static BlockEmbed image(String imageUrl) => BlockEmbed(imageType, imageUrl); 36 | 37 | static const String videoType = 'video'; 38 | static BlockEmbed video(String videoUrl) => BlockEmbed(videoType, videoUrl); 39 | } 40 | -------------------------------------------------------------------------------- /example/windows/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "flutter_window.h" 6 | #include "run_loop.h" 7 | #include "utils.h" 8 | 9 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, 10 | _In_ wchar_t *command_line, _In_ int show_command) { 11 | // Attach to console when present (e.g., 'flutter run') or create a 12 | // new console when running with a debugger. 13 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { 14 | CreateAndAttachConsole(); 15 | } 16 | 17 | // Initialize COM, so that it is available for use in the library and/or 18 | // plugins. 19 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 20 | 21 | RunLoop run_loop; 22 | 23 | flutter::DartProject project(L"data"); 24 | 25 | std::vector command_line_arguments = 26 | GetCommandLineArguments(); 27 | 28 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 29 | 30 | FlutterWindow window(&run_loop, project); 31 | Win32Window::Point origin(10, 10); 32 | Win32Window::Size size(1280, 720); 33 | if (!window.CreateAndShow(L"app", origin, size)) { 34 | return EXIT_FAILURE; 35 | } 36 | window.SetQuitOnClose(true); 37 | 38 | run_loop.Run(); 39 | 40 | ::CoUninitialize(); 41 | return EXIT_SUCCESS; 42 | } 43 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'pages/home_page.dart'; 4 | 5 | void main() { 6 | WidgetsFlutterBinding.ensureInitialized(); 7 | runApp(MyApp()); 8 | } 9 | 10 | class MyApp extends StatelessWidget { 11 | // This widget is the root of your application. 12 | @override 13 | Widget build(BuildContext context) { 14 | return MaterialApp( 15 | debugShowCheckedModeBanner: false, 16 | title: 'Quill Demo', 17 | theme: ThemeData( 18 | // This is the theme of your application. 19 | // 20 | // Try running your application with "flutter run". You'll see the 21 | // application has a blue toolbar. Then, without quitting the app, try 22 | // changing the primarySwatch below to Colors.green and then invoke 23 | // "hot reload" (press "r" in the console where you ran "flutter run", 24 | // or simply save your changes to "hot reload" in a Flutter IDE). 25 | // Notice that the counter didn't reset back to zero; the application 26 | // is not restarted. 27 | primarySwatch: Colors.blue, 28 | // This makes the visual density adapt to the platform that you run 29 | // the app on. For desktop platforms, the controls will be smaller and 30 | // closer together (more dense) than on mobile platforms. 31 | visualDensity: VisualDensity.adaptivePlatformDensity, 32 | ), 33 | home: HomePage(), 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /example/macos/Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '10.11' 2 | 3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 | 6 | project 'Runner', { 7 | 'Debug' => :debug, 8 | 'Profile' => :release, 9 | 'Release' => :release, 10 | } 11 | 12 | def flutter_root 13 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) 14 | unless File.exist?(generated_xcode_build_settings_path) 15 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" 16 | end 17 | 18 | File.foreach(generated_xcode_build_settings_path) do |line| 19 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 20 | return matches[1].strip if matches 21 | end 22 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" 23 | end 24 | 25 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 26 | 27 | flutter_macos_podfile_setup 28 | 29 | target 'Runner' do 30 | use_frameworks! 31 | use_modular_headers! 32 | 33 | flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) 34 | end 35 | 36 | post_install do |installer| 37 | installer.pods_project.targets.each do |target| 38 | flutter_additional_macos_build_settings(target) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /example/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | app 30 | 31 | 32 | 33 | 36 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "app_icon_16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "app_icon_32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "app_icon_32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "app_icon_64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "app_icon_128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "app_icon_256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "app_icon_256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "app_icon_512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "app_icon_512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "app_icon_1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | app 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/src/widgets/toolbar/clear_format_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../../flutter_quill.dart'; 4 | 5 | class ClearFormatButton extends StatefulWidget { 6 | const ClearFormatButton({ 7 | required this.icon, 8 | required this.controller, 9 | this.iconSize = kDefaultIconSize, 10 | this.iconTheme, 11 | Key? key, 12 | }) : super(key: key); 13 | 14 | final IconData icon; 15 | final double iconSize; 16 | 17 | final QuillController controller; 18 | 19 | final QuillIconTheme? iconTheme; 20 | 21 | @override 22 | _ClearFormatButtonState createState() => _ClearFormatButtonState(); 23 | } 24 | 25 | class _ClearFormatButtonState extends State { 26 | @override 27 | Widget build(BuildContext context) { 28 | final theme = Theme.of(context); 29 | final iconColor = 30 | widget.iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; 31 | final fillColor = 32 | widget.iconTheme?.iconUnselectedFillColor ?? theme.canvasColor; 33 | return QuillIconButton( 34 | highlightElevation: 0, 35 | hoverElevation: 0, 36 | size: widget.iconSize * kIconButtonFactor, 37 | icon: Icon(widget.icon, size: widget.iconSize, color: iconColor), 38 | fillColor: fillColor, 39 | onPressed: () { 40 | final attrs = {}; 41 | for (final style in widget.controller.getAllSelectionStyles()) { 42 | for (final attr in style.attributes.values) { 43 | attrs.add(attr); 44 | } 45 | } 46 | for (final attr in attrs) { 47 | widget.controller.formatSelection(Attribute.clone(attr, null)); 48 | } 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | build/ 32 | 33 | # Android related 34 | **/android/**/gradle-wrapper.jar 35 | **/android/.gradle 36 | **/android/captures/ 37 | **/android/gradlew 38 | **/android/gradlew.bat 39 | **/android/local.properties 40 | **/android/**/GeneratedPluginRegistrant.java 41 | 42 | # iOS/XCode related 43 | **/ios/**/*.mode1v3 44 | **/ios/**/*.mode2v3 45 | **/ios/**/*.moved-aside 46 | **/ios/**/*.pbxuser 47 | **/ios/**/*.perspectivev3 48 | **/ios/**/*sync/ 49 | **/ios/**/.sconsign.dblite 50 | **/ios/**/.tags* 51 | **/ios/**/.vagrant/ 52 | **/ios/**/DerivedData/ 53 | **/ios/**/Icon? 54 | **/ios/**/Pods/ 55 | **/ios/**/.symlinks/ 56 | **/ios/**/profile 57 | **/ios/**/xcuserdata 58 | **/ios/.generated/ 59 | **/ios/Flutter/App.framework 60 | **/ios/Flutter/Flutter.framework 61 | **/ios/Flutter/Flutter.podspec 62 | **/ios/Flutter/Generated.xcconfig 63 | **/ios/Flutter/app.flx 64 | **/ios/Flutter/app.zip 65 | **/ios/Flutter/flutter_assets/ 66 | **/ios/Flutter/flutter_export_environment.sh 67 | **/ios/ServiceDefinitions.json 68 | **/ios/Runner/GeneratedPluginRegistrant.* 69 | 70 | # Exceptions to above rules. 71 | !**/ios/**/default.mode1v3 72 | !**/ios/**/default.mode2v3 73 | !**/ios/**/default.pbxuser 74 | !**/ios/**/default.perspectivev3 75 | pubspec.lock -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /example/windows/runner/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | void CreateAndAttachConsole() { 11 | if (::AllocConsole()) { 12 | FILE *unused; 13 | if (freopen_s(&unused, "CONOUT$", "w", stdout)) { 14 | _dup2(_fileno(stdout), 1); 15 | } 16 | if (freopen_s(&unused, "CONOUT$", "w", stderr)) { 17 | _dup2(_fileno(stdout), 2); 18 | } 19 | std::ios::sync_with_stdio(); 20 | FlutterDesktopResyncOutputStreams(); 21 | } 22 | } 23 | 24 | std::vector GetCommandLineArguments() { 25 | // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. 26 | int argc; 27 | wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); 28 | if (argv == nullptr) { 29 | return std::vector(); 30 | } 31 | 32 | std::vector command_line_arguments; 33 | 34 | // Skip the first argument as it's the binary name. 35 | for (int i = 1; i < argc; i++) { 36 | command_line_arguments.push_back(Utf8FromUtf16(argv[i])); 37 | } 38 | 39 | ::LocalFree(argv); 40 | 41 | return command_line_arguments; 42 | } 43 | 44 | std::string Utf8FromUtf16(const wchar_t* utf16_string) { 45 | if (utf16_string == nullptr) { 46 | return std::string(); 47 | } 48 | int target_length = ::WideCharToMultiByte( 49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 50 | -1, nullptr, 0, nullptr, nullptr); 51 | if (target_length == 0) { 52 | return std::string(); 53 | } 54 | std::string utf8_string; 55 | utf8_string.resize(target_length); 56 | int converted_length = ::WideCharToMultiByte( 57 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 58 | -1, utf8_string.data(), 59 | target_length, nullptr, nullptr); 60 | if (converted_length == 0) { 61 | return std::string(); 62 | } 63 | return utf8_string; 64 | } 65 | -------------------------------------------------------------------------------- /example/lib/universal_ui/universal_ui.dart: -------------------------------------------------------------------------------- 1 | library universal_ui; 2 | 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:flutter_quill/flutter_quill.dart'; 6 | import 'package:universal_html/html.dart' as html; 7 | 8 | import '../widgets/responsive_widget.dart'; 9 | import 'fake_ui.dart' if (dart.library.html) 'real_ui.dart' as ui_instance; 10 | 11 | class PlatformViewRegistryFix { 12 | void registerViewFactory(dynamic x, dynamic y) { 13 | if (kIsWeb) { 14 | ui_instance.PlatformViewRegistry.registerViewFactory( 15 | x, 16 | y, 17 | ); 18 | } 19 | } 20 | } 21 | 22 | class UniversalUI { 23 | PlatformViewRegistryFix platformViewRegistry = PlatformViewRegistryFix(); 24 | } 25 | 26 | var ui = UniversalUI(); 27 | 28 | Widget defaultEmbedBuilderWeb(BuildContext context, Embed node, bool readOnly) { 29 | switch (node.value.type) { 30 | case 'image': 31 | final imageUrl = node.value.data; 32 | final size = MediaQuery.of(context).size; 33 | UniversalUI().platformViewRegistry.registerViewFactory( 34 | imageUrl, (viewId) => html.ImageElement()..src = imageUrl); 35 | return Padding( 36 | padding: EdgeInsets.only( 37 | right: ResponsiveWidget.isMediumScreen(context) 38 | ? size.width * 0.5 39 | : (ResponsiveWidget.isLargeScreen(context)) 40 | ? size.width * 0.75 41 | : size.width * 0.2, 42 | ), 43 | child: SizedBox( 44 | height: MediaQuery.of(context).size.height * 0.45, 45 | child: HtmlElementView( 46 | viewType: imageUrl, 47 | ), 48 | ), 49 | ); 50 | 51 | default: 52 | throw UnimplementedError( 53 | 'Embeddable type "${node.value.type}" is not supported by default ' 54 | 'embed builder of QuillEditor. You must pass your own builder function ' 55 | 'to embedBuilder property of QuillEditor or QuillField widgets.', 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/src/models/documents/nodes/block.dart: -------------------------------------------------------------------------------- 1 | import '../../quill_delta.dart'; 2 | import 'container.dart'; 3 | import 'line.dart'; 4 | import 'node.dart'; 5 | 6 | /// Represents a group of adjacent [Line]s with the same block style. 7 | /// 8 | /// Block elements are: 9 | /// - Blockquote 10 | /// - Header 11 | /// - Indent 12 | /// - List 13 | /// - Text Alignment 14 | /// - Text Direction 15 | /// - Code Block 16 | class Block extends Container { 17 | /// Creates new unmounted [Block]. 18 | @override 19 | Node newInstance() => Block(); 20 | 21 | @override 22 | Line get defaultChild => Line(); 23 | 24 | @override 25 | Delta toDelta() { 26 | return children 27 | .map((child) => child.toDelta()) 28 | .fold(Delta(), (a, b) => a.concat(b)); 29 | } 30 | 31 | @override 32 | void adjust() { 33 | if (isEmpty) { 34 | final sibling = previous; 35 | unlink(); 36 | if (sibling != null) { 37 | sibling.adjust(); 38 | } 39 | return; 40 | } 41 | 42 | var block = this; 43 | final prev = block.previous; 44 | // merging it with previous block if style is the same 45 | if (!block.isFirst && 46 | block.previous is Block && 47 | prev!.style == block.style) { 48 | block 49 | ..moveChildToNewParent(prev as Container?) 50 | ..unlink(); 51 | block = prev as Block; 52 | } 53 | final next = block.next; 54 | // merging it with next block if style is the same 55 | if (!block.isLast && block.next is Block && next!.style == block.style) { 56 | (next as Block).moveChildToNewParent(block); 57 | next.unlink(); 58 | } 59 | } 60 | 61 | @override 62 | String toString() { 63 | final block = style.attributes.toString(); 64 | final buffer = StringBuffer('§ {$block}\n'); 65 | for (final child in children) { 66 | final tree = child.isLast ? '└' : '├'; 67 | buffer.write(' $tree $child'); 68 | if (!child.isLast) buffer.writeln(); 69 | } 70 | return buffer.toString(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/src/widgets/toolbar/indent_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../../flutter_quill.dart'; 4 | 5 | class IndentButton extends StatefulWidget { 6 | const IndentButton({ 7 | required this.icon, 8 | required this.controller, 9 | required this.isIncrease, 10 | this.iconSize = kDefaultIconSize, 11 | this.iconTheme, 12 | Key? key, 13 | }) : super(key: key); 14 | 15 | final IconData icon; 16 | final double iconSize; 17 | final QuillController controller; 18 | final bool isIncrease; 19 | 20 | final QuillIconTheme? iconTheme; 21 | 22 | @override 23 | _IndentButtonState createState() => _IndentButtonState(); 24 | } 25 | 26 | class _IndentButtonState extends State { 27 | @override 28 | Widget build(BuildContext context) { 29 | final theme = Theme.of(context); 30 | 31 | final iconColor = 32 | widget.iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; 33 | final iconFillColor = 34 | widget.iconTheme?.iconUnselectedFillColor ?? theme.canvasColor; 35 | return QuillIconButton( 36 | highlightElevation: 0, 37 | hoverElevation: 0, 38 | size: widget.iconSize * 1.77, 39 | icon: Icon(widget.icon, size: widget.iconSize, color: iconColor), 40 | fillColor: iconFillColor, 41 | onPressed: () { 42 | final indent = widget.controller 43 | .getSelectionStyle() 44 | .attributes[Attribute.indent.key]; 45 | if (indent == null) { 46 | if (widget.isIncrease) { 47 | widget.controller.formatSelection(Attribute.indentL1); 48 | } 49 | return; 50 | } 51 | if (indent.value == 1 && !widget.isIncrease) { 52 | widget.controller 53 | .formatSelection(Attribute.clone(Attribute.indentL1, null)); 54 | return; 55 | } 56 | if (widget.isIncrease) { 57 | widget.controller 58 | .formatSelection(Attribute.getIndentLevel(indent.value + 1)); 59 | return; 60 | } 61 | widget.controller 62 | .formatSelection(Attribute.getIndentLevel(indent.value - 1)); 63 | }, 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /example/lib/pages/read_only_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_quill/flutter_quill.dart' hide Text; 4 | 5 | import '../universal_ui/universal_ui.dart'; 6 | import '../widgets/demo_scaffold.dart'; 7 | 8 | class ReadOnlyPage extends StatefulWidget { 9 | @override 10 | _ReadOnlyPageState createState() => _ReadOnlyPageState(); 11 | } 12 | 13 | class _ReadOnlyPageState extends State { 14 | final FocusNode _focusNode = FocusNode(); 15 | 16 | bool _edit = false; 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return DemoScaffold( 21 | documentFilename: 'sample_data.json', 22 | builder: _buildContent, 23 | showToolbar: _edit == true, 24 | floatingActionButton: FloatingActionButton.extended( 25 | label: Text(_edit == true ? 'Done' : 'Edit'), 26 | onPressed: _toggleEdit, 27 | icon: Icon(_edit == true ? Icons.check : Icons.edit)), 28 | ); 29 | } 30 | 31 | Widget _buildContent(BuildContext context, QuillController? controller) { 32 | var quillEditor = QuillEditor( 33 | controller: controller!, 34 | scrollController: ScrollController(), 35 | scrollable: true, 36 | focusNode: _focusNode, 37 | autoFocus: true, 38 | readOnly: !_edit, 39 | expands: false, 40 | padding: EdgeInsets.zero, 41 | ); 42 | if (kIsWeb) { 43 | quillEditor = QuillEditor( 44 | controller: controller, 45 | scrollController: ScrollController(), 46 | scrollable: true, 47 | focusNode: _focusNode, 48 | autoFocus: true, 49 | readOnly: !_edit, 50 | expands: false, 51 | padding: EdgeInsets.zero, 52 | embedBuilder: defaultEmbedBuilderWeb); 53 | } 54 | return Padding( 55 | padding: const EdgeInsets.all(8), 56 | child: Container( 57 | decoration: BoxDecoration( 58 | color: Colors.white, 59 | border: Border.all(color: Colors.grey.shade200), 60 | ), 61 | child: quillEditor, 62 | ), 63 | ); 64 | } 65 | 66 | void _toggleEdit() { 67 | setState(() { 68 | _edit = !_edit; 69 | }); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_quill 2 | description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us) 3 | version: 3.7.2 4 | #author: bulletjournal 5 | homepage: https://bulletjournal.us/home/index.html 6 | repository: https://github.com/singerdmx/flutter-quill 7 | 8 | environment: 9 | sdk: ">=2.12.0 <3.0.0" 10 | flutter: ">=2.5.3" 11 | 12 | dependencies: 13 | flutter: 14 | sdk: flutter 15 | collection: ^1.15.0 16 | flutter_colorpicker: ^1.0.3 17 | flutter_keyboard_visibility: ^5.0.0 18 | image_picker: ^0.8.2 19 | photo_view: ^0.13.0 20 | quiver: ^3.0.0 21 | string_validator: ^0.3.0 22 | tuple: ^2.0.0 23 | url_launcher: ^6.0.2 24 | pedantic: ^1.11.0 25 | video_player: ^2.1.10 26 | characters: ^1.1.0 27 | youtube_player_flutter: ^8.0.0 28 | diff_match_patch: ^0.4.1 29 | i18n_extension: ^4.1.3 30 | gallery_saver: ^2.3.2 31 | 32 | dev_dependencies: 33 | flutter_test: 34 | sdk: flutter 35 | 36 | # For information on the generic Dart part of this file, see the 37 | # following page: https://dart.dev/tools/pub/pubspec 38 | # The following section is specific to Flutter. 39 | flutter: null 40 | # To add assets to your package, add an assets section, like this: 41 | # assets: 42 | # - images/a_dot_burr.jpeg 43 | # - images/a_dot_ham.jpeg 44 | # 45 | # For details regarding assets in packages, see 46 | # https://flutter.dev/assets-and-images/#from-packages 47 | # 48 | # An image asset can refer to one or more resolution-specific "variants", see 49 | # https://flutter.dev/assets-and-images/#resolution-aware. 50 | # To add custom fonts to your package, add a fonts section here, 51 | # in this "flutter" section. Each entry in this list should have a 52 | # "family" key with the font family name, and a "fonts" key with a 53 | # list giving the asset and other descriptors for the font. For 54 | # example: 55 | # fonts: 56 | # - family: Schyler 57 | # fonts: 58 | # - asset: fonts/Schyler-Regular.ttf 59 | # - asset: fonts/Schyler-Italic.ttf 60 | # style: italic 61 | # - family: Trajan Pro 62 | # fonts: 63 | # - asset: fonts/TrajanPro.ttf 64 | # - asset: fonts/TrajanPro_Bold.ttf 65 | # weight: 700 66 | # 67 | # For details regarding fonts in packages, see 68 | # https://flutter.dev/custom-fonts/#from-packages 69 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion flutter.compileSdkVersion 30 | 31 | compileOptions { 32 | sourceCompatibility JavaVersion.VERSION_1_8 33 | targetCompatibility JavaVersion.VERSION_1_8 34 | } 35 | 36 | kotlinOptions { 37 | jvmTarget = '1.8' 38 | } 39 | 40 | sourceSets { 41 | main.java.srcDirs += 'src/main/kotlin' 42 | } 43 | 44 | defaultConfig { 45 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 46 | applicationId "com.example.app" 47 | minSdkVersion 21 48 | targetSdkVersion flutter.targetSdkVersion 49 | versionCode flutterVersionCode.toInteger() 50 | versionName flutterVersionName 51 | multiDexEnabled true 52 | } 53 | 54 | buildTypes { 55 | release { 56 | // TODO: Add your own signing config for the release build. 57 | // Signing with the debug keys for now, so `flutter run --release` works. 58 | signingConfig signingConfigs.debug 59 | } 60 | } 61 | } 62 | 63 | flutter { 64 | source '../..' 65 | } 66 | 67 | dependencies { 68 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 69 | implementation 'com.android.support:multidex:1.0.3' 70 | } 71 | -------------------------------------------------------------------------------- /example/windows/runner/flutter_window.cpp: -------------------------------------------------------------------------------- 1 | #include "flutter_window.h" 2 | 3 | #include 4 | 5 | #include "flutter/generated_plugin_registrant.h" 6 | 7 | FlutterWindow::FlutterWindow(RunLoop* run_loop, 8 | const flutter::DartProject& project) 9 | : run_loop_(run_loop), project_(project) {} 10 | 11 | FlutterWindow::~FlutterWindow() {} 12 | 13 | bool FlutterWindow::OnCreate() { 14 | if (!Win32Window::OnCreate()) { 15 | return false; 16 | } 17 | 18 | RECT frame = GetClientArea(); 19 | 20 | // The size here must match the window dimensions to avoid unnecessary surface 21 | // creation / destruction in the startup path. 22 | flutter_controller_ = std::make_unique( 23 | frame.right - frame.left, frame.bottom - frame.top, project_); 24 | // Ensure that basic setup of the controller was successful. 25 | if (!flutter_controller_->engine() || !flutter_controller_->view()) { 26 | return false; 27 | } 28 | RegisterPlugins(flutter_controller_->engine()); 29 | run_loop_->RegisterFlutterInstance(flutter_controller_->engine()); 30 | SetChildContent(flutter_controller_->view()->GetNativeWindow()); 31 | return true; 32 | } 33 | 34 | void FlutterWindow::OnDestroy() { 35 | if (flutter_controller_) { 36 | run_loop_->UnregisterFlutterInstance(flutter_controller_->engine()); 37 | flutter_controller_ = nullptr; 38 | } 39 | 40 | Win32Window::OnDestroy(); 41 | } 42 | 43 | LRESULT 44 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message, 45 | WPARAM const wparam, 46 | LPARAM const lparam) noexcept { 47 | // Give Flutter, including plugins, an opporutunity to handle window messages. 48 | if (flutter_controller_) { 49 | std::optional result = 50 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, 51 | lparam); 52 | if (result) { 53 | return *result; 54 | } 55 | } 56 | 57 | switch (message) { 58 | case WM_FONTCHANGE: 59 | flutter_controller_->engine()->ReloadSystemFonts(); 60 | break; 61 | } 62 | 63 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam); 64 | } 65 | -------------------------------------------------------------------------------- /lib/src/widgets/toolbar/history_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../../flutter_quill.dart'; 4 | 5 | class HistoryButton extends StatefulWidget { 6 | const HistoryButton({ 7 | required this.icon, 8 | required this.controller, 9 | required this.undo, 10 | this.iconSize = kDefaultIconSize, 11 | this.iconTheme, 12 | Key? key, 13 | }) : super(key: key); 14 | 15 | final IconData icon; 16 | final double iconSize; 17 | final bool undo; 18 | final QuillController controller; 19 | final QuillIconTheme? iconTheme; 20 | 21 | @override 22 | _HistoryButtonState createState() => _HistoryButtonState(); 23 | } 24 | 25 | class _HistoryButtonState extends State { 26 | Color? _iconColor; 27 | late ThemeData theme; 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | theme = Theme.of(context); 32 | _setIconColor(); 33 | 34 | final fillColor = 35 | widget.iconTheme?.iconUnselectedFillColor ?? theme.canvasColor; 36 | widget.controller.changes.listen((event) async { 37 | _setIconColor(); 38 | }); 39 | return QuillIconButton( 40 | highlightElevation: 0, 41 | hoverElevation: 0, 42 | size: widget.iconSize * 1.77, 43 | icon: Icon(widget.icon, size: widget.iconSize, color: _iconColor), 44 | fillColor: fillColor, 45 | onPressed: _changeHistory, 46 | ); 47 | } 48 | 49 | void _setIconColor() { 50 | if (!mounted) return; 51 | 52 | if (widget.undo) { 53 | setState(() { 54 | _iconColor = widget.controller.hasUndo 55 | ? widget.iconTheme?.iconUnselectedColor ?? theme.iconTheme.color 56 | : widget.iconTheme?.disabledIconColor ?? theme.disabledColor; 57 | }); 58 | } else { 59 | setState(() { 60 | _iconColor = widget.controller.hasRedo 61 | ? widget.iconTheme?.iconUnselectedColor ?? theme.iconTheme.color 62 | : widget.iconTheme?.disabledIconColor ?? theme.disabledColor; 63 | }); 64 | } 65 | } 66 | 67 | void _changeHistory() { 68 | if (widget.undo) { 69 | if (widget.controller.hasUndo) { 70 | widget.controller.undo(); 71 | } 72 | } else { 73 | if (widget.controller.hasRedo) { 74 | widget.controller.redo(); 75 | } 76 | } 77 | 78 | _setIconColor(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/src/widgets/embeds/youtube_video_app.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/gestures.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:url_launcher/url_launcher.dart'; 4 | import 'package:youtube_player_flutter/youtube_player_flutter.dart'; 5 | 6 | import '../../../flutter_quill.dart'; 7 | 8 | class YoutubeVideoApp extends StatefulWidget { 9 | const YoutubeVideoApp( 10 | {required this.videoUrl, required this.context, required this.readOnly}); 11 | 12 | final String videoUrl; 13 | final BuildContext context; 14 | final bool readOnly; 15 | 16 | @override 17 | _YoutubeVideoAppState createState() => _YoutubeVideoAppState(); 18 | } 19 | 20 | class _YoutubeVideoAppState extends State { 21 | var _youtubeController; 22 | 23 | @override 24 | void initState() { 25 | super.initState(); 26 | final videoId = YoutubePlayer.convertUrlToId(widget.videoUrl); 27 | if (videoId != null) { 28 | _youtubeController = YoutubePlayerController( 29 | initialVideoId: videoId, 30 | flags: const YoutubePlayerFlags( 31 | autoPlay: false, 32 | ), 33 | ); 34 | } 35 | } 36 | 37 | @override 38 | Widget build(BuildContext context) { 39 | final defaultStyles = DefaultStyles.getInstance(context); 40 | if (_youtubeController == null) { 41 | if (widget.readOnly) { 42 | return RichText( 43 | text: TextSpan( 44 | text: widget.videoUrl, 45 | style: defaultStyles.link, 46 | recognizer: TapGestureRecognizer() 47 | ..onTap = () => launch(widget.videoUrl)), 48 | ); 49 | } 50 | 51 | return RichText( 52 | text: TextSpan(text: widget.videoUrl, style: defaultStyles.link)); 53 | } 54 | 55 | return Container( 56 | height: 300, 57 | child: YoutubePlayerBuilder( 58 | player: YoutubePlayer( 59 | controller: _youtubeController, 60 | showVideoProgressIndicator: true, 61 | ), 62 | builder: (context, player) { 63 | return Column( 64 | children: [ 65 | // some widgets 66 | player, 67 | //some other widgets 68 | ], 69 | ); 70 | }, 71 | ), 72 | ); 73 | } 74 | 75 | @override 76 | void dispose() { 77 | super.dispose(); 78 | _youtubeController.dispose(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /example/windows/runner/run_loop.cpp: -------------------------------------------------------------------------------- 1 | #include "run_loop.h" 2 | 3 | #include 4 | 5 | #include 6 | 7 | RunLoop::RunLoop() {} 8 | 9 | RunLoop::~RunLoop() {} 10 | 11 | void RunLoop::Run() { 12 | bool keep_running = true; 13 | TimePoint next_flutter_event_time = TimePoint::clock::now(); 14 | while (keep_running) { 15 | std::chrono::nanoseconds wait_duration = 16 | std::max(std::chrono::nanoseconds(0), 17 | next_flutter_event_time - TimePoint::clock::now()); 18 | ::MsgWaitForMultipleObjects( 19 | 0, nullptr, FALSE, static_cast(wait_duration.count() / 1000), 20 | QS_ALLINPUT); 21 | bool processed_events = false; 22 | MSG message; 23 | // All pending Windows messages must be processed; MsgWaitForMultipleObjects 24 | // won't return again for items left in the queue after PeekMessage. 25 | while (::PeekMessage(&message, nullptr, 0, 0, PM_REMOVE)) { 26 | processed_events = true; 27 | if (message.message == WM_QUIT) { 28 | keep_running = false; 29 | break; 30 | } 31 | ::TranslateMessage(&message); 32 | ::DispatchMessage(&message); 33 | // Allow Flutter to process messages each time a Windows message is 34 | // processed, to prevent starvation. 35 | next_flutter_event_time = 36 | std::min(next_flutter_event_time, ProcessFlutterMessages()); 37 | } 38 | // If the PeekMessage loop didn't run, process Flutter messages. 39 | if (!processed_events) { 40 | next_flutter_event_time = 41 | std::min(next_flutter_event_time, ProcessFlutterMessages()); 42 | } 43 | } 44 | } 45 | 46 | void RunLoop::RegisterFlutterInstance( 47 | flutter::FlutterEngine* flutter_instance) { 48 | flutter_instances_.insert(flutter_instance); 49 | } 50 | 51 | void RunLoop::UnregisterFlutterInstance( 52 | flutter::FlutterEngine* flutter_instance) { 53 | flutter_instances_.erase(flutter_instance); 54 | } 55 | 56 | RunLoop::TimePoint RunLoop::ProcessFlutterMessages() { 57 | TimePoint next_event_time = TimePoint::max(); 58 | for (auto instance : flutter_instances_) { 59 | std::chrono::nanoseconds wait_duration = instance->ProcessMessages(); 60 | if (wait_duration != std::chrono::nanoseconds::max()) { 61 | next_event_time = 62 | std::min(next_event_time, TimePoint::clock::now() + wait_duration); 63 | } 64 | } 65 | return next_event_time; 66 | } 67 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /lib/src/widgets/style_widgets/checkbox_point.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CheckboxPoint extends StatefulWidget { 4 | const CheckboxPoint({ 5 | required this.size, 6 | required this.value, 7 | required this.enabled, 8 | required this.onChanged, 9 | this.uiBuilder, 10 | Key? key, 11 | }) : super(key: key); 12 | 13 | final double size; 14 | final bool value; 15 | final bool enabled; 16 | final ValueChanged onChanged; 17 | final QuillCheckboxBuilder? uiBuilder; 18 | 19 | @override 20 | _CheckboxPointState createState() => _CheckboxPointState(); 21 | } 22 | 23 | class _CheckboxPointState extends State { 24 | @override 25 | Widget build(BuildContext context) { 26 | if (widget.uiBuilder != null) { 27 | return widget.uiBuilder!.build( 28 | context: context, 29 | isChecked: widget.value, 30 | onChanged: widget.onChanged, 31 | ); 32 | } 33 | final theme = Theme.of(context); 34 | final fillColor = widget.value 35 | ? (widget.enabled 36 | ? theme.colorScheme.primary 37 | : theme.colorScheme.onSurface.withOpacity(0.5)) 38 | : theme.colorScheme.surface; 39 | final borderColor = widget.value 40 | ? (widget.enabled 41 | ? theme.colorScheme.primary 42 | : theme.colorScheme.onSurface.withOpacity(0)) 43 | : (widget.enabled 44 | ? theme.colorScheme.onSurface.withOpacity(0.5) 45 | : theme.colorScheme.onSurface.withOpacity(0.3)); 46 | return Center( 47 | child: SizedBox( 48 | width: widget.size, 49 | height: widget.size, 50 | child: Material( 51 | color: fillColor, 52 | shape: RoundedRectangleBorder( 53 | side: BorderSide( 54 | color: borderColor, 55 | ), 56 | borderRadius: BorderRadius.circular(2), 57 | ), 58 | child: InkWell( 59 | onTap: 60 | widget.enabled ? () => widget.onChanged(!widget.value) : null, 61 | child: widget.value 62 | ? Icon(Icons.check, 63 | size: widget.size, color: theme.colorScheme.onPrimary) 64 | : null, 65 | ), 66 | ), 67 | ), 68 | ); 69 | } 70 | } 71 | 72 | abstract class QuillCheckboxBuilder { 73 | Widget build({ 74 | required BuildContext context, 75 | required bool isChecked, 76 | required ValueChanged onChanged, 77 | }); 78 | } 79 | -------------------------------------------------------------------------------- /lib/src/models/rules/rule.dart: -------------------------------------------------------------------------------- 1 | import '../documents/attribute.dart'; 2 | import '../documents/document.dart'; 3 | import '../quill_delta.dart'; 4 | import 'delete.dart'; 5 | import 'format.dart'; 6 | import 'insert.dart'; 7 | 8 | enum RuleType { INSERT, DELETE, FORMAT } 9 | 10 | abstract class Rule { 11 | const Rule(); 12 | 13 | Delta? apply(Delta document, int index, 14 | {int? len, Object? data, Attribute? attribute}) { 15 | validateArgs(len, data, attribute); 16 | return applyRule(document, index, 17 | len: len, data: data, attribute: attribute); 18 | } 19 | 20 | void validateArgs(int? len, Object? data, Attribute? attribute); 21 | 22 | /// Applies heuristic rule to an operation on a [document] and returns 23 | /// resulting [Delta]. 24 | Delta? applyRule(Delta document, int index, 25 | {int? len, Object? data, Attribute? attribute}); 26 | 27 | RuleType get type; 28 | } 29 | 30 | class Rules { 31 | Rules(this._rules); 32 | 33 | List _customRules = []; 34 | 35 | final List _rules; 36 | static final Rules _instance = Rules([ 37 | const FormatLinkAtCaretPositionRule(), 38 | const ResolveLineFormatRule(), 39 | const ResolveInlineFormatRule(), 40 | const InsertEmbedsRule(), 41 | const AutoExitBlockRule(), 42 | const PreserveBlockStyleOnInsertRule(), 43 | const PreserveLineStyleOnSplitRule(), 44 | const ResetLineFormatOnNewLineRule(), 45 | const AutoFormatLinksRule(), 46 | const AutoFormatMultipleLinksRule(), 47 | const PreserveInlineStylesRule(), 48 | const CatchAllInsertRule(), 49 | const EnsureEmbedLineRule(), 50 | const PreserveLineStyleOnMergeRule(), 51 | const CatchAllDeleteRule(), 52 | const EnsureLastLineBreakDeleteRule() 53 | ]); 54 | 55 | static Rules getInstance() => _instance; 56 | 57 | void setCustomRules(List customRules) { 58 | _customRules = customRules; 59 | } 60 | 61 | Delta apply(RuleType ruleType, Document document, int index, 62 | {int? len, Object? data, Attribute? attribute}) { 63 | final delta = document.toDelta(); 64 | for (final rule in _customRules + _rules) { 65 | if (rule.type != ruleType) { 66 | continue; 67 | } 68 | try { 69 | final result = rule.apply(delta, index, 70 | len: len, data: data, attribute: attribute); 71 | if (result != null) { 72 | return result..trim(); 73 | } 74 | } catch (e) { 75 | rethrow; 76 | } 77 | } 78 | throw 'Apply rules failed'; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/src/utils/delta.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as math; 2 | 3 | import '../models/quill_delta.dart'; 4 | 5 | // Diff between two texts - old text and new text 6 | class Diff { 7 | Diff(this.start, this.deleted, this.inserted); 8 | 9 | // Start index in old text at which changes begin. 10 | final int start; 11 | 12 | /// The deleted text 13 | final String deleted; 14 | 15 | // The inserted text 16 | final String inserted; 17 | 18 | @override 19 | String toString() { 20 | return 'Diff[$start, "$deleted", "$inserted"]'; 21 | } 22 | } 23 | 24 | /* Get diff operation between old text and new text */ 25 | Diff getDiff(String oldText, String newText, int cursorPosition) { 26 | var end = oldText.length; 27 | final delta = newText.length - end; 28 | for (final limit = math.max(0, cursorPosition - delta); 29 | end > limit && oldText[end - 1] == newText[end + delta - 1]; 30 | end--) {} 31 | var start = 0; 32 | for (final startLimit = cursorPosition - math.max(0, delta); 33 | start < startLimit && oldText[start] == newText[start]; 34 | start++) {} 35 | final deleted = (start >= end) ? '' : oldText.substring(start, end); 36 | final inserted = newText.substring(start, end + delta); 37 | return Diff(start, deleted, inserted); 38 | } 39 | 40 | int getPositionDelta(Delta user, Delta actual) { 41 | if (actual.isEmpty) { 42 | return 0; 43 | } 44 | 45 | final userItr = DeltaIterator(user); 46 | final actualItr = DeltaIterator(actual); 47 | var diff = 0; 48 | while (userItr.hasNext || actualItr.hasNext) { 49 | final length = math.min(userItr.peekLength(), actualItr.peekLength()); 50 | final userOperation = userItr.next(length); 51 | final actualOperation = actualItr.next(length); 52 | if (userOperation.length != actualOperation.length) { 53 | throw 'userOp ${userOperation.length} does not match actualOp ' 54 | '${actualOperation.length}'; 55 | } 56 | if (userOperation.key == actualOperation.key) { 57 | continue; 58 | } else if (userOperation.isInsert && actualOperation.isRetain) { 59 | diff -= userOperation.length!; 60 | } else if (userOperation.isDelete && actualOperation.isRetain) { 61 | diff += userOperation.length!; 62 | } else if (userOperation.isRetain && actualOperation.isInsert) { 63 | String? operationTxt = ''; 64 | if (actualOperation.data is String) { 65 | operationTxt = actualOperation.data as String?; 66 | } 67 | if (operationTxt!.startsWith('\n')) { 68 | continue; 69 | } 70 | diff += actualOperation.length!; 71 | } 72 | } 73 | return diff; 74 | } 75 | -------------------------------------------------------------------------------- /lib/src/widgets/keyboard_listener.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | 4 | class QuillPressedKeys extends ChangeNotifier { 5 | static QuillPressedKeys of(BuildContext context) { 6 | final widget = 7 | context.dependOnInheritedWidgetOfExactType<_QuillPressedKeysAccess>(); 8 | return widget!.pressedKeys; 9 | } 10 | 11 | bool _metaPressed = false; 12 | bool _controlPressed = false; 13 | 14 | /// Whether meta key is currently pressed. 15 | bool get metaPressed => _metaPressed; 16 | 17 | /// Whether control key is currently pressed. 18 | bool get controlPressed => _controlPressed; 19 | 20 | void _updatePressedKeys(Set pressedKeys) { 21 | final meta = pressedKeys.contains(LogicalKeyboardKey.metaLeft) || 22 | pressedKeys.contains(LogicalKeyboardKey.metaRight); 23 | final control = pressedKeys.contains(LogicalKeyboardKey.controlLeft) || 24 | pressedKeys.contains(LogicalKeyboardKey.controlRight); 25 | if (_metaPressed != meta || _controlPressed != control) { 26 | _metaPressed = meta; 27 | _controlPressed = control; 28 | notifyListeners(); 29 | } 30 | } 31 | } 32 | 33 | class QuillKeyboardListener extends StatefulWidget { 34 | const QuillKeyboardListener({required this.child, Key? key}) 35 | : super(key: key); 36 | 37 | final Widget child; 38 | 39 | @override 40 | QuillKeyboardListenerState createState() => QuillKeyboardListenerState(); 41 | } 42 | 43 | class QuillKeyboardListenerState extends State { 44 | final QuillPressedKeys _pressedKeys = QuillPressedKeys(); 45 | 46 | bool _keyEvent(KeyEvent event) { 47 | _pressedKeys 48 | ._updatePressedKeys(HardwareKeyboard.instance.logicalKeysPressed); 49 | return false; 50 | } 51 | 52 | @override 53 | void initState() { 54 | super.initState(); 55 | HardwareKeyboard.instance.addHandler(_keyEvent); 56 | _pressedKeys 57 | ._updatePressedKeys(HardwareKeyboard.instance.logicalKeysPressed); 58 | } 59 | 60 | @override 61 | void dispose() { 62 | HardwareKeyboard.instance.removeHandler(_keyEvent); 63 | _pressedKeys.dispose(); 64 | super.dispose(); 65 | } 66 | 67 | @override 68 | Widget build(BuildContext context) { 69 | return _QuillPressedKeysAccess( 70 | pressedKeys: _pressedKeys, 71 | child: widget.child, 72 | ); 73 | } 74 | } 75 | 76 | class _QuillPressedKeysAccess extends InheritedWidget { 77 | const _QuillPressedKeysAccess({ 78 | required this.pressedKeys, 79 | required Widget child, 80 | Key? key, 81 | }) : super(key: key, child: child); 82 | 83 | final QuillPressedKeys pressedKeys; 84 | 85 | @override 86 | bool updateShouldNotify(covariant _QuillPressedKeysAccess oldWidget) { 87 | return oldWidget.pressedKeys != pressedKeys; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /lib/src/widgets/toolbar/toggle_check_list_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../models/documents/attribute.dart'; 4 | import '../../models/documents/style.dart'; 5 | import '../../models/themes/quill_icon_theme.dart'; 6 | import '../controller.dart'; 7 | import '../toolbar.dart'; 8 | 9 | class ToggleCheckListButton extends StatefulWidget { 10 | const ToggleCheckListButton({ 11 | required this.icon, 12 | required this.controller, 13 | required this.attribute, 14 | this.iconSize = kDefaultIconSize, 15 | this.fillColor, 16 | this.childBuilder = defaultToggleStyleButtonBuilder, 17 | this.iconTheme, 18 | Key? key, 19 | }) : super(key: key); 20 | 21 | final IconData icon; 22 | final double iconSize; 23 | 24 | final Color? fillColor; 25 | 26 | final QuillController controller; 27 | 28 | final ToggleStyleButtonBuilder childBuilder; 29 | 30 | final Attribute attribute; 31 | 32 | final QuillIconTheme? iconTheme; 33 | 34 | @override 35 | _ToggleCheckListButtonState createState() => _ToggleCheckListButtonState(); 36 | } 37 | 38 | class _ToggleCheckListButtonState extends State { 39 | bool? _isToggled; 40 | 41 | Style get _selectionStyle => widget.controller.getSelectionStyle(); 42 | 43 | void _didChangeEditingValue() { 44 | setState(() { 45 | _isToggled = 46 | _getIsToggled(widget.controller.getSelectionStyle().attributes); 47 | }); 48 | } 49 | 50 | @override 51 | void initState() { 52 | super.initState(); 53 | _isToggled = _getIsToggled(_selectionStyle.attributes); 54 | widget.controller.addListener(_didChangeEditingValue); 55 | } 56 | 57 | bool _getIsToggled(Map attrs) { 58 | if (widget.attribute.key == Attribute.list.key) { 59 | final attribute = attrs[widget.attribute.key]; 60 | if (attribute == null) { 61 | return false; 62 | } 63 | return attribute.value == widget.attribute.value || 64 | attribute.value == Attribute.checked.value; 65 | } 66 | return attrs.containsKey(widget.attribute.key); 67 | } 68 | 69 | @override 70 | void didUpdateWidget(covariant ToggleCheckListButton oldWidget) { 71 | super.didUpdateWidget(oldWidget); 72 | if (oldWidget.controller != widget.controller) { 73 | oldWidget.controller.removeListener(_didChangeEditingValue); 74 | widget.controller.addListener(_didChangeEditingValue); 75 | _isToggled = _getIsToggled(_selectionStyle.attributes); 76 | } 77 | } 78 | 79 | @override 80 | void dispose() { 81 | widget.controller.removeListener(_didChangeEditingValue); 82 | super.dispose(); 83 | } 84 | 85 | @override 86 | Widget build(BuildContext context) { 87 | return widget.childBuilder( 88 | context, 89 | Attribute.unchecked, 90 | widget.icon, 91 | widget.fillColor, 92 | _isToggled, 93 | _toggleAttribute, 94 | widget.iconSize, 95 | widget.iconTheme, 96 | ); 97 | } 98 | 99 | void _toggleAttribute() { 100 | widget.controller.formatSelection(_isToggled! 101 | ? Attribute.clone(Attribute.unchecked, null) 102 | : Attribute.unchecked); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /example/linux/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 4 | 5 | # Configuration provided via flutter tool. 6 | include(${EPHEMERAL_DIR}/generated_config.cmake) 7 | 8 | # TODO: Move the rest of this into files in ephemeral. See 9 | # https://github.com/flutter/flutter/issues/57146. 10 | 11 | # Serves the same purpose as list(TRANSFORM ... PREPEND ...), 12 | # which isn't available in 3.10. 13 | function(list_prepend LIST_NAME PREFIX) 14 | set(NEW_LIST "") 15 | foreach(element ${${LIST_NAME}}) 16 | list(APPEND NEW_LIST "${PREFIX}${element}") 17 | endforeach(element) 18 | set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) 19 | endfunction() 20 | 21 | # === Flutter Library === 22 | # System-level dependencies. 23 | find_package(PkgConfig REQUIRED) 24 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) 25 | pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) 26 | pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) 27 | pkg_check_modules(BLKID REQUIRED IMPORTED_TARGET blkid) 28 | pkg_check_modules(LZMA REQUIRED IMPORTED_TARGET liblzma) 29 | 30 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") 31 | 32 | # Published to parent scope for install step. 33 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 34 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 35 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 36 | set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) 37 | 38 | list(APPEND FLUTTER_LIBRARY_HEADERS 39 | "fl_basic_message_channel.h" 40 | "fl_binary_codec.h" 41 | "fl_binary_messenger.h" 42 | "fl_dart_project.h" 43 | "fl_engine.h" 44 | "fl_json_message_codec.h" 45 | "fl_json_method_codec.h" 46 | "fl_message_codec.h" 47 | "fl_method_call.h" 48 | "fl_method_channel.h" 49 | "fl_method_codec.h" 50 | "fl_method_response.h" 51 | "fl_plugin_registrar.h" 52 | "fl_plugin_registry.h" 53 | "fl_standard_message_codec.h" 54 | "fl_standard_method_codec.h" 55 | "fl_string_codec.h" 56 | "fl_value.h" 57 | "fl_view.h" 58 | "flutter_linux.h" 59 | ) 60 | list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") 61 | add_library(flutter INTERFACE) 62 | target_include_directories(flutter INTERFACE 63 | "${EPHEMERAL_DIR}" 64 | ) 65 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") 66 | target_link_libraries(flutter INTERFACE 67 | PkgConfig::GTK 68 | PkgConfig::GLIB 69 | PkgConfig::GIO 70 | PkgConfig::BLKID 71 | PkgConfig::LZMA 72 | ) 73 | add_dependencies(flutter flutter_assemble) 74 | 75 | # === Flutter tool backend === 76 | # _phony_ is a non-existent file to force this command to run every time, 77 | # since currently there's no way to get a full input/output list from the 78 | # flutter tool. 79 | add_custom_command( 80 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 81 | ${CMAKE_CURRENT_BINARY_DIR}/_phony_ 82 | COMMAND ${CMAKE_COMMAND} -E env 83 | ${FLUTTER_TOOL_ENVIRONMENT} 84 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" 85 | linux-x64 ${CMAKE_BUILD_TYPE} 86 | VERBATIM 87 | ) 88 | add_custom_target(flutter_assemble DEPENDS 89 | "${FLUTTER_LIBRARY}" 90 | ${FLUTTER_LIBRARY_HEADERS} 91 | ) 92 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: app 2 | description: demo app 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 1.0.0+1 19 | 20 | environment: 21 | sdk: '>=2.12.0 <3.0.0' 22 | 23 | dependencies: 24 | flutter: 25 | sdk: flutter 26 | universal_html: ^2.0.7 27 | 28 | # The following adds the Cupertino Icons font to your application. 29 | # Use with the CupertinoIcons class for iOS style icons. 30 | cupertino_icons: ^1.0.2 31 | path_provider: ^2.0.1 32 | filesystem_picker: ^2.0.0-nullsafety.0 33 | file_picker: ^3.0.2+2 34 | flutter_quill: 35 | path: ../ 36 | 37 | dev_dependencies: 38 | flutter_test: 39 | sdk: flutter 40 | 41 | # For information on the generic Dart part of this file, see the 42 | # following page: https://dart.dev/tools/pub/pubspec 43 | 44 | # The following section is specific to Flutter. 45 | flutter: 46 | 47 | # The following line ensures that the Material Icons font is 48 | # included with your application, so that you can use the icons in 49 | # the material Icons class. 50 | uses-material-design: true 51 | 52 | # To add assets to your application, add an assets section, like this: 53 | # assets: 54 | # - images/a_dot_burr.jpeg 55 | # - images/a_dot_ham.jpeg 56 | assets: 57 | - assets/ 58 | 59 | 60 | # An image asset can refer to one or more resolution-specific "variants", see 61 | # https://flutter.dev/assets-and-images/#resolution-aware. 62 | 63 | # For details regarding adding assets from package dependencies, see 64 | # https://flutter.dev/assets-and-images/#from-packages 65 | 66 | # To add custom fonts to your application, add a fonts section here, 67 | # in this "flutter" section. Each entry in this list should have a 68 | # "family" key with the font family name, and a "fonts" key with a 69 | # list giving the asset and other descriptors for the font. For 70 | # example: 71 | # fonts: 72 | # - family: Schyler 73 | # fonts: 74 | # - asset: fonts/Schyler-Regular.ttf 75 | # - asset: fonts/Schyler-Italic.ttf 76 | # style: italic 77 | # - family: Trajan Pro 78 | # fonts: 79 | # - asset: fonts/TrajanPro.ttf 80 | # - asset: fonts/TrajanPro_Bold.ttf 81 | # weight: 700 82 | # 83 | # For details regarding fonts from package dependencies, 84 | # see https://flutter.dev/custom-fonts/#from-packages 85 | -------------------------------------------------------------------------------- /lib/src/widgets/toolbar/image_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:image_picker/image_picker.dart'; 3 | 4 | import '../../models/documents/nodes/embeddable.dart'; 5 | import '../../models/themes/quill_dialog_theme.dart'; 6 | import '../../models/themes/quill_icon_theme.dart'; 7 | import '../controller.dart'; 8 | import '../toolbar.dart'; 9 | 10 | class ImageButton extends StatelessWidget { 11 | const ImageButton({ 12 | required this.icon, 13 | required this.controller, 14 | this.iconSize = kDefaultIconSize, 15 | this.onImagePickCallback, 16 | this.fillColor, 17 | this.filePickImpl, 18 | this.webImagePickImpl, 19 | this.mediaPickSettingSelector, 20 | this.iconTheme, 21 | this.dialogTheme, 22 | Key? key, 23 | }) : super(key: key); 24 | 25 | final IconData icon; 26 | final double iconSize; 27 | 28 | final Color? fillColor; 29 | 30 | final QuillController controller; 31 | 32 | final OnImagePickCallback? onImagePickCallback; 33 | 34 | final WebImagePickImpl? webImagePickImpl; 35 | 36 | final FilePickImpl? filePickImpl; 37 | 38 | final MediaPickSettingSelector? mediaPickSettingSelector; 39 | 40 | final QuillIconTheme? iconTheme; 41 | 42 | final QuillDialogTheme? dialogTheme; 43 | 44 | @override 45 | Widget build(BuildContext context) { 46 | final theme = Theme.of(context); 47 | 48 | final iconColor = iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; 49 | final iconFillColor = 50 | iconTheme?.iconUnselectedFillColor ?? (fillColor ?? theme.canvasColor); 51 | 52 | return QuillIconButton( 53 | icon: Icon(icon, size: iconSize, color: iconColor), 54 | highlightElevation: 0, 55 | hoverElevation: 0, 56 | size: iconSize * 1.77, 57 | fillColor: iconFillColor, 58 | onPressed: () => _onPressedHandler(context), 59 | ); 60 | } 61 | 62 | Future _onPressedHandler(BuildContext context) async { 63 | if (onImagePickCallback != null) { 64 | final selector = 65 | mediaPickSettingSelector ?? ImageVideoUtils.selectMediaPickSetting; 66 | final source = await selector(context); 67 | if (source != null) { 68 | if (source == MediaPickSetting.Gallery) { 69 | _pickImage(context); 70 | } else { 71 | _typeLink(context); 72 | } 73 | } 74 | } else { 75 | _typeLink(context); 76 | } 77 | } 78 | 79 | void _pickImage(BuildContext context) => ImageVideoUtils.handleImageButtonTap( 80 | context, 81 | controller, 82 | ImageSource.gallery, 83 | onImagePickCallback!, 84 | filePickImpl: filePickImpl, 85 | webImagePickImpl: webImagePickImpl, 86 | ); 87 | 88 | void _typeLink(BuildContext context) { 89 | showDialog( 90 | context: context, 91 | builder: (_) => LinkDialog(dialogTheme: dialogTheme), 92 | ).then(_linkSubmitted); 93 | } 94 | 95 | void _linkSubmitted(String? value) { 96 | if (value != null && value.isNotEmpty) { 97 | final index = controller.selection.baseOffset; 98 | final length = controller.selection.extentOffset - index; 99 | 100 | controller.replaceText(index, length, BlockEmbed.image(value), null); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /lib/src/widgets/toolbar/video_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:image_picker/image_picker.dart'; 3 | 4 | import '../../models/documents/nodes/embeddable.dart'; 5 | import '../../models/themes/quill_dialog_theme.dart'; 6 | import '../../models/themes/quill_icon_theme.dart'; 7 | import '../controller.dart'; 8 | import '../toolbar.dart'; 9 | 10 | class VideoButton extends StatelessWidget { 11 | const VideoButton({ 12 | required this.icon, 13 | required this.controller, 14 | this.iconSize = kDefaultIconSize, 15 | this.onVideoPickCallback, 16 | this.fillColor, 17 | this.filePickImpl, 18 | this.webVideoPickImpl, 19 | this.mediaPickSettingSelector, 20 | this.iconTheme, 21 | this.dialogTheme, 22 | Key? key, 23 | }) : super(key: key); 24 | 25 | final IconData icon; 26 | final double iconSize; 27 | 28 | final Color? fillColor; 29 | 30 | final QuillController controller; 31 | 32 | final OnVideoPickCallback? onVideoPickCallback; 33 | 34 | final WebVideoPickImpl? webVideoPickImpl; 35 | 36 | final FilePickImpl? filePickImpl; 37 | 38 | final MediaPickSettingSelector? mediaPickSettingSelector; 39 | 40 | final QuillIconTheme? iconTheme; 41 | 42 | final QuillDialogTheme? dialogTheme; 43 | 44 | @override 45 | Widget build(BuildContext context) { 46 | final theme = Theme.of(context); 47 | 48 | final iconColor = iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; 49 | final iconFillColor = 50 | iconTheme?.iconUnselectedFillColor ?? (fillColor ?? theme.canvasColor); 51 | 52 | return QuillIconButton( 53 | icon: Icon(icon, size: iconSize, color: iconColor), 54 | highlightElevation: 0, 55 | hoverElevation: 0, 56 | size: iconSize * 1.77, 57 | fillColor: iconFillColor, 58 | onPressed: () => _onPressedHandler(context), 59 | ); 60 | } 61 | 62 | Future _onPressedHandler(BuildContext context) async { 63 | if (onVideoPickCallback != null) { 64 | final selector = 65 | mediaPickSettingSelector ?? ImageVideoUtils.selectMediaPickSetting; 66 | final source = await selector(context); 67 | if (source != null) { 68 | if (source == MediaPickSetting.Gallery) { 69 | _pickVideo(context); 70 | } else { 71 | _typeLink(context); 72 | } 73 | } 74 | } else { 75 | _typeLink(context); 76 | } 77 | } 78 | 79 | void _pickVideo(BuildContext context) => ImageVideoUtils.handleVideoButtonTap( 80 | context, 81 | controller, 82 | ImageSource.gallery, 83 | onVideoPickCallback!, 84 | filePickImpl: filePickImpl, 85 | webVideoPickImpl: webVideoPickImpl, 86 | ); 87 | 88 | void _typeLink(BuildContext context) { 89 | showDialog( 90 | context: context, 91 | builder: (_) => LinkDialog(dialogTheme: dialogTheme), 92 | ).then(_linkSubmitted); 93 | } 94 | 95 | void _linkSubmitted(String? value) { 96 | if (value != null && value.isNotEmpty) { 97 | final index = controller.selection.baseOffset; 98 | final length = controller.selection.extentOffset - index; 99 | 100 | controller.replaceText(index, length, BlockEmbed.video(value), null); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /lib/src/widgets/toolbar/quill_dropdown_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | @Deprecated('Not being used') 4 | class QuillDropdownButton extends StatefulWidget { 5 | const QuillDropdownButton({ 6 | required this.child, 7 | required this.initialValue, 8 | required this.items, 9 | required this.onSelected, 10 | this.height = 40, 11 | this.fillColor, 12 | this.hoverElevation = 1, 13 | this.highlightElevation = 1, 14 | Key? key, 15 | }) : super(key: key); 16 | 17 | final double height; 18 | final Color? fillColor; 19 | final double hoverElevation; 20 | final double highlightElevation; 21 | final Widget child; 22 | final T initialValue; 23 | final List> items; 24 | final ValueChanged onSelected; 25 | 26 | @override 27 | _QuillDropdownButtonState createState() => _QuillDropdownButtonState(); 28 | } 29 | 30 | // ignore: deprecated_member_use_from_same_package 31 | class _QuillDropdownButtonState extends State> { 32 | @override 33 | Widget build(BuildContext context) { 34 | return ConstrainedBox( 35 | constraints: BoxConstraints.tightFor(height: widget.height), 36 | child: RawMaterialButton( 37 | visualDensity: VisualDensity.compact, 38 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)), 39 | fillColor: widget.fillColor, 40 | elevation: 0, 41 | hoverElevation: widget.hoverElevation, 42 | highlightElevation: widget.hoverElevation, 43 | onPressed: _showMenu, 44 | child: _buildContent(context), 45 | ), 46 | ); 47 | } 48 | 49 | void _showMenu() { 50 | final popupMenuTheme = PopupMenuTheme.of(context); 51 | final button = context.findRenderObject() as RenderBox; 52 | final overlay = 53 | Overlay.of(context)!.context.findRenderObject() as RenderBox; 54 | final position = RelativeRect.fromRect( 55 | Rect.fromPoints( 56 | button.localToGlobal(Offset.zero, ancestor: overlay), 57 | button.localToGlobal(button.size.bottomLeft(Offset.zero), 58 | ancestor: overlay), 59 | ), 60 | Offset.zero & overlay.size, 61 | ); 62 | showMenu( 63 | context: context, 64 | elevation: 4, 65 | // widget.elevation ?? popupMenuTheme.elevation, 66 | initialValue: widget.initialValue, 67 | items: widget.items, 68 | position: position, 69 | shape: popupMenuTheme.shape, 70 | // widget.shape ?? popupMenuTheme.shape, 71 | color: popupMenuTheme.color, // widget.color ?? popupMenuTheme.color, 72 | // captureInheritedThemes: widget.captureInheritedThemes, 73 | ).then((newValue) { 74 | if (!mounted) return null; 75 | if (newValue == null) { 76 | // if (widget.onCanceled != null) widget.onCanceled(); 77 | return null; 78 | } 79 | widget.onSelected(newValue); 80 | }); 81 | } 82 | 83 | Widget _buildContent(BuildContext context) { 84 | return ConstrainedBox( 85 | constraints: const BoxConstraints.tightFor(width: 110), 86 | child: Padding( 87 | padding: const EdgeInsets.symmetric(horizontal: 8), 88 | child: Row( 89 | children: [ 90 | widget.child, 91 | Expanded(child: Container()), 92 | const Icon(Icons.arrow_drop_down, size: 15) 93 | ], 94 | ), 95 | ), 96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /lib/src/widgets/embeds/video_app.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/gestures.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:url_launcher/url_launcher.dart'; 6 | import 'package:video_player/video_player.dart'; 7 | 8 | import '../../../flutter_quill.dart'; 9 | 10 | /// Widget for playing back video 11 | /// Refer to https://github.com/flutter/plugins/tree/master/packages/video_player/video_player 12 | class VideoApp extends StatefulWidget { 13 | const VideoApp( 14 | {required this.videoUrl, required this.context, required this.readOnly}); 15 | 16 | final String videoUrl; 17 | final BuildContext context; 18 | final bool readOnly; 19 | 20 | @override 21 | _VideoAppState createState() => _VideoAppState(); 22 | } 23 | 24 | class _VideoAppState extends State { 25 | late VideoPlayerController _controller; 26 | 27 | @override 28 | void initState() { 29 | super.initState(); 30 | 31 | _controller = widget.videoUrl.startsWith('http') 32 | ? VideoPlayerController.network(widget.videoUrl) 33 | : VideoPlayerController.file(File(widget.videoUrl)) 34 | ..initialize().then((_) { 35 | // Ensure the first frame is shown after the video is initialized, 36 | // even before the play button has been pressed. 37 | setState(() {}); 38 | }).catchError((error) { 39 | setState(() {}); 40 | }); 41 | ; 42 | } 43 | 44 | @override 45 | Widget build(BuildContext context) { 46 | final defaultStyles = DefaultStyles.getInstance(context); 47 | if (_controller.value.hasError) { 48 | if (widget.readOnly) { 49 | return RichText( 50 | text: TextSpan( 51 | text: widget.videoUrl, 52 | style: defaultStyles.link, 53 | recognizer: TapGestureRecognizer() 54 | ..onTap = () => launch(widget.videoUrl)), 55 | ); 56 | } 57 | 58 | return RichText( 59 | text: TextSpan(text: widget.videoUrl, style: defaultStyles.link)); 60 | } else if (!_controller.value.isInitialized) { 61 | return VideoProgressIndicator( 62 | _controller, 63 | allowScrubbing: true, 64 | colors: const VideoProgressColors(playedColor: Colors.blue), 65 | ); 66 | } 67 | 68 | return Container( 69 | height: 300, 70 | child: InkWell( 71 | onTap: () { 72 | setState(() { 73 | _controller.value.isPlaying 74 | ? _controller.pause() 75 | : _controller.play(); 76 | }); 77 | }, 78 | child: Stack(alignment: Alignment.center, children: [ 79 | Center( 80 | child: AspectRatio( 81 | aspectRatio: _controller.value.aspectRatio, 82 | child: VideoPlayer(_controller), 83 | )), 84 | _controller.value.isPlaying 85 | ? const SizedBox.shrink() 86 | : Container( 87 | color: const Color(0xfff5f5f5), 88 | child: const Icon( 89 | Icons.play_arrow, 90 | size: 60, 91 | color: Colors.blueGrey, 92 | )) 93 | ]), 94 | ), 95 | ); 96 | } 97 | 98 | @override 99 | void dispose() { 100 | super.dispose(); 101 | _controller.dispose(); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /lib/src/widgets/embeds/image.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io' as io; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:photo_view/photo_view.dart'; 6 | import 'package:string_validator/string_validator.dart'; 7 | 8 | bool isImageBase64(String imageUrl) { 9 | return !imageUrl.startsWith('http') && isBase64(imageUrl); 10 | } 11 | 12 | Widget imageByUrl(String imageUrl, 13 | {double? width, 14 | double? height, 15 | AlignmentGeometry alignment = Alignment.center}) { 16 | if (isImageBase64(imageUrl)) { 17 | return Image.memory(base64.decode(imageUrl), 18 | width: width, height: height, alignment: alignment); 19 | } 20 | 21 | if (imageUrl.startsWith('http')) { 22 | return Image.network(imageUrl, 23 | width: width, height: height, alignment: alignment); 24 | } 25 | return Image.file(io.File(imageUrl), 26 | width: width, height: height, alignment: alignment); 27 | } 28 | 29 | String standardizeImageUrl(String url) { 30 | if (url.contains('base64')) { 31 | return url.split(',')[1]; 32 | } 33 | return url; 34 | } 35 | 36 | class ImageTapWrapper extends StatelessWidget { 37 | const ImageTapWrapper({ 38 | required this.imageUrl, 39 | }); 40 | 41 | final String imageUrl; 42 | 43 | ImageProvider _imageProviderByUrl(String imageUrl) { 44 | if (imageUrl.startsWith('http')) { 45 | return NetworkImage(imageUrl); 46 | } 47 | 48 | return FileImage(io.File(imageUrl)); 49 | } 50 | 51 | @override 52 | Widget build(BuildContext context) { 53 | return Scaffold( 54 | body: Container( 55 | constraints: BoxConstraints.expand( 56 | height: MediaQuery.of(context).size.height, 57 | ), 58 | child: Stack( 59 | children: [ 60 | PhotoView( 61 | imageProvider: _imageProviderByUrl(imageUrl), 62 | loadingBuilder: (context, event) { 63 | return Container( 64 | color: Colors.black, 65 | child: const Center( 66 | child: CircularProgressIndicator(), 67 | ), 68 | ); 69 | }, 70 | ), 71 | Positioned( 72 | right: 10, 73 | top: MediaQuery.of(context).padding.top + 10.0, 74 | child: InkWell( 75 | onTap: () { 76 | Navigator.pop(context); 77 | }, 78 | child: Stack( 79 | children: [ 80 | Opacity( 81 | opacity: 0.2, 82 | child: Container( 83 | height: 30, 84 | width: 30, 85 | decoration: const BoxDecoration( 86 | shape: BoxShape.circle, 87 | color: Colors.black87, 88 | ), 89 | ), 90 | ), 91 | Positioned( 92 | top: 0, 93 | bottom: 0, 94 | left: 0, 95 | right: 0, 96 | child: 97 | Icon(Icons.close, color: Colors.grey[400], size: 28), 98 | ) 99 | ], 100 | ), 101 | ), 102 | ), 103 | ], 104 | ), 105 | ), 106 | ); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /example/windows/runner/Runner.rc: -------------------------------------------------------------------------------- 1 | // Microsoft Visual C++ generated resource script. 2 | // 3 | #pragma code_page(65001) 4 | #include "resource.h" 5 | 6 | #define APSTUDIO_READONLY_SYMBOLS 7 | ///////////////////////////////////////////////////////////////////////////// 8 | // 9 | // Generated from the TEXTINCLUDE 2 resource. 10 | // 11 | #include "winres.h" 12 | 13 | ///////////////////////////////////////////////////////////////////////////// 14 | #undef APSTUDIO_READONLY_SYMBOLS 15 | 16 | ///////////////////////////////////////////////////////////////////////////// 17 | // English (United States) resources 18 | 19 | #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) 20 | LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US 21 | 22 | #ifdef APSTUDIO_INVOKED 23 | ///////////////////////////////////////////////////////////////////////////// 24 | // 25 | // TEXTINCLUDE 26 | // 27 | 28 | 1 TEXTINCLUDE 29 | BEGIN 30 | "resource.h\0" 31 | END 32 | 33 | 2 TEXTINCLUDE 34 | BEGIN 35 | "#include ""winres.h""\r\n" 36 | "\0" 37 | END 38 | 39 | 3 TEXTINCLUDE 40 | BEGIN 41 | "\r\n" 42 | "\0" 43 | END 44 | 45 | #endif // APSTUDIO_INVOKED 46 | 47 | 48 | ///////////////////////////////////////////////////////////////////////////// 49 | // 50 | // Icon 51 | // 52 | 53 | // Icon with lowest ID value placed first to ensure application icon 54 | // remains consistent on all systems. 55 | IDI_APP_ICON ICON "resources\\app_icon.ico" 56 | 57 | 58 | ///////////////////////////////////////////////////////////////////////////// 59 | // 60 | // Version 61 | // 62 | 63 | #ifdef FLUTTER_BUILD_NUMBER 64 | #define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER 65 | #else 66 | #define VERSION_AS_NUMBER 1,0,0 67 | #endif 68 | 69 | #ifdef FLUTTER_BUILD_NAME 70 | #define VERSION_AS_STRING #FLUTTER_BUILD_NAME 71 | #else 72 | #define VERSION_AS_STRING "1.0.0" 73 | #endif 74 | 75 | VS_VERSION_INFO VERSIONINFO 76 | FILEVERSION VERSION_AS_NUMBER 77 | PRODUCTVERSION VERSION_AS_NUMBER 78 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK 79 | #ifdef _DEBUG 80 | FILEFLAGS VS_FF_DEBUG 81 | #else 82 | FILEFLAGS 0x0L 83 | #endif 84 | FILEOS VOS__WINDOWS32 85 | FILETYPE VFT_APP 86 | FILESUBTYPE 0x0L 87 | BEGIN 88 | BLOCK "StringFileInfo" 89 | BEGIN 90 | BLOCK "040904e4" 91 | BEGIN 92 | VALUE "CompanyName", "com.example" "\0" 93 | VALUE "FileDescription", "A new Flutter project." "\0" 94 | VALUE "FileVersion", VERSION_AS_STRING "\0" 95 | VALUE "InternalName", "app" "\0" 96 | VALUE "LegalCopyright", "Copyright (C) 2021 com.example. All rights reserved." "\0" 97 | VALUE "OriginalFilename", "app.exe" "\0" 98 | VALUE "ProductName", "app" "\0" 99 | VALUE "ProductVersion", VERSION_AS_STRING "\0" 100 | END 101 | END 102 | BLOCK "VarFileInfo" 103 | BEGIN 104 | VALUE "Translation", 0x409, 1252 105 | END 106 | END 107 | 108 | #endif // English (United States) resources 109 | ///////////////////////////////////////////////////////////////////////////// 110 | 111 | 112 | 113 | #ifndef APSTUDIO_INVOKED 114 | ///////////////////////////////////////////////////////////////////////////// 115 | // 116 | // Generated from the TEXTINCLUDE 3 resource. 117 | // 118 | 119 | 120 | ///////////////////////////////////////////////////////////////////////////// 121 | #endif // not APSTUDIO_INVOKED 122 | -------------------------------------------------------------------------------- /lib/src/widgets/style_widgets/number_point.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../models/documents/attribute.dart'; 4 | import '../text_block.dart'; 5 | 6 | class QuillNumberPoint extends StatelessWidget { 7 | const QuillNumberPoint({ 8 | required this.index, 9 | required this.indentLevelCounts, 10 | required this.count, 11 | required this.style, 12 | required this.width, 13 | required this.attrs, 14 | this.withDot = true, 15 | this.padding = 0.0, 16 | Key? key, 17 | }) : super(key: key); 18 | 19 | final int index; 20 | final Map indentLevelCounts; 21 | final int count; 22 | final TextStyle style; 23 | final double width; 24 | final Map attrs; 25 | final bool withDot; 26 | final double padding; 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | var s = index.toString(); 31 | int? level = 0; 32 | if (!attrs.containsKey(Attribute.indent.key) && 33 | !indentLevelCounts.containsKey(1)) { 34 | indentLevelCounts.clear(); 35 | return Container( 36 | alignment: AlignmentDirectional.topEnd, 37 | width: width, 38 | padding: EdgeInsetsDirectional.only(end: padding), 39 | child: Text(withDot ? '$s.' : s, style: style), 40 | ); 41 | } 42 | if (attrs.containsKey(Attribute.indent.key)) { 43 | level = attrs[Attribute.indent.key]!.value; 44 | } else { 45 | // first level but is back from previous indent level 46 | // supposed to be "2." 47 | indentLevelCounts[0] = 1; 48 | } 49 | if (indentLevelCounts.containsKey(level! + 1)) { 50 | // last visited level is done, going up 51 | indentLevelCounts.remove(level + 1); 52 | } 53 | final count = (indentLevelCounts[level] ?? 0) + 1; 54 | indentLevelCounts[level] = count; 55 | 56 | s = count.toString(); 57 | if (level % 3 == 1) { 58 | // a. b. c. d. e. ... 59 | s = _toExcelSheetColumnTitle(count); 60 | } else if (level % 3 == 2) { 61 | // i. ii. iii. ... 62 | s = _intToRoman(count); 63 | } 64 | // level % 3 == 0 goes back to 1. 2. 3. 65 | 66 | return Container( 67 | alignment: AlignmentDirectional.topEnd, 68 | width: width, 69 | padding: EdgeInsetsDirectional.only(end: padding), 70 | child: Text(withDot ? '$s.' : s, style: style), 71 | ); 72 | } 73 | 74 | String _toExcelSheetColumnTitle(int n) { 75 | final result = StringBuffer(); 76 | while (n > 0) { 77 | n--; 78 | result.write(String.fromCharCode((n % 26).floor() + 97)); 79 | n = (n / 26).floor(); 80 | } 81 | 82 | return result.toString().split('').reversed.join(); 83 | } 84 | 85 | String _intToRoman(int input) { 86 | var num = input; 87 | 88 | if (num < 0) { 89 | return ''; 90 | } else if (num == 0) { 91 | return 'nulla'; 92 | } 93 | 94 | final builder = StringBuffer(); 95 | for (var a = 0; a < arabianRomanNumbers.length; a++) { 96 | final times = (num / arabianRomanNumbers[a]) 97 | .truncate(); // equals 1 only when arabianRomanNumbers[a] = num 98 | // executes n times where n is the number of times you have to add 99 | // the current roman number value to reach current num. 100 | builder.write(romanNumbers[a] * times); 101 | num -= times * 102 | arabianRomanNumbers[ 103 | a]; // subtract previous roman number value from num 104 | } 105 | 106 | return builder.toString().toLowerCase(); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 64 | 65 | 71 | 73 | 79 | 80 | 81 | 82 | 84 | 85 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /lib/src/utils/color.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | Color stringToColor(String? s) { 4 | switch (s) { 5 | case 'transparent': 6 | return Colors.transparent; 7 | case 'black': 8 | return Colors.black; 9 | case 'black12': 10 | return Colors.black12; 11 | case 'black26': 12 | return Colors.black26; 13 | case 'black38': 14 | return Colors.black38; 15 | case 'black45': 16 | return Colors.black45; 17 | case 'black54': 18 | return Colors.black54; 19 | case 'black87': 20 | return Colors.black87; 21 | case 'white': 22 | return Colors.white; 23 | case 'white10': 24 | return Colors.white10; 25 | case 'white12': 26 | return Colors.white12; 27 | case 'white24': 28 | return Colors.white24; 29 | case 'white30': 30 | return Colors.white30; 31 | case 'white38': 32 | return Colors.white38; 33 | case 'white54': 34 | return Colors.white54; 35 | case 'white60': 36 | return Colors.white60; 37 | case 'white70': 38 | return Colors.white70; 39 | case 'red': 40 | return Colors.red; 41 | case 'redAccent': 42 | return Colors.redAccent; 43 | case 'amber': 44 | return Colors.amber; 45 | case 'amberAccent': 46 | return Colors.amberAccent; 47 | case 'yellow': 48 | return Colors.yellow; 49 | case 'yellowAccent': 50 | return Colors.yellowAccent; 51 | case 'teal': 52 | return Colors.teal; 53 | case 'tealAccent': 54 | return Colors.tealAccent; 55 | case 'purple': 56 | return Colors.purple; 57 | case 'purpleAccent': 58 | return Colors.purpleAccent; 59 | case 'pink': 60 | return Colors.pink; 61 | case 'pinkAccent': 62 | return Colors.pinkAccent; 63 | case 'orange': 64 | return Colors.orange; 65 | case 'orangeAccent': 66 | return Colors.orangeAccent; 67 | case 'deepOrange': 68 | return Colors.deepOrange; 69 | case 'deepOrangeAccent': 70 | return Colors.deepOrangeAccent; 71 | case 'indigo': 72 | return Colors.indigo; 73 | case 'indigoAccent': 74 | return Colors.indigoAccent; 75 | case 'lime': 76 | return Colors.lime; 77 | case 'limeAccent': 78 | return Colors.limeAccent; 79 | case 'grey': 80 | return Colors.grey; 81 | case 'blueGrey': 82 | return Colors.blueGrey; 83 | case 'green': 84 | return Colors.green; 85 | case 'greenAccent': 86 | return Colors.greenAccent; 87 | case 'lightGreen': 88 | return Colors.lightGreen; 89 | case 'lightGreenAccent': 90 | return Colors.lightGreenAccent; 91 | case 'blue': 92 | return Colors.blue; 93 | case 'blueAccent': 94 | return Colors.blueAccent; 95 | case 'lightBlue': 96 | return Colors.lightBlue; 97 | case 'lightBlueAccent': 98 | return Colors.lightBlueAccent; 99 | case 'cyan': 100 | return Colors.cyan; 101 | case 'cyanAccent': 102 | return Colors.cyanAccent; 103 | case 'brown': 104 | return Colors.brown; 105 | } 106 | 107 | if (s!.startsWith('rgba')) { 108 | s = s.substring(5); // trim left 'rgba(' 109 | s = s.substring(0, s.length - 1); // trim right ')' 110 | final arr = s.split(',').map((e) => e.trim()).toList(); 111 | return Color.fromRGBO(int.parse(arr[0]), int.parse(arr[1]), 112 | int.parse(arr[2]), double.parse(arr[3])); 113 | } 114 | 115 | if (!s.startsWith('#')) { 116 | throw 'Color code not supported'; 117 | } 118 | 119 | var hex = s.replaceFirst('#', ''); 120 | hex = hex.length == 6 ? 'ff$hex' : hex; 121 | final val = int.parse(hex, radix: 16); 122 | return Color(val); 123 | } 124 | -------------------------------------------------------------------------------- /example/windows/runner/win32_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_WIN32_WINDOW_H_ 2 | #define RUNNER_WIN32_WINDOW_H_ 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | // A class abstraction for a high DPI-aware Win32 Window. Intended to be 11 | // inherited from by classes that wish to specialize with custom 12 | // rendering and input handling 13 | class Win32Window { 14 | public: 15 | struct Point { 16 | unsigned int x; 17 | unsigned int y; 18 | Point(unsigned int x, unsigned int y) : x(x), y(y) {} 19 | }; 20 | 21 | struct Size { 22 | unsigned int width; 23 | unsigned int height; 24 | Size(unsigned int width, unsigned int height) 25 | : width(width), height(height) {} 26 | }; 27 | 28 | Win32Window(); 29 | virtual ~Win32Window(); 30 | 31 | // Creates and shows a win32 window with |title| and position and size using 32 | // |origin| and |size|. New windows are created on the default monitor. Window 33 | // sizes are specified to the OS in physical pixels, hence to ensure a 34 | // consistent size to will treat the width height passed in to this function 35 | // as logical pixels and scale to appropriate for the default monitor. Returns 36 | // true if the window was created successfully. 37 | bool CreateAndShow(const std::wstring& title, 38 | const Point& origin, 39 | const Size& size); 40 | 41 | // Release OS resources associated with window. 42 | void Destroy(); 43 | 44 | // Inserts |content| into the window tree. 45 | void SetChildContent(HWND content); 46 | 47 | // Returns the backing Window handle to enable clients to set icon and other 48 | // window properties. Returns nullptr if the window has been destroyed. 49 | HWND GetHandle(); 50 | 51 | // If true, closing this window will quit the application. 52 | void SetQuitOnClose(bool quit_on_close); 53 | 54 | // Return a RECT representing the bounds of the current client area. 55 | RECT GetClientArea(); 56 | 57 | protected: 58 | // Processes and route salient window messages for mouse handling, 59 | // size change and DPI. Delegates handling of these to member overloads that 60 | // inheriting classes can handle. 61 | virtual LRESULT MessageHandler(HWND window, 62 | UINT const message, 63 | WPARAM const wparam, 64 | LPARAM const lparam) noexcept; 65 | 66 | // Called when CreateAndShow is called, allowing subclass window-related 67 | // setup. Subclasses should return false if setup fails. 68 | virtual bool OnCreate(); 69 | 70 | // Called when Destroy is called. 71 | virtual void OnDestroy(); 72 | 73 | private: 74 | friend class WindowClassRegistrar; 75 | 76 | // OS callback called by message pump. Handles the WM_NCCREATE message which 77 | // is passed when the non-client area is being created and enables automatic 78 | // non-client DPI scaling so that the non-client area automatically 79 | // responsponds to changes in DPI. All other messages are handled by 80 | // MessageHandler. 81 | static LRESULT CALLBACK WndProc(HWND const window, 82 | UINT const message, 83 | WPARAM const wparam, 84 | LPARAM const lparam) noexcept; 85 | 86 | // Retrieves a class instance pointer for |window| 87 | static Win32Window* GetThisFromHandle(HWND const window) noexcept; 88 | 89 | bool quit_on_close_ = false; 90 | 91 | // window handle for top level window. 92 | HWND window_handle_ = nullptr; 93 | 94 | // window handle for hosted content. 95 | HWND child_content_ = nullptr; 96 | }; 97 | 98 | #endif // RUNNER_WIN32_WINDOW_H_ 99 | -------------------------------------------------------------------------------- /example/windows/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15) 2 | project(app LANGUAGES CXX) 3 | 4 | set(BINARY_NAME "app") 5 | 6 | cmake_policy(SET CMP0063 NEW) 7 | 8 | set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") 9 | 10 | # Configure build options. 11 | get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) 12 | if(IS_MULTICONFIG) 13 | set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" 14 | CACHE STRING "" FORCE) 15 | else() 16 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 17 | set(CMAKE_BUILD_TYPE "Debug" CACHE 18 | STRING "Flutter build mode" FORCE) 19 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 20 | "Debug" "Profile" "Release") 21 | endif() 22 | endif() 23 | 24 | set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") 25 | set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") 26 | set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") 27 | set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") 28 | 29 | # Use Unicode for all projects. 30 | add_definitions(-DUNICODE -D_UNICODE) 31 | 32 | # Compilation settings that should be applied to most targets. 33 | function(APPLY_STANDARD_SETTINGS TARGET) 34 | target_compile_features(${TARGET} PUBLIC cxx_std_17) 35 | target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") 36 | target_compile_options(${TARGET} PRIVATE /EHsc) 37 | target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") 38 | target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") 39 | endfunction() 40 | 41 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") 42 | 43 | # Flutter library and tool build rules. 44 | add_subdirectory(${FLUTTER_MANAGED_DIR}) 45 | 46 | # Application build 47 | add_subdirectory("runner") 48 | 49 | # Generated plugin build rules, which manage building the plugins and adding 50 | # them to the application. 51 | include(flutter/generated_plugins.cmake) 52 | 53 | 54 | # === Installation === 55 | # Support files are copied into place next to the executable, so that it can 56 | # run in place. This is done instead of making a separate bundle (as on Linux) 57 | # so that building and running from within Visual Studio will work. 58 | set(BUILD_BUNDLE_DIR "$") 59 | # Make the "install" step default, as it's required to run. 60 | set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) 61 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 62 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) 63 | endif() 64 | 65 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") 66 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") 67 | 68 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" 69 | COMPONENT Runtime) 70 | 71 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 72 | COMPONENT Runtime) 73 | 74 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 75 | COMPONENT Runtime) 76 | 77 | if(PLUGIN_BUNDLED_LIBRARIES) 78 | install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" 79 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 80 | COMPONENT Runtime) 81 | endif() 82 | 83 | # Fully re-copy the assets directory on each build to avoid having stale files 84 | # from a previous install. 85 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets") 86 | install(CODE " 87 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") 88 | " COMPONENT Runtime) 89 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" 90 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) 91 | 92 | # Install the AOT library on non-Debug builds only. 93 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 94 | CONFIGURATIONS Profile;Release 95 | COMPONENT Runtime) 96 | -------------------------------------------------------------------------------- /lib/src/models/documents/history.dart: -------------------------------------------------------------------------------- 1 | import 'package:tuple/tuple.dart'; 2 | 3 | import '../quill_delta.dart'; 4 | import 'document.dart'; 5 | 6 | class History { 7 | History({ 8 | this.ignoreChange = false, 9 | this.interval = 400, 10 | this.maxStack = 100, 11 | this.userOnly = false, 12 | this.lastRecorded = 0, 13 | }); 14 | 15 | final HistoryStack stack = HistoryStack.empty(); 16 | 17 | bool get hasUndo => stack.undo.isNotEmpty; 18 | 19 | bool get hasRedo => stack.redo.isNotEmpty; 20 | 21 | /// used for disable redo or undo function 22 | bool ignoreChange; 23 | 24 | int lastRecorded; 25 | 26 | /// Collaborative editing's conditions should be true 27 | final bool userOnly; 28 | 29 | ///max operation count for undo 30 | final int maxStack; 31 | 32 | ///record delay 33 | final int interval; 34 | 35 | void handleDocChange(Tuple3 change) { 36 | if (ignoreChange) return; 37 | if (!userOnly || change.item3 == ChangeSource.LOCAL) { 38 | record(change.item2, change.item1); 39 | } else { 40 | transform(change.item2); 41 | } 42 | } 43 | 44 | void clear() { 45 | stack.clear(); 46 | } 47 | 48 | void record(Delta change, Delta before) { 49 | if (change.isEmpty) return; 50 | stack.redo.clear(); 51 | var undoDelta = change.invert(before); 52 | final timeStamp = DateTime.now().millisecondsSinceEpoch; 53 | 54 | if (lastRecorded + interval > timeStamp && stack.undo.isNotEmpty) { 55 | final lastDelta = stack.undo.removeLast(); 56 | undoDelta = undoDelta.compose(lastDelta); 57 | } else { 58 | lastRecorded = timeStamp; 59 | } 60 | 61 | if (undoDelta.isEmpty) return; 62 | stack.undo.add(undoDelta); 63 | 64 | if (stack.undo.length > maxStack) { 65 | stack.undo.removeAt(0); 66 | } 67 | } 68 | 69 | /// 70 | ///It will override pre local undo delta,replaced by remote change 71 | /// 72 | void transform(Delta delta) { 73 | transformStack(stack.undo, delta); 74 | transformStack(stack.redo, delta); 75 | } 76 | 77 | void transformStack(List stack, Delta delta) { 78 | for (var i = stack.length - 1; i >= 0; i -= 1) { 79 | final oldDelta = stack[i]; 80 | stack[i] = delta.transform(oldDelta, true); 81 | delta = oldDelta.transform(delta, false); 82 | if (stack[i].length == 0) { 83 | stack.removeAt(i); 84 | } 85 | } 86 | } 87 | 88 | Tuple2 _change(Document doc, List source, List dest) { 89 | if (source.isEmpty) { 90 | return const Tuple2(false, 0); 91 | } 92 | final delta = source.removeLast(); 93 | // look for insert or delete 94 | int? len = 0; 95 | final ops = delta.toList(); 96 | for (var i = 0; i < ops.length; i++) { 97 | if (ops[i].key == Operation.insertKey) { 98 | len = ops[i].length; 99 | } else if (ops[i].key == Operation.deleteKey) { 100 | len = ops[i].length! * -1; 101 | } 102 | } 103 | final base = Delta.from(doc.toDelta()); 104 | final inverseDelta = delta.invert(base); 105 | dest.add(inverseDelta); 106 | lastRecorded = 0; 107 | ignoreChange = true; 108 | doc.compose(delta, ChangeSource.LOCAL); 109 | ignoreChange = false; 110 | return Tuple2(true, len); 111 | } 112 | 113 | Tuple2 undo(Document doc) { 114 | return _change(doc, stack.undo, stack.redo); 115 | } 116 | 117 | Tuple2 redo(Document doc) { 118 | return _change(doc, stack.redo, stack.undo); 119 | } 120 | } 121 | 122 | class HistoryStack { 123 | HistoryStack.empty() 124 | : undo = [], 125 | redo = []; 126 | 127 | final List undo; 128 | final List redo; 129 | 130 | void clear() { 131 | undo.clear(); 132 | redo.clear(); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /lib/src/models/documents/nodes/node.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | 3 | import '../../quill_delta.dart'; 4 | import '../attribute.dart'; 5 | import '../style.dart'; 6 | import 'container.dart'; 7 | import 'line.dart'; 8 | 9 | /// An abstract node in a document tree. 10 | /// 11 | /// Represents a segment of a Quill document with specified [offset] 12 | /// and [length]. 13 | /// 14 | /// The [offset] property is relative to [parent]. See also [documentOffset] 15 | /// which provides absolute offset of this node within the document. 16 | /// 17 | /// The current parent node is exposed by the [parent] property. A node is 18 | /// considered [mounted] when the [parent] property is not `null`. 19 | abstract class Node extends LinkedListEntry { 20 | /// Current parent of this node. May be null if this node is not mounted. 21 | Container? parent; 22 | 23 | Style get style => _style; 24 | Style _style = Style(); 25 | 26 | /// Returns `true` if this node is the first node in the [parent] list. 27 | bool get isFirst => list!.first == this; 28 | 29 | /// Returns `true` if this node is the last node in the [parent] list. 30 | bool get isLast => list!.last == this; 31 | 32 | /// Length of this node in characters. 33 | int get length; 34 | 35 | Node clone() => newInstance()..applyStyle(style); 36 | 37 | /// Offset in characters of this node relative to [parent] node. 38 | /// 39 | /// To get offset of this node in the document see [documentOffset]. 40 | int get offset { 41 | var offset = 0; 42 | 43 | if (list == null || isFirst) { 44 | return offset; 45 | } 46 | 47 | var cur = this; 48 | do { 49 | cur = cur.previous!; 50 | offset += cur.length; 51 | } while (!cur.isFirst); 52 | return offset; 53 | } 54 | 55 | /// Offset in characters of this node in the document. 56 | int get documentOffset { 57 | if (parent == null) { 58 | return offset; 59 | } 60 | final parentOffset = (parent is! Root) ? parent!.documentOffset : 0; 61 | return parentOffset + offset; 62 | } 63 | 64 | /// Returns `true` if this node contains character at specified [offset] in 65 | /// the document. 66 | bool containsOffset(int offset) { 67 | final o = documentOffset; 68 | return o <= offset && offset < o + length; 69 | } 70 | 71 | void applyAttribute(Attribute attribute) { 72 | _style = _style.merge(attribute); 73 | } 74 | 75 | void applyStyle(Style value) { 76 | _style = _style.mergeAll(value); 77 | } 78 | 79 | void clearStyle() { 80 | _style = Style(); 81 | } 82 | 83 | @override 84 | void insertBefore(Node entry) { 85 | assert(entry.parent == null && parent != null); 86 | entry.parent = parent; 87 | super.insertBefore(entry); 88 | } 89 | 90 | @override 91 | void insertAfter(Node entry) { 92 | assert(entry.parent == null && parent != null); 93 | entry.parent = parent; 94 | super.insertAfter(entry); 95 | } 96 | 97 | @override 98 | void unlink() { 99 | assert(parent != null); 100 | parent = null; 101 | super.unlink(); 102 | } 103 | 104 | void adjust() {/* no-op */} 105 | 106 | /// abstract methods begin 107 | 108 | Node newInstance(); 109 | 110 | String toPlainText(); 111 | 112 | Delta toDelta(); 113 | 114 | void insert(int index, Object data, Style? style); 115 | 116 | void retain(int index, int? len, Style? style); 117 | 118 | void delete(int index, int? len); 119 | 120 | /// abstract methods end 121 | } 122 | 123 | /// Root node of document tree. 124 | class Root extends Container> { 125 | @override 126 | Node newInstance() => Root(); 127 | 128 | @override 129 | Container get defaultChild => Line(); 130 | 131 | @override 132 | Delta toDelta() => children 133 | .map((child) => child.toDelta()) 134 | .fold(Delta(), (a, b) => a.concat(b)); 135 | } 136 | -------------------------------------------------------------------------------- /lib/src/models/documents/style.dart: -------------------------------------------------------------------------------- 1 | import 'package:collection/collection.dart'; 2 | import 'package:quiver/core.dart'; 3 | 4 | import 'attribute.dart'; 5 | 6 | /* Collection of style attributes */ 7 | class Style { 8 | Style() : _attributes = {}; 9 | 10 | Style.attr(this._attributes); 11 | 12 | final Map _attributes; 13 | 14 | static Style fromJson(Map? attributes) { 15 | if (attributes == null) { 16 | return Style(); 17 | } 18 | 19 | final result = attributes.map((key, dynamic value) { 20 | final attr = Attribute.fromKeyValue(key, value); 21 | return MapEntry( 22 | key, attr ?? Attribute(key, AttributeScope.IGNORE, value)); 23 | }); 24 | return Style.attr(result); 25 | } 26 | 27 | Map? toJson() => _attributes.isEmpty 28 | ? null 29 | : _attributes.map((_, attribute) => 30 | MapEntry(attribute.key, attribute.value)); 31 | 32 | Iterable get keys => _attributes.keys; 33 | 34 | Iterable get values => _attributes.values.sorted( 35 | (a, b) => Attribute.getRegistryOrder(a) - Attribute.getRegistryOrder(b)); 36 | 37 | Map get attributes => _attributes; 38 | 39 | bool get isEmpty => _attributes.isEmpty; 40 | 41 | bool get isNotEmpty => _attributes.isNotEmpty; 42 | 43 | bool get isInline => isNotEmpty && values.every((item) => item.isInline); 44 | 45 | bool get isIgnored => 46 | isNotEmpty && values.every((item) => item.scope == AttributeScope.IGNORE); 47 | 48 | Attribute get single => _attributes.values.single; 49 | 50 | bool containsKey(String key) => _attributes.containsKey(key); 51 | 52 | Attribute? getBlockExceptHeader() { 53 | for (final val in values) { 54 | if (val.isBlockExceptHeader && val.value != null) { 55 | return val; 56 | } 57 | } 58 | for (final val in values) { 59 | if (val.isBlockExceptHeader) { 60 | return val; 61 | } 62 | } 63 | return null; 64 | } 65 | 66 | Map getBlocksExceptHeader() { 67 | final m = {}; 68 | attributes.forEach((key, value) { 69 | if (Attribute.blockKeysExceptHeader.contains(key)) { 70 | m[key] = value; 71 | } 72 | }); 73 | return m; 74 | } 75 | 76 | Style merge(Attribute attribute) { 77 | final merged = Map.from(_attributes); 78 | if (attribute.value == null) { 79 | merged.remove(attribute.key); 80 | } else { 81 | merged[attribute.key] = attribute; 82 | } 83 | return Style.attr(merged); 84 | } 85 | 86 | Style mergeAll(Style other) { 87 | var result = Style.attr(_attributes); 88 | for (final attribute in other.values) { 89 | result = result.merge(attribute); 90 | } 91 | return result; 92 | } 93 | 94 | Style removeAll(Set attributes) { 95 | final merged = Map.from(_attributes); 96 | attributes.map((item) => item.key).forEach(merged.remove); 97 | return Style.attr(merged); 98 | } 99 | 100 | Style put(Attribute attribute) { 101 | final m = Map.from(attributes); 102 | m[attribute.key] = attribute; 103 | return Style.attr(m); 104 | } 105 | 106 | @override 107 | bool operator ==(Object other) { 108 | if (identical(this, other)) { 109 | return true; 110 | } 111 | if (other is! Style) { 112 | return false; 113 | } 114 | final typedOther = other; 115 | const eq = MapEquality(); 116 | return eq.equals(_attributes, typedOther._attributes); 117 | } 118 | 119 | @override 120 | int get hashCode { 121 | final hashes = 122 | _attributes.entries.map((entry) => hash2(entry.key, entry.value)); 123 | return hashObjects(hashes); 124 | } 125 | 126 | @override 127 | String toString() => "{${_attributes.values.join(', ')}}"; 128 | } 129 | -------------------------------------------------------------------------------- /example/windows/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15) 2 | 3 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 4 | 5 | # Configuration provided via flutter tool. 6 | include(${EPHEMERAL_DIR}/generated_config.cmake) 7 | 8 | # TODO: Move the rest of this into files in ephemeral. See 9 | # https://github.com/flutter/flutter/issues/57146. 10 | set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") 11 | 12 | # === Flutter Library === 13 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") 14 | 15 | # Published to parent scope for install step. 16 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 17 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 18 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 19 | set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) 20 | 21 | list(APPEND FLUTTER_LIBRARY_HEADERS 22 | "flutter_export.h" 23 | "flutter_windows.h" 24 | "flutter_messenger.h" 25 | "flutter_plugin_registrar.h" 26 | "flutter_texture_registrar.h" 27 | ) 28 | list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") 29 | add_library(flutter INTERFACE) 30 | target_include_directories(flutter INTERFACE 31 | "${EPHEMERAL_DIR}" 32 | ) 33 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") 34 | add_dependencies(flutter flutter_assemble) 35 | 36 | # === Wrapper === 37 | list(APPEND CPP_WRAPPER_SOURCES_CORE 38 | "core_implementations.cc" 39 | "standard_codec.cc" 40 | ) 41 | list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") 42 | list(APPEND CPP_WRAPPER_SOURCES_PLUGIN 43 | "plugin_registrar.cc" 44 | ) 45 | list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") 46 | list(APPEND CPP_WRAPPER_SOURCES_APP 47 | "flutter_engine.cc" 48 | "flutter_view_controller.cc" 49 | ) 50 | list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") 51 | 52 | # Wrapper sources needed for a plugin. 53 | add_library(flutter_wrapper_plugin STATIC 54 | ${CPP_WRAPPER_SOURCES_CORE} 55 | ${CPP_WRAPPER_SOURCES_PLUGIN} 56 | ) 57 | apply_standard_settings(flutter_wrapper_plugin) 58 | set_target_properties(flutter_wrapper_plugin PROPERTIES 59 | POSITION_INDEPENDENT_CODE ON) 60 | set_target_properties(flutter_wrapper_plugin PROPERTIES 61 | CXX_VISIBILITY_PRESET hidden) 62 | target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) 63 | target_include_directories(flutter_wrapper_plugin PUBLIC 64 | "${WRAPPER_ROOT}/include" 65 | ) 66 | add_dependencies(flutter_wrapper_plugin flutter_assemble) 67 | 68 | # Wrapper sources needed for the runner. 69 | add_library(flutter_wrapper_app STATIC 70 | ${CPP_WRAPPER_SOURCES_CORE} 71 | ${CPP_WRAPPER_SOURCES_APP} 72 | ) 73 | apply_standard_settings(flutter_wrapper_app) 74 | target_link_libraries(flutter_wrapper_app PUBLIC flutter) 75 | target_include_directories(flutter_wrapper_app PUBLIC 76 | "${WRAPPER_ROOT}/include" 77 | ) 78 | add_dependencies(flutter_wrapper_app flutter_assemble) 79 | 80 | # === Flutter tool backend === 81 | # _phony_ is a non-existent file to force this command to run every time, 82 | # since currently there's no way to get a full input/output list from the 83 | # flutter tool. 84 | set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") 85 | set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) 86 | add_custom_command( 87 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 88 | ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} 89 | ${CPP_WRAPPER_SOURCES_APP} 90 | ${PHONY_OUTPUT} 91 | COMMAND ${CMAKE_COMMAND} -E env 92 | ${FLUTTER_TOOL_ENVIRONMENT} 93 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" 94 | windows-x64 $ 95 | VERBATIM 96 | ) 97 | add_custom_target(flutter_assemble DEPENDS 98 | "${FLUTTER_LIBRARY}" 99 | ${FLUTTER_LIBRARY_HEADERS} 100 | ${CPP_WRAPPER_SOURCES_CORE} 101 | ${CPP_WRAPPER_SOURCES_PLUGIN} 102 | ${CPP_WRAPPER_SOURCES_APP} 103 | ) 104 | -------------------------------------------------------------------------------- /example/lib/widgets/demo_scaffold.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:filesystem_picker/filesystem_picker.dart'; 5 | import 'package:flutter/foundation.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter/services.dart'; 8 | import 'package:flutter_quill/flutter_quill.dart' hide Text; 9 | import 'package:path_provider/path_provider.dart'; 10 | 11 | typedef DemoContentBuilder = Widget Function( 12 | BuildContext context, QuillController? controller); 13 | 14 | // Common scaffold for all examples. 15 | class DemoScaffold extends StatefulWidget { 16 | const DemoScaffold({ 17 | required this.documentFilename, 18 | required this.builder, 19 | this.actions, 20 | this.showToolbar = true, 21 | this.floatingActionButton, 22 | Key? key, 23 | }) : super(key: key); 24 | 25 | /// Filename of the document to load into the editor. 26 | final String documentFilename; 27 | final DemoContentBuilder builder; 28 | final List? actions; 29 | final Widget? floatingActionButton; 30 | final bool showToolbar; 31 | 32 | @override 33 | _DemoScaffoldState createState() => _DemoScaffoldState(); 34 | } 35 | 36 | class _DemoScaffoldState extends State { 37 | final _scaffoldKey = GlobalKey(); 38 | QuillController? _controller; 39 | 40 | bool _loading = false; 41 | 42 | @override 43 | void didChangeDependencies() { 44 | super.didChangeDependencies(); 45 | if (_controller == null && !_loading) { 46 | _loading = true; 47 | _loadFromAssets(); 48 | } 49 | } 50 | 51 | @override 52 | void dispose() { 53 | _controller?.dispose(); 54 | super.dispose(); 55 | } 56 | 57 | Future _loadFromAssets() async { 58 | try { 59 | final result = 60 | await rootBundle.loadString('assets/${widget.documentFilename}'); 61 | final doc = Document.fromJson(jsonDecode(result)); 62 | setState(() { 63 | _controller = QuillController( 64 | document: doc, selection: const TextSelection.collapsed(offset: 0)); 65 | _loading = false; 66 | }); 67 | } catch (error) { 68 | final doc = Document()..insert(0, 'Empty asset'); 69 | setState(() { 70 | _controller = QuillController( 71 | document: doc, selection: const TextSelection.collapsed(offset: 0)); 72 | _loading = false; 73 | }); 74 | } 75 | } 76 | 77 | Future openFileSystemPickerForDesktop(BuildContext context) async { 78 | return await FilesystemPicker.open( 79 | context: context, 80 | rootDirectory: await getApplicationDocumentsDirectory(), 81 | fsType: FilesystemType.file, 82 | fileTileSelectMode: FileTileSelectMode.wholeTile, 83 | ); 84 | } 85 | 86 | @override 87 | Widget build(BuildContext context) { 88 | final actions = widget.actions ?? []; 89 | var toolbar = QuillToolbar.basic(controller: _controller!); 90 | if (_isDesktop()) { 91 | toolbar = QuillToolbar.basic( 92 | controller: _controller!, 93 | filePickImpl: openFileSystemPickerForDesktop); 94 | } 95 | return Scaffold( 96 | key: _scaffoldKey, 97 | appBar: AppBar( 98 | elevation: 0, 99 | backgroundColor: Theme.of(context).canvasColor, 100 | centerTitle: false, 101 | titleSpacing: 0, 102 | leading: IconButton( 103 | icon: Icon( 104 | Icons.chevron_left, 105 | color: Colors.grey.shade800, 106 | size: 18, 107 | ), 108 | onPressed: () => Navigator.pop(context), 109 | ), 110 | title: _loading || !widget.showToolbar ? null : toolbar, 111 | actions: actions, 112 | ), 113 | floatingActionButton: widget.floatingActionButton, 114 | body: _loading 115 | ? const Center(child: Text('Loading...')) 116 | : widget.builder(context, _controller), 117 | ); 118 | } 119 | 120 | bool _isDesktop() => !kIsWeb && !Platform.isAndroid && !Platform.isIOS; 121 | } 122 | -------------------------------------------------------------------------------- /lib/src/widgets/toolbar/arrow_indicated_button_list.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | /// Scrollable list with arrow indicators. 6 | /// 7 | /// The arrow indicators are automatically hidden if the list is not 8 | /// scrollable in the direction of the respective arrow. 9 | class ArrowIndicatedButtonList extends StatefulWidget { 10 | const ArrowIndicatedButtonList({required this.buttons, Key? key}) 11 | : super(key: key); 12 | 13 | final List buttons; 14 | 15 | @override 16 | _ArrowIndicatedButtonListState createState() => 17 | _ArrowIndicatedButtonListState(); 18 | } 19 | 20 | class _ArrowIndicatedButtonListState extends State 21 | with WidgetsBindingObserver { 22 | final ScrollController _controller = ScrollController(); 23 | bool _showLeftArrow = false; 24 | bool _showRightArrow = false; 25 | 26 | @override 27 | void initState() { 28 | super.initState(); 29 | _controller.addListener(_handleScroll); 30 | 31 | // Listening to the WidgetsBinding instance is necessary so that we can 32 | // hide the arrows when the window gets a new size and thus the toolbar 33 | // becomes scrollable/unscrollable. 34 | WidgetsBinding.instance!.addObserver(this); 35 | 36 | // Workaround to allow the scroll controller attach to our ListView so that 37 | // we can detect if overflow arrows need to be shown on init. 38 | Timer.run(_handleScroll); 39 | } 40 | 41 | @override 42 | Widget build(BuildContext context) { 43 | return Row( 44 | children: [ 45 | _buildLeftArrow(), 46 | _buildScrollableList(), 47 | _buildRightColor(), 48 | ], 49 | ); 50 | } 51 | 52 | @override 53 | void didChangeMetrics() => _handleScroll(); 54 | 55 | @override 56 | void dispose() { 57 | _controller.dispose(); 58 | WidgetsBinding.instance!.removeObserver(this); 59 | super.dispose(); 60 | } 61 | 62 | void _handleScroll() { 63 | if (!mounted) return; 64 | 65 | setState(() { 66 | _showLeftArrow = 67 | _controller.position.minScrollExtent != _controller.position.pixels; 68 | _showRightArrow = 69 | _controller.position.maxScrollExtent != _controller.position.pixels; 70 | }); 71 | } 72 | 73 | Widget _buildLeftArrow() { 74 | return SizedBox( 75 | width: 8, 76 | child: Transform.translate( 77 | // Move the icon a few pixels to center it 78 | offset: const Offset(-5, 0), 79 | child: _showLeftArrow ? const Icon(Icons.arrow_left, size: 18) : null, 80 | ), 81 | ); 82 | } 83 | 84 | Widget _buildScrollableList() { 85 | return Expanded( 86 | child: ScrollConfiguration( 87 | // Remove the glowing effect, as we already have the arrow indicators 88 | behavior: _NoGlowBehavior(), 89 | // The CustomScrollView is necessary so that the children are not 90 | // stretched to the height of the toolbar, https://bit.ly/3uC3bjI 91 | child: CustomScrollView( 92 | scrollDirection: Axis.horizontal, 93 | controller: _controller, 94 | physics: const ClampingScrollPhysics(), 95 | slivers: [ 96 | SliverFillRemaining( 97 | hasScrollBody: false, 98 | child: Row( 99 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 100 | children: widget.buttons, 101 | ), 102 | ) 103 | ], 104 | ), 105 | ), 106 | ); 107 | } 108 | 109 | Widget _buildRightColor() { 110 | return SizedBox( 111 | width: 8, 112 | child: Transform.translate( 113 | // Move the icon a few pixels to center it 114 | offset: const Offset(-5, 0), 115 | child: _showRightArrow ? const Icon(Icons.arrow_right, size: 18) : null, 116 | ), 117 | ); 118 | } 119 | } 120 | 121 | /// ScrollBehavior without the Material glow effect. 122 | class _NoGlowBehavior extends ScrollBehavior { 123 | @override 124 | Widget buildViewportChrome(BuildContext _, Widget child, AxisDirection __) { 125 | return child; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /example/linux/my_application.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | #include 4 | #ifdef GDK_WINDOWING_X11 5 | #include 6 | #endif 7 | 8 | #include "flutter/generated_plugin_registrant.h" 9 | 10 | struct _MyApplication { 11 | GtkApplication parent_instance; 12 | char** dart_entrypoint_arguments; 13 | }; 14 | 15 | G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) 16 | 17 | // Implements GApplication::activate. 18 | static void my_application_activate(GApplication* application) { 19 | MyApplication* self = MY_APPLICATION(application); 20 | GtkWindow* window = 21 | GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); 22 | 23 | // Use a header bar when running in GNOME as this is the common style used 24 | // by applications and is the setup most users will be using (e.g. Ubuntu 25 | // desktop). 26 | // If running on X and not using GNOME then just use a traditional title bar 27 | // in case the window manager does more exotic layout, e.g. tiling. 28 | // If running on Wayland assume the header bar will work (may need changing 29 | // if future cases occur). 30 | gboolean use_header_bar = TRUE; 31 | #ifdef GDK_WINDOWING_X11 32 | GdkScreen *screen = gtk_window_get_screen(window); 33 | if (GDK_IS_X11_SCREEN(screen)) { 34 | const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); 35 | if (g_strcmp0(wm_name, "GNOME Shell") != 0) { 36 | use_header_bar = FALSE; 37 | } 38 | } 39 | #endif 40 | if (use_header_bar) { 41 | GtkHeaderBar *header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); 42 | gtk_widget_show(GTK_WIDGET(header_bar)); 43 | gtk_header_bar_set_title(header_bar, "app"); 44 | gtk_header_bar_set_show_close_button(header_bar, TRUE); 45 | gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); 46 | } 47 | else { 48 | gtk_window_set_title(window, "app"); 49 | } 50 | 51 | gtk_window_set_default_size(window, 1280, 720); 52 | gtk_widget_show(GTK_WIDGET(window)); 53 | 54 | g_autoptr(FlDartProject) project = fl_dart_project_new(); 55 | fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); 56 | 57 | FlView* view = fl_view_new(project); 58 | gtk_widget_show(GTK_WIDGET(view)); 59 | gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); 60 | 61 | fl_register_plugins(FL_PLUGIN_REGISTRY(view)); 62 | 63 | gtk_widget_grab_focus(GTK_WIDGET(view)); 64 | } 65 | 66 | // Implements GApplication::local_command_line. 67 | static gboolean my_application_local_command_line(GApplication* application, gchar ***arguments, int *exit_status) { 68 | MyApplication* self = MY_APPLICATION(application); 69 | // Strip out the first argument as it is the binary name. 70 | self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); 71 | 72 | g_autoptr(GError) error = nullptr; 73 | if (!g_application_register(application, nullptr, &error)) { 74 | g_warning("Failed to register: %s", error->message); 75 | *exit_status = 1; 76 | return TRUE; 77 | } 78 | 79 | g_application_activate(application); 80 | *exit_status = 0; 81 | 82 | return TRUE; 83 | } 84 | 85 | // Implements GObject::dispose. 86 | static void my_application_dispose(GObject *object) { 87 | MyApplication* self = MY_APPLICATION(object); 88 | g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); 89 | G_OBJECT_CLASS(my_application_parent_class)->dispose(object); 90 | } 91 | 92 | static void my_application_class_init(MyApplicationClass* klass) { 93 | G_APPLICATION_CLASS(klass)->activate = my_application_activate; 94 | G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; 95 | G_OBJECT_CLASS(klass)->dispose = my_application_dispose; 96 | } 97 | 98 | static void my_application_init(MyApplication* self) {} 99 | 100 | MyApplication* my_application_new() { 101 | return MY_APPLICATION(g_object_new(my_application_get_type(), 102 | "application-id", APPLICATION_ID, 103 | nullptr)); 104 | } 105 | -------------------------------------------------------------------------------- /example/linux/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | project(runner LANGUAGES CXX) 3 | 4 | set(BINARY_NAME "app") 5 | set(APPLICATION_ID "com.example.app") 6 | 7 | cmake_policy(SET CMP0063 NEW) 8 | 9 | set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") 10 | 11 | # Configure build options. 12 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 13 | set(CMAKE_BUILD_TYPE "Debug" CACHE 14 | STRING "Flutter build mode" FORCE) 15 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 16 | "Debug" "Profile" "Release") 17 | endif() 18 | 19 | # Compilation settings that should be applied to most targets. 20 | function(APPLY_STANDARD_SETTINGS TARGET) 21 | target_compile_features(${TARGET} PUBLIC cxx_std_14) 22 | target_compile_options(${TARGET} PRIVATE -Wall -Werror) 23 | target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") 24 | target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") 25 | endfunction() 26 | 27 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") 28 | 29 | # Flutter library and tool build rules. 30 | add_subdirectory(${FLUTTER_MANAGED_DIR}) 31 | 32 | # System-level dependencies. 33 | find_package(PkgConfig REQUIRED) 34 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) 35 | 36 | add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") 37 | 38 | # Application build 39 | add_executable(${BINARY_NAME} 40 | "main.cc" 41 | "my_application.cc" 42 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 43 | ) 44 | apply_standard_settings(${BINARY_NAME}) 45 | target_link_libraries(${BINARY_NAME} PRIVATE flutter) 46 | target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) 47 | add_dependencies(${BINARY_NAME} flutter_assemble) 48 | # Only the install-generated bundle's copy of the executable will launch 49 | # correctly, since the resources must in the right relative locations. To avoid 50 | # people trying to run the unbundled copy, put it in a subdirectory instead of 51 | # the default top-level location. 52 | set_target_properties(${BINARY_NAME} 53 | PROPERTIES 54 | RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" 55 | ) 56 | 57 | # Generated plugin build rules, which manage building the plugins and adding 58 | # them to the application. 59 | include(flutter/generated_plugins.cmake) 60 | 61 | 62 | # === Installation === 63 | # By default, "installing" just makes a relocatable bundle in the build 64 | # directory. 65 | set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") 66 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 67 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) 68 | endif() 69 | 70 | # Start with a clean build bundle directory every time. 71 | install(CODE " 72 | file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") 73 | " COMPONENT Runtime) 74 | 75 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") 76 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") 77 | 78 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" 79 | COMPONENT Runtime) 80 | 81 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 82 | COMPONENT Runtime) 83 | 84 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 85 | COMPONENT Runtime) 86 | 87 | if(PLUGIN_BUNDLED_LIBRARIES) 88 | install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" 89 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 90 | COMPONENT Runtime) 91 | endif() 92 | 93 | # Fully re-copy the assets directory on each build to avoid having stale files 94 | # from a previous install. 95 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets") 96 | install(CODE " 97 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") 98 | " COMPONENT Runtime) 99 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" 100 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) 101 | 102 | # Install the AOT library on non-Debug builds only. 103 | if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") 104 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 105 | COMPONENT Runtime) 106 | endif() 107 | -------------------------------------------------------------------------------- /lib/src/widgets/toolbar/camera_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:image_picker/image_picker.dart'; 3 | 4 | import '../../models/themes/quill_icon_theme.dart'; 5 | import '../controller.dart'; 6 | import '../toolbar.dart'; 7 | 8 | class CameraButton extends StatelessWidget { 9 | const CameraButton({ 10 | required this.icon, 11 | required this.controller, 12 | this.iconSize = kDefaultIconSize, 13 | this.fillColor, 14 | this.onImagePickCallback, 15 | this.onVideoPickCallback, 16 | this.filePickImpl, 17 | this.webImagePickImpl, 18 | this.webVideoPickImpl, 19 | this.iconTheme, 20 | Key? key, 21 | }) : super(key: key); 22 | 23 | final IconData icon; 24 | final double iconSize; 25 | 26 | final Color? fillColor; 27 | 28 | final QuillController controller; 29 | 30 | final OnImagePickCallback? onImagePickCallback; 31 | 32 | final OnVideoPickCallback? onVideoPickCallback; 33 | 34 | final WebImagePickImpl? webImagePickImpl; 35 | 36 | final WebVideoPickImpl? webVideoPickImpl; 37 | 38 | final FilePickImpl? filePickImpl; 39 | 40 | final QuillIconTheme? iconTheme; 41 | 42 | @override 43 | Widget build(BuildContext context) { 44 | final theme = Theme.of(context); 45 | 46 | final iconColor = iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; 47 | final iconFillColor = 48 | iconTheme?.iconUnselectedFillColor ?? (fillColor ?? theme.canvasColor); 49 | 50 | return QuillIconButton( 51 | icon: Icon(icon, size: iconSize, color: iconColor), 52 | highlightElevation: 0, 53 | hoverElevation: 0, 54 | size: iconSize * 1.77, 55 | fillColor: iconFillColor, 56 | onPressed: () => _handleCameraButtonTap(context, controller, 57 | onImagePickCallback: onImagePickCallback, 58 | onVideoPickCallback: onVideoPickCallback, 59 | filePickImpl: filePickImpl, 60 | webImagePickImpl: webImagePickImpl), 61 | ); 62 | } 63 | 64 | Future _handleCameraButtonTap( 65 | BuildContext context, QuillController controller, 66 | {OnImagePickCallback? onImagePickCallback, 67 | OnVideoPickCallback? onVideoPickCallback, 68 | FilePickImpl? filePickImpl, 69 | WebImagePickImpl? webImagePickImpl}) async { 70 | if (onImagePickCallback != null && onVideoPickCallback != null) { 71 | // Show dialog to choose Photo or Video 72 | return await showDialog( 73 | context: context, 74 | builder: (context) { 75 | return AlertDialog( 76 | contentPadding: const EdgeInsets.all(0), 77 | backgroundColor: Colors.transparent, 78 | content: Column(mainAxisSize: MainAxisSize.min, children: [ 79 | TextButton.icon( 80 | icon: const Icon(Icons.photo, color: Colors.cyanAccent), 81 | label: const Text('Photo'), 82 | onPressed: () { 83 | ImageVideoUtils.handleImageButtonTap(context, controller, 84 | ImageSource.camera, onImagePickCallback, 85 | filePickImpl: filePickImpl, 86 | webImagePickImpl: webImagePickImpl); 87 | }, 88 | ), 89 | TextButton.icon( 90 | icon: const Icon(Icons.movie_creation, 91 | color: Colors.orangeAccent), 92 | label: const Text('Video'), 93 | onPressed: () { 94 | ImageVideoUtils.handleVideoButtonTap(context, controller, 95 | ImageSource.camera, onVideoPickCallback, 96 | filePickImpl: filePickImpl, 97 | webVideoPickImpl: webVideoPickImpl); 98 | }, 99 | ) 100 | ])); 101 | }); 102 | } 103 | 104 | if (onImagePickCallback != null) { 105 | return ImageVideoUtils.handleImageButtonTap( 106 | context, controller, ImageSource.camera, onImagePickCallback, 107 | filePickImpl: filePickImpl, webImagePickImpl: webImagePickImpl); 108 | } 109 | 110 | assert(onVideoPickCallback != null, 'onVideoPickCallback must not be null'); 111 | return ImageVideoUtils.handleVideoButtonTap( 112 | context, controller, ImageSource.camera, onVideoPickCallback!, 113 | filePickImpl: filePickImpl, webVideoPickImpl: webVideoPickImpl); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /lib/src/widgets/toolbar/select_header_style_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import '../../models/documents/attribute.dart'; 5 | import '../../models/documents/style.dart'; 6 | import '../../models/themes/quill_icon_theme.dart'; 7 | import '../controller.dart'; 8 | import '../toolbar.dart'; 9 | 10 | class SelectHeaderStyleButton extends StatefulWidget { 11 | const SelectHeaderStyleButton({ 12 | required this.controller, 13 | this.iconSize = kDefaultIconSize, 14 | this.iconTheme, 15 | Key? key, 16 | }) : super(key: key); 17 | 18 | final QuillController controller; 19 | final double iconSize; 20 | 21 | final QuillIconTheme? iconTheme; 22 | 23 | @override 24 | _SelectHeaderStyleButtonState createState() => 25 | _SelectHeaderStyleButtonState(); 26 | } 27 | 28 | class _SelectHeaderStyleButtonState extends State { 29 | Attribute? _value; 30 | 31 | Style get _selectionStyle => widget.controller.getSelectionStyle(); 32 | 33 | @override 34 | void initState() { 35 | super.initState(); 36 | setState(() { 37 | _value = 38 | _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header; 39 | }); 40 | widget.controller.addListener(_didChangeEditingValue); 41 | } 42 | 43 | @override 44 | Widget build(BuildContext context) { 45 | final _valueToText = { 46 | Attribute.header: 'N', 47 | Attribute.h1: 'H1', 48 | Attribute.h2: 'H2', 49 | Attribute.h3: 'H3', 50 | }; 51 | 52 | final _valueAttribute = [ 53 | Attribute.header, 54 | Attribute.h1, 55 | Attribute.h2, 56 | Attribute.h3 57 | ]; 58 | final _valueString = ['N', 'H1', 'H2', 'H3']; 59 | 60 | final theme = Theme.of(context); 61 | final style = TextStyle( 62 | fontWeight: FontWeight.w600, 63 | fontSize: widget.iconSize * 0.7, 64 | ); 65 | 66 | return Row( 67 | mainAxisSize: MainAxisSize.min, 68 | children: List.generate(4, (index) { 69 | return Padding( 70 | // ignore: prefer_const_constructors 71 | padding: EdgeInsets.symmetric(horizontal: !kIsWeb ? 1.0 : 5.0), 72 | child: ConstrainedBox( 73 | constraints: BoxConstraints.tightFor( 74 | width: widget.iconSize * kIconButtonFactor, 75 | height: widget.iconSize * kIconButtonFactor, 76 | ), 77 | child: RawMaterialButton( 78 | hoverElevation: 0, 79 | highlightElevation: 0, 80 | elevation: 0, 81 | visualDensity: VisualDensity.compact, 82 | shape: RoundedRectangleBorder( 83 | borderRadius: BorderRadius.circular(2)), 84 | fillColor: _valueToText[_value] == _valueString[index] 85 | ? (widget.iconTheme?.iconSelectedFillColor ?? 86 | theme.toggleableActiveColor) 87 | : (widget.iconTheme?.iconUnselectedFillColor ?? 88 | theme.canvasColor), 89 | onPressed: () => 90 | widget.controller.formatSelection(_valueAttribute[index]), 91 | child: Text( 92 | _valueString[index], 93 | style: style.copyWith( 94 | color: _valueToText[_value] == _valueString[index] 95 | ? (widget.iconTheme?.iconSelectedColor ?? 96 | theme.primaryIconTheme.color) 97 | : (widget.iconTheme?.iconUnselectedColor ?? 98 | theme.iconTheme.color), 99 | ), 100 | ), 101 | ), 102 | ), 103 | ); 104 | }), 105 | ); 106 | } 107 | 108 | void _didChangeEditingValue() { 109 | setState(() { 110 | _value = 111 | _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header; 112 | }); 113 | } 114 | 115 | @override 116 | void didUpdateWidget(covariant SelectHeaderStyleButton oldWidget) { 117 | super.didUpdateWidget(oldWidget); 118 | if (oldWidget.controller != widget.controller) { 119 | oldWidget.controller.removeListener(_didChangeEditingValue); 120 | widget.controller.addListener(_didChangeEditingValue); 121 | _value = 122 | _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header; 123 | } 124 | } 125 | 126 | @override 127 | void dispose() { 128 | widget.controller.removeListener(_didChangeEditingValue); 129 | super.dispose(); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /lib/src/widgets/toolbar/toggle_style_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../models/documents/attribute.dart'; 4 | import '../../models/documents/style.dart'; 5 | import '../../models/themes/quill_icon_theme.dart'; 6 | import '../controller.dart'; 7 | import '../toolbar.dart'; 8 | 9 | typedef ToggleStyleButtonBuilder = Widget Function( 10 | BuildContext context, 11 | Attribute attribute, 12 | IconData icon, 13 | Color? fillColor, 14 | bool? isToggled, 15 | VoidCallback? onPressed, [ 16 | double iconSize, 17 | QuillIconTheme? iconTheme, 18 | ]); 19 | 20 | class ToggleStyleButton extends StatefulWidget { 21 | const ToggleStyleButton({ 22 | required this.attribute, 23 | required this.icon, 24 | required this.controller, 25 | this.iconSize = kDefaultIconSize, 26 | this.fillColor, 27 | this.childBuilder = defaultToggleStyleButtonBuilder, 28 | this.iconTheme, 29 | Key? key, 30 | }) : super(key: key); 31 | 32 | final Attribute attribute; 33 | 34 | final IconData icon; 35 | final double iconSize; 36 | 37 | final Color? fillColor; 38 | 39 | final QuillController controller; 40 | 41 | final ToggleStyleButtonBuilder childBuilder; 42 | 43 | ///Specify an icon theme for the icons in the toolbar 44 | final QuillIconTheme? iconTheme; 45 | 46 | @override 47 | _ToggleStyleButtonState createState() => _ToggleStyleButtonState(); 48 | } 49 | 50 | class _ToggleStyleButtonState extends State { 51 | bool? _isToggled; 52 | 53 | Style get _selectionStyle => widget.controller.getSelectionStyle(); 54 | 55 | @override 56 | void initState() { 57 | super.initState(); 58 | _isToggled = _getIsToggled(_selectionStyle.attributes); 59 | widget.controller.addListener(_didChangeEditingValue); 60 | } 61 | 62 | @override 63 | Widget build(BuildContext context) { 64 | return widget.childBuilder( 65 | context, 66 | widget.attribute, 67 | widget.icon, 68 | widget.fillColor, 69 | _isToggled, 70 | _toggleAttribute, 71 | widget.iconSize, 72 | widget.iconTheme, 73 | ); 74 | } 75 | 76 | @override 77 | void didUpdateWidget(covariant ToggleStyleButton oldWidget) { 78 | super.didUpdateWidget(oldWidget); 79 | if (oldWidget.controller != widget.controller) { 80 | oldWidget.controller.removeListener(_didChangeEditingValue); 81 | widget.controller.addListener(_didChangeEditingValue); 82 | _isToggled = _getIsToggled(_selectionStyle.attributes); 83 | } 84 | } 85 | 86 | @override 87 | void dispose() { 88 | widget.controller.removeListener(_didChangeEditingValue); 89 | super.dispose(); 90 | } 91 | 92 | void _didChangeEditingValue() { 93 | setState(() => _isToggled = _getIsToggled(_selectionStyle.attributes)); 94 | } 95 | 96 | bool _getIsToggled(Map attrs) { 97 | if (widget.attribute.key == Attribute.list.key) { 98 | final attribute = attrs[widget.attribute.key]; 99 | if (attribute == null) { 100 | return false; 101 | } 102 | return attribute.value == widget.attribute.value; 103 | } 104 | return attrs.containsKey(widget.attribute.key); 105 | } 106 | 107 | void _toggleAttribute() { 108 | widget.controller.formatSelection(_isToggled! 109 | ? Attribute.clone(widget.attribute, null) 110 | : widget.attribute); 111 | } 112 | } 113 | 114 | Widget defaultToggleStyleButtonBuilder( 115 | BuildContext context, 116 | Attribute attribute, 117 | IconData icon, 118 | Color? fillColor, 119 | bool? isToggled, 120 | VoidCallback? onPressed, [ 121 | double iconSize = kDefaultIconSize, 122 | QuillIconTheme? iconTheme, 123 | ]) { 124 | final theme = Theme.of(context); 125 | final isEnabled = onPressed != null; 126 | final iconColor = isEnabled 127 | ? isToggled == true 128 | ? (iconTheme?.iconSelectedColor ?? 129 | theme 130 | .primaryIconTheme.color) //You can specify your own icon color 131 | : (iconTheme?.iconUnselectedColor ?? theme.iconTheme.color) 132 | : (iconTheme?.disabledIconColor ?? theme.disabledColor); 133 | final fill = isEnabled 134 | ? isToggled == true 135 | ? (iconTheme?.iconSelectedFillColor ?? 136 | theme.toggleableActiveColor) //Selected icon fill color 137 | : (iconTheme?.iconUnselectedFillColor ?? 138 | theme.canvasColor) //Unselected icon fill color : 139 | : (iconTheme?.disabledIconFillColor ?? 140 | (fillColor ?? theme.canvasColor)); //Disabled icon fill color 141 | return QuillIconButton( 142 | highlightElevation: 0, 143 | hoverElevation: 0, 144 | size: iconSize * kIconButtonFactor, 145 | icon: Icon(icon, size: iconSize, color: iconColor), 146 | fillColor: fill, 147 | onPressed: onPressed, 148 | ); 149 | } 150 | --------------------------------------------------------------------------------