diff --git a/package/example/README.md b/package/example/README.md new file mode 100644 index 000000000..27a38c696 --- /dev/null +++ b/package/example/README.md @@ -0,0 +1,7 @@ +# Examples + +### My Controls + +A library of Flet controls with a testing app. + +[Source](my_controls) diff --git a/package/example/my_controls/.gitignore b/package/example/my_controls/.gitignore new file mode 100644 index 000000000..ac5aa9893 --- /dev/null +++ b/package/example/my_controls/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +build/ diff --git a/package/example/my_controls/.metadata b/package/example/my_controls/.metadata new file mode 100644 index 000000000..07d8623a3 --- /dev/null +++ b/package/example/my_controls/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "2e9cb0aa71a386a91f73f7088d115c0d96654829" + channel: "stable" + +project_type: package diff --git a/package/example/my_controls/CHANGELOG.md b/package/example/my_controls/CHANGELOG.md new file mode 100644 index 000000000..4c8732f5e --- /dev/null +++ b/package/example/my_controls/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +Initial version of the package. diff --git a/package/example/my_controls/LICENSE b/package/example/my_controls/LICENSE new file mode 100644 index 000000000..f49a4e16e --- /dev/null +++ b/package/example/my_controls/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/package/example/my_controls/README.md b/package/example/my_controls/README.md new file mode 100644 index 000000000..c689bd6b7 --- /dev/null +++ b/package/example/my_controls/README.md @@ -0,0 +1,3 @@ +# Flet custom controls example + +TODO \ No newline at end of file diff --git a/package/example/my_controls/analysis_options.yaml b/package/example/my_controls/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/package/example/my_controls/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/package/example/my_controls/example/.gitignore b/package/example/my_controls/example/.gitignore new file mode 100644 index 000000000..29a3a5017 --- /dev/null +++ b/package/example/my_controls/example/.gitignore @@ -0,0 +1,43 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/package/example/my_controls/example/.metadata b/package/example/my_controls/example/.metadata new file mode 100644 index 000000000..ff67a35e8 --- /dev/null +++ b/package/example/my_controls/example/.metadata @@ -0,0 +1,33 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "2e9cb0aa71a386a91f73f7088d115c0d96654829" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829 + base_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829 + - platform: macos + create_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829 + base_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829 + - platform: windows + create_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829 + base_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/package/example/my_controls/example/README.md b/package/example/my_controls/example/README.md new file mode 100644 index 000000000..ababfb5d7 --- /dev/null +++ b/package/example/my_controls/example/README.md @@ -0,0 +1,16 @@ +# my_controls_demo + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/package/example/my_controls/example/analysis_options.yaml b/package/example/my_controls/example/analysis_options.yaml new file mode 100644 index 000000000..0d2902135 --- /dev/null +++ b/package/example/my_controls/example/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/package/example/my_controls/example/lib/main.dart b/package/example/my_controls/example/lib/main.dart new file mode 100644 index 000000000..8877eafdf --- /dev/null +++ b/package/example/my_controls/example/lib/main.dart @@ -0,0 +1,13 @@ +import 'package:flet/flet.dart'; +import 'package:flutter/material.dart'; +import 'package:my_controls/my_controls.dart' as my_controls; + +void main() async { + await setupDesktop(); + + runApp(FletApp( + pageUrl: 'http://localhost:8550', + assetsDir: '', + createControlFactories: [my_controls.createControl], + )); +} diff --git a/package/example/my_controls/example/macos/.gitignore b/package/example/my_controls/example/macos/.gitignore new file mode 100644 index 000000000..746adbb6b --- /dev/null +++ b/package/example/my_controls/example/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/package/example/my_controls/example/macos/Flutter/Flutter-Debug.xcconfig b/package/example/my_controls/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 000000000..4b81f9b2d --- /dev/null +++ b/package/example/my_controls/example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/package/example/my_controls/example/macos/Flutter/Flutter-Release.xcconfig b/package/example/my_controls/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 000000000..5caa9d157 --- /dev/null +++ b/package/example/my_controls/example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/package/example/my_controls/example/macos/Flutter/GeneratedPluginRegistrant.swift b/package/example/my_controls/example/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 000000000..6b7922d0a --- /dev/null +++ b/package/example/my_controls/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,24 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import audioplayers_darwin +import path_provider_foundation +import screen_retriever +import shared_preferences_foundation +import url_launcher_macos +import window_manager +import window_to_front + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) + WindowToFrontPlugin.register(with: registry.registrar(forPlugin: "WindowToFrontPlugin")) +} diff --git a/package/example/my_controls/example/macos/Podfile b/package/example/my_controls/example/macos/Podfile new file mode 100644 index 000000000..c795730db --- /dev/null +++ b/package/example/my_controls/example/macos/Podfile @@ -0,0 +1,43 @@ +platform :osx, '10.14' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/package/example/my_controls/example/macos/Podfile.lock b/package/example/my_controls/example/macos/Podfile.lock new file mode 100644 index 000000000..0c6114e25 --- /dev/null +++ b/package/example/my_controls/example/macos/Podfile.lock @@ -0,0 +1,60 @@ +PODS: + - audioplayers_darwin (0.0.1): + - FlutterMacOS + - FlutterMacOS (1.0.0) + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - screen_retriever (0.0.1): + - FlutterMacOS + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - url_launcher_macos (0.0.1): + - FlutterMacOS + - window_manager (0.2.0): + - FlutterMacOS + - window_to_front (0.0.1): + - FlutterMacOS + +DEPENDENCIES: + - audioplayers_darwin (from `Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/macos`) + - FlutterMacOS (from `Flutter/ephemeral`) + - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) + - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) + - window_to_front (from `Flutter/ephemeral/.symlinks/plugins/window_to_front/macos`) + +EXTERNAL SOURCES: + audioplayers_darwin: + :path: Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/macos + FlutterMacOS: + :path: Flutter/ephemeral + path_provider_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + screen_retriever: + :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos + shared_preferences_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + window_manager: + :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos + window_to_front: + :path: Flutter/ephemeral/.symlinks/plugins/window_to_front/macos + +SPEC CHECKSUMS: + audioplayers_darwin: dcad41de4fbd0099cb3749f7ab3b0cb8f70b810c + FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c + screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 + shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 + url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 + window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 + window_to_front: 4cdc24ddd8461ad1a55fa06286d6a79d8b29e8d8 + +PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 + +COCOAPODS: 1.14.3 diff --git a/package/example/my_controls/example/macos/Runner.xcodeproj/project.pbxproj b/package/example/my_controls/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000..fc9471079 --- /dev/null +++ b/package/example/my_controls/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,791 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 1F436DF7916E8105F477B616 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 562DBBF5F836E5CEFDACC048 /* Pods_Runner.framework */; }; + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 902249D026A61799A7BFBA1A /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 555D41E30EE456DB0F08DCCD /* Pods_RunnerTests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; }; + 33CC10ED2044A3C60003C045 /* my_controls_demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = my_controls_demo.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; }; + 34B725726563F5BC78ABA1A6 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; }; + 555D41E30EE456DB0F08DCCD /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 562DBBF5F836E5CEFDACC048 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; }; + 838963C1989C99CB14E19755 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; }; + A45925ACBE920A75209D7F2B /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; }; + C550727A38D81AB9AA84FABD /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; }; + D0DA0AC5AA357360A1E81CBE /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; }; + F8F87DE9FA7D0C1AB850C561 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 902249D026A61799A7BFBA1A /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 1F436DF7916E8105F477B616 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = "<group>"; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = "<group>"; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 896787364530D557D0CEDDF5 /* Pods */, + ); + sourceTree = "<group>"; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* my_controls_demo.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = "<group>"; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = "<group>"; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = "<group>"; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = "<group>"; + }; + 896787364530D557D0CEDDF5 /* Pods */ = { + isa = PBXGroup; + children = ( + 838963C1989C99CB14E19755 /* Pods-Runner.debug.xcconfig */, + C550727A38D81AB9AA84FABD /* Pods-Runner.release.xcconfig */, + A45925ACBE920A75209D7F2B /* Pods-Runner.profile.xcconfig */, + 34B725726563F5BC78ABA1A6 /* Pods-RunnerTests.debug.xcconfig */, + D0DA0AC5AA357360A1E81CBE /* Pods-RunnerTests.release.xcconfig */, + F8F87DE9FA7D0C1AB850C561 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = "<group>"; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 562DBBF5F836E5CEFDACC048 /* Pods_Runner.framework */, + 555D41E30EE456DB0F08DCCD /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = "<group>"; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + E46C7DC6CC180D9C56FCE75B /* [CP] Check Pods Manifest.lock */, + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + EDA849CD90C5AE7B53C8A7D5 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + C3985B58A3995385B8C6EA62 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* my_controls_demo.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1430; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + C3985B58A3995385B8C6EA62 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + E46C7DC6CC180D9C56FCE75B /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + EDA849CD90C5AE7B53C8A7D5 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = "<group>"; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 34B725726563F5BC78ABA1A6 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.myControlsDemo.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/my_controls_demo.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/my_controls_demo"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0DA0AC5AA357360A1E81CBE /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.myControlsDemo.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/my_controls_demo.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/my_controls_demo"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F8F87DE9FA7D0C1AB850C561 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.myControlsDemo.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/my_controls_demo.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/my_controls_demo"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/package/example/my_controls/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/package/example/my_controls/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/package/example/my_controls/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>IDEDidComputeMac32BitWarning</key> + <true/> +</dict> +</plist> diff --git a/package/example/my_controls/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/package/example/my_controls/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000..163486e0b --- /dev/null +++ b/package/example/my_controls/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "1430" + version = "1.3"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + <BuildActionEntries> + <BuildActionEntry + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "33CC10EC2044A3C60003C045" + BuildableName = "my_controls_demo.app" + BlueprintName = "Runner" + ReferencedContainer = "container:Runner.xcodeproj"> + </BuildableReference> + </BuildActionEntry> + </BuildActionEntries> + </BuildAction> + <TestAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + shouldUseLaunchSchemeArgsEnv = "YES"> + <MacroExpansion> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "33CC10EC2044A3C60003C045" + BuildableName = "my_controls_demo.app" + BlueprintName = "Runner" + ReferencedContainer = "container:Runner.xcodeproj"> + </BuildableReference> + </MacroExpansion> + <Testables> + <TestableReference + skipped = "NO" + parallelizable = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "331C80D4294CF70F00263BE5" + BuildableName = "RunnerTests.xctest" + BlueprintName = "RunnerTests" + ReferencedContainer = "container:Runner.xcodeproj"> + </BuildableReference> + </TestableReference> + </Testables> + </TestAction> + <LaunchAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + launchStyle = "0" + useCustomWorkingDirectory = "NO" + ignoresPersistentStateOnLaunch = "NO" + debugDocumentVersioning = "YES" + debugServiceExtension = "internal" + allowLocationSimulation = "YES"> + <BuildableProductRunnable + runnableDebuggingMode = "0"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "33CC10EC2044A3C60003C045" + BuildableName = "my_controls_demo.app" + BlueprintName = "Runner" + ReferencedContainer = "container:Runner.xcodeproj"> + </BuildableReference> + </BuildableProductRunnable> + </LaunchAction> + <ProfileAction + buildConfiguration = "Profile" + shouldUseLaunchSchemeArgsEnv = "YES" + savedToolIdentifier = "" + useCustomWorkingDirectory = "NO" + debugDocumentVersioning = "YES"> + <BuildableProductRunnable + runnableDebuggingMode = "0"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "33CC10EC2044A3C60003C045" + BuildableName = "my_controls_demo.app" + BlueprintName = "Runner" + ReferencedContainer = "container:Runner.xcodeproj"> + </BuildableReference> + </BuildableProductRunnable> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/package/example/my_controls/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/package/example/my_controls/example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..21a3cc14c --- /dev/null +++ b/package/example/my_controls/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Workspace + version = "1.0"> + <FileRef + location = "group:Runner.xcodeproj"> + </FileRef> + <FileRef + location = "group:Pods/Pods.xcodeproj"> + </FileRef> +</Workspace> diff --git a/package/example/my_controls/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/package/example/my_controls/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/package/example/my_controls/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>IDEDidComputeMac32BitWarning</key> + <true/> +</dict> +</plist> diff --git a/package/example/my_controls/example/macos/Runner/AppDelegate.swift b/package/example/my_controls/example/macos/Runner/AppDelegate.swift new file mode 100644 index 000000000..d53ef6437 --- /dev/null +++ b/package/example/my_controls/example/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..a2ec33f19 --- /dev/null +++ b/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 000000000..82b6f9d9a Binary files /dev/null and b/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 000000000..13b35eba5 Binary files /dev/null and b/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 000000000..0a3f5fa40 Binary files /dev/null and b/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 000000000..bdb57226d Binary files /dev/null and b/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 000000000..f083318e0 Binary files /dev/null and b/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 000000000..326c0e72c Binary files /dev/null and b/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 000000000..2f1632cfd Binary files /dev/null and b/package/example/my_controls/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/package/example/my_controls/example/macos/Runner/Base.lproj/MainMenu.xib b/package/example/my_controls/example/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 000000000..80e867a4e --- /dev/null +++ b/package/example/my_controls/example/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ +<?xml version="1.0" encoding="UTF-8"?> +<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct"> + <dependencies> + <deployment identifier="macosx"/> + <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/> + <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> + </dependencies> + <objects> + <customObject id="-2" userLabel="File's Owner" customClass="NSApplication"> + <connections> + <outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/> + </connections> + </customObject> + <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/> + <customObject id="-3" userLabel="Application" customClass="NSObject"/> + <customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Runner" customModuleProvider="target"> + <connections> + <outlet property="applicationMenu" destination="uQy-DD-JDr" id="XBo-yE-nKs"/> + <outlet property="mainFlutterWindow" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/> + </connections> + </customObject> + <customObject id="YLy-65-1bz" customClass="NSFontManager"/> + <menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6"> + <items> + <menuItem title="APP_NAME" id="1Xt-HY-uBw"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="APP_NAME" systemMenu="apple" id="uQy-DD-JDr"> + <items> + <menuItem title="About APP_NAME" id="5kV-Vb-QxS"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/> + <menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/> + <menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/> + <menuItem title="Services" id="NMo-om-nkz"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/> + </menuItem> + <menuItem isSeparatorItem="YES" id="4je-JR-u6R"/> + <menuItem title="Hide APP_NAME" keyEquivalent="h" id="Olw-nP-bQN"> + <connections> + <action selector="hide:" target="-1" id="PnN-Uc-m68"/> + </connections> + </menuItem> + <menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO"> + <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/> + <connections> + <action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/> + </connections> + </menuItem> + <menuItem title="Show All" id="Kd2-mp-pUS"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/> + <menuItem title="Quit APP_NAME" keyEquivalent="q" id="4sb-4s-VLi"> + <connections> + <action selector="terminate:" target="-1" id="Te7-pn-YzF"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Edit" id="5QF-Oa-p0T"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Edit" id="W48-6f-4Dl"> + <items> + <menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg"> + <connections> + <action selector="undo:" target="-1" id="M6e-cu-g7V"/> + </connections> + </menuItem> + <menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam"> + <connections> + <action selector="redo:" target="-1" id="oIA-Rs-6OD"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/> + <menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG"> + <connections> + <action selector="cut:" target="-1" id="YJe-68-I9s"/> + </connections> + </menuItem> + <menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU"> + <connections> + <action selector="copy:" target="-1" id="G1f-GL-Joy"/> + </connections> + </menuItem> + <menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL"> + <connections> + <action selector="paste:" target="-1" id="UvS-8e-Qdg"/> + </connections> + </menuItem> + <menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk"> + <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/> + <connections> + <action selector="pasteAsPlainText:" target="-1" id="cEh-KX-wJQ"/> + </connections> + </menuItem> + <menuItem title="Delete" id="pa3-QI-u2k"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="delete:" target="-1" id="0Mk-Ml-PaM"/> + </connections> + </menuItem> + <menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m"> + <connections> + <action selector="selectAll:" target="-1" id="VNm-Mi-diN"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/> + <menuItem title="Find" id="4EN-yA-p0u"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Find" id="1b7-l0-nxx"> + <items> + <menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W"> + <connections> + <action selector="performFindPanelAction:" target="-1" id="cD7-Qs-BN4"/> + </connections> + </menuItem> + <menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz"> + <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/> + <connections> + <action selector="performFindPanelAction:" target="-1" id="WD3-Gg-5AJ"/> + </connections> + </menuItem> + <menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye"> + <connections> + <action selector="performFindPanelAction:" target="-1" id="NDo-RZ-v9R"/> + </connections> + </menuItem> + <menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV"> + <connections> + <action selector="performFindPanelAction:" target="-1" id="HOh-sY-3ay"/> + </connections> + </menuItem> + <menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt"> + <connections> + <action selector="performFindPanelAction:" target="-1" id="U76-nv-p5D"/> + </connections> + </menuItem> + <menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd"> + <connections> + <action selector="centerSelectionInVisibleArea:" target="-1" id="IOG-6D-g5B"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Spelling and Grammar" id="Dv1-io-Yv7"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Spelling" id="3IN-sU-3Bg"> + <items> + <menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI"> + <connections> + <action selector="showGuessPanel:" target="-1" id="vFj-Ks-hy3"/> + </connections> + </menuItem> + <menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7"> + <connections> + <action selector="checkSpelling:" target="-1" id="fz7-VC-reM"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="bNw-od-mp5"/> + <menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleContinuousSpellChecking:" target="-1" id="7w6-Qz-0kB"/> + </connections> + </menuItem> + <menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleGrammarChecking:" target="-1" id="muD-Qn-j4w"/> + </connections> + </menuItem> + <menuItem title="Correct Spelling Automatically" id="78Y-hA-62v"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleAutomaticSpellingCorrection:" target="-1" id="2lM-Qi-WAP"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Substitutions" id="9ic-FL-obx"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Substitutions" id="FeM-D8-WVr"> + <items> + <menuItem title="Show Substitutions" id="z6F-FW-3nz"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="orderFrontSubstitutionsPanel:" target="-1" id="oku-mr-iSq"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/> + <menuItem title="Smart Copy/Paste" id="9yt-4B-nSM"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleSmartInsertDelete:" target="-1" id="3IJ-Se-DZD"/> + </connections> + </menuItem> + <menuItem title="Smart Quotes" id="hQb-2v-fYv"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="ptq-xd-QOA"/> + </connections> + </menuItem> + <menuItem title="Smart Dashes" id="rgM-f4-ycn"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleAutomaticDashSubstitution:" target="-1" id="oCt-pO-9gS"/> + </connections> + </menuItem> + <menuItem title="Smart Links" id="cwL-P1-jid"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleAutomaticLinkDetection:" target="-1" id="Gip-E3-Fov"/> + </connections> + </menuItem> + <menuItem title="Data Detectors" id="tRr-pd-1PS"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleAutomaticDataDetection:" target="-1" id="R1I-Nq-Kbl"/> + </connections> + </menuItem> + <menuItem title="Text Replacement" id="HFQ-gK-NFA"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="toggleAutomaticTextReplacement:" target="-1" id="DvP-Fe-Py6"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Transformations" id="2oI-Rn-ZJC"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Transformations" id="c8a-y6-VQd"> + <items> + <menuItem title="Make Upper Case" id="vmV-6d-7jI"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="uppercaseWord:" target="-1" id="sPh-Tk-edu"/> + </connections> + </menuItem> + <menuItem title="Make Lower Case" id="d9M-CD-aMd"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="lowercaseWord:" target="-1" id="iUZ-b5-hil"/> + </connections> + </menuItem> + <menuItem title="Capitalize" id="UEZ-Bs-lqG"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="capitalizeWord:" target="-1" id="26H-TL-nsh"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Speech" id="xrE-MZ-jX0"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Speech" id="3rS-ZA-NoH"> + <items> + <menuItem title="Start Speaking" id="Ynk-f8-cLZ"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="startSpeaking:" target="-1" id="654-Ng-kyl"/> + </connections> + </menuItem> + <menuItem title="Stop Speaking" id="Oyz-dy-DGm"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="stopSpeaking:" target="-1" id="dX8-6p-jy9"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="View" id="H8h-7b-M4v"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="View" id="HyV-fh-RgO"> + <items> + <menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa"> + <modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/> + <connections> + <action selector="toggleFullScreen:" target="-1" id="dU3-MA-1Rq"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Window" id="aUF-d1-5bR"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo"> + <items> + <menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV"> + <connections> + <action selector="performMiniaturize:" target="-1" id="VwT-WD-YPe"/> + </connections> + </menuItem> + <menuItem title="Zoom" id="R4o-n2-Eq4"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="performZoom:" target="-1" id="DIl-cC-cCs"/> + </connections> + </menuItem> + <menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/> + <menuItem title="Bring All to Front" id="LE2-aR-0XJ"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/> + </connections> + </menuItem> + </items> + </menu> + </menuItem> + <menuItem title="Help" id="EPT-qC-fAb"> + <modifierMask key="keyEquivalentModifierMask"/> + <menu key="submenu" title="Help" systemMenu="help" id="rJ0-wn-3NY"/> + </menuItem> + </items> + <point key="canvasLocation" x="142" y="-258"/> + </menu> + <window title="APP_NAME" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="MainFlutterWindow" customModule="Runner" customModuleProvider="target"> + <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/> + <rect key="contentRect" x="335" y="390" width="800" height="600"/> + <rect key="screenRect" x="0.0" y="0.0" width="2560" height="1577"/> + <view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ"> + <rect key="frame" x="0.0" y="0.0" width="800" height="600"/> + <autoresizingMask key="autoresizingMask"/> + </view> + </window> + </objects> +</document> diff --git a/package/example/my_controls/example/macos/Runner/Configs/AppInfo.xcconfig b/package/example/my_controls/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 000000000..c49c27972 --- /dev/null +++ b/package/example/my_controls/example/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = my_controls_demo + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.myControlsDemo + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2024 com.example. All rights reserved. diff --git a/package/example/my_controls/example/macos/Runner/Configs/Debug.xcconfig b/package/example/my_controls/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 000000000..36b0fd946 --- /dev/null +++ b/package/example/my_controls/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/package/example/my_controls/example/macos/Runner/Configs/Release.xcconfig b/package/example/my_controls/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 000000000..dff4f4956 --- /dev/null +++ b/package/example/my_controls/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/package/example/my_controls/example/macos/Runner/Configs/Warnings.xcconfig b/package/example/my_controls/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 000000000..42bcbf478 --- /dev/null +++ b/package/example/my_controls/example/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/package/example/my_controls/example/macos/Runner/DebugProfile.entitlements b/package/example/my_controls/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 000000000..9f56413f3 --- /dev/null +++ b/package/example/my_controls/example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>com.apple.security.app-sandbox</key> + <false/> + <key>com.apple.security.cs.allow-jit</key> + <true/> + <key>com.apple.security.network.server</key> + <true/> +</dict> +</plist> diff --git a/package/example/my_controls/example/macos/Runner/Info.plist b/package/example/my_controls/example/macos/Runner/Info.plist new file mode 100644 index 000000000..4789daa6a --- /dev/null +++ b/package/example/my_controls/example/macos/Runner/Info.plist @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>$(DEVELOPMENT_LANGUAGE)</string> + <key>CFBundleExecutable</key> + <string>$(EXECUTABLE_NAME)</string> + <key>CFBundleIconFile</key> + <string></string> + <key>CFBundleIdentifier</key> + <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>$(PRODUCT_NAME)</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleShortVersionString</key> + <string>$(FLUTTER_BUILD_NAME)</string> + <key>CFBundleVersion</key> + <string>$(FLUTTER_BUILD_NUMBER)</string> + <key>LSMinimumSystemVersion</key> + <string>$(MACOSX_DEPLOYMENT_TARGET)</string> + <key>NSHumanReadableCopyright</key> + <string>$(PRODUCT_COPYRIGHT)</string> + <key>NSMainNibFile</key> + <string>MainMenu</string> + <key>NSPrincipalClass</key> + <string>NSApplication</string> +</dict> +</plist> diff --git a/package/example/my_controls/example/macos/Runner/MainFlutterWindow.swift b/package/example/my_controls/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 000000000..3cc05eb23 --- /dev/null +++ b/package/example/my_controls/example/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/package/example/my_controls/example/macos/Runner/Release.entitlements b/package/example/my_controls/example/macos/Runner/Release.entitlements new file mode 100644 index 000000000..e89b7f323 --- /dev/null +++ b/package/example/my_controls/example/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>com.apple.security.app-sandbox</key> + <false/> +</dict> +</plist> diff --git a/package/example/my_controls/example/macos/RunnerTests/RunnerTests.swift b/package/example/my_controls/example/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 000000000..5418c9f53 --- /dev/null +++ b/package/example/my_controls/example/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import FlutterMacOS +import Cocoa +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/package/example/my_controls/example/pubspec.lock b/package/example/my_controls/example/pubspec.lock new file mode 100644 index 000000000..e75b6769a --- /dev/null +++ b/package/example/my_controls/example/pubspec.lock @@ -0,0 +1,784 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + args: + dependency: transitive + description: + name: args + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + audioplayers: + dependency: transitive + description: + name: audioplayers + sha256: c05c6147124cd63e725e861335a8b4d57300b80e6e92cea7c145c739223bbaef + url: "https://pub.dev" + source: hosted + version: "5.2.1" + audioplayers_android: + dependency: transitive + description: + name: audioplayers_android + sha256: b00e1a0e11365d88576320ec2d8c192bc21f1afb6c0e5995d1c57ae63156acb5 + url: "https://pub.dev" + source: hosted + version: "4.0.3" + audioplayers_darwin: + dependency: transitive + description: + name: audioplayers_darwin + sha256: "3034e99a6df8d101da0f5082dcca0a2a99db62ab1d4ddb3277bed3f6f81afe08" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + audioplayers_linux: + dependency: transitive + description: + name: audioplayers_linux + sha256: "60787e73fefc4d2e0b9c02c69885402177e818e4e27ef087074cf27c02246c9e" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + audioplayers_platform_interface: + dependency: transitive + description: + name: audioplayers_platform_interface + sha256: "365c547f1bb9e77d94dd1687903a668d8f7ac3409e48e6e6a3668a1ac2982adb" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + audioplayers_web: + dependency: transitive + description: + name: audioplayers_web + sha256: "22cd0173e54d92bd9b2c80b1204eb1eb159ece87475ab58c9788a70ec43c2a62" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + audioplayers_windows: + dependency: transitive + description: + name: audioplayers_windows + sha256: "9536812c9103563644ada2ef45ae523806b0745f7a78e89d1b5fb1951de90e1a" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + url: "https://pub.dev" + source: hosted + version: "1.0.6" + equatable: + dependency: transitive + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + file_picker: + dependency: transitive + description: + name: file_picker + sha256: "4e42aacde3b993c5947467ab640882c56947d9d27342a5b6f2895b23956954a6" + url: "https://pub.dev" + source: hosted + version: "6.1.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + fl_chart: + dependency: transitive + description: + name: fl_chart + sha256: "5a74434cc83bf64346efb562f1a06eefaf1bcb530dc3d96a104f631a1eff8d79" + url: "https://pub.dev" + source: hosted + version: "0.65.0" + flet: + dependency: "direct main" + description: + path: "../../.." + relative: true + source: path + version: "0.19.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_highlight: + dependency: transitive + description: + name: flutter_highlight + sha256: "7b96333867aa07e122e245c033b8ad622e4e3a42a1a2372cbb098a2541d8782c" + url: "https://pub.dev" + source: hosted + version: "0.7.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + url: "https://pub.dev" + source: hosted + version: "2.0.3" + flutter_markdown: + dependency: transitive + description: + name: flutter_markdown + sha256: "30088ce826b5b9cfbf9e8bece34c716c8a59fa54461dcae1e4ac01a94639e762" + url: "https://pub.dev" + source: hosted + version: "0.6.18+3" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da + url: "https://pub.dev" + source: hosted + version: "2.0.17" + flutter_redux: + dependency: transitive + description: + name: flutter_redux + sha256: "3b20be9e08d0038e1452fbfa1fdb1ea0a7c3738c997734530b3c6d0bb5fcdbdc" + url: "https://pub.dev" + source: hosted + version: "0.10.0" + flutter_svg: + dependency: transitive + description: + name: flutter_svg + sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c + url: "https://pub.dev" + source: hosted + version: "2.0.9" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + highlight: + dependency: transitive + description: + name: highlight + sha256: "5353a83ffe3e3eca7df0abfb72dcf3fa66cc56b953728e7113ad4ad88497cf21" + url: "https://pub.dev" + source: hosted + version: "0.7.0" + http: + dependency: transitive + description: + name: http + sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba + url: "https://pub.dev" + source: hosted + version: "1.2.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + lints: + dependency: transitive + description: + name: lints + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + markdown: + dependency: transitive + description: + name: markdown + sha256: "1b134d9f8ff2da15cb298efe6cd8b7d2a78958c1b00384ebcbdf13fe340a6c90" + url: "https://pub.dev" + source: hosted + version: "7.2.1" + matcher: + dependency: transitive + description: + name: matcher + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + url: "https://pub.dev" + source: hosted + version: "0.12.16" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + url: "https://pub.dev" + source: hosted + version: "0.5.0" + meta: + dependency: transitive + description: + name: meta + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + url: "https://pub.dev" + source: hosted + version: "1.10.0" + my_controls: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "1.0.0" + path: + dependency: transitive + description: + name: path + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" + source: hosted + version: "1.8.3" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + url: "https://pub.dev" + source: hosted + version: "1.0.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" + source: hosted + version: "6.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + redux: + dependency: transitive + description: + name: redux + sha256: "1e86ed5b1a9a717922d0a0ca41f9bf49c1a587d50050e9426fc65b14e85ec4d7" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + screen_retriever: + dependency: transitive + description: + name: screen_retriever + sha256: "6ee02c8a1158e6dae7ca430da79436e3b1c9563c8cf02f524af997c201ac2b90" + url: "https://pub.dev" + source: hosted + version: "0.1.9" + sensors_plus: + dependency: transitive + description: + name: sensors_plus + sha256: "8e7fa79b4940442bb595bfc0ee9da4af5a22a0fe6ebacc74998245ee9496a82d" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + sensors_plus_platform_interface: + dependency: transitive + description: + name: sensors_plus_platform_interface + sha256: bc472d6cfd622acb4f020e726433ee31788b038056691ba433fec80e448a094f + url: "https://pub.dev" + source: hosted + version: "1.2.0" + shared_preferences: + dependency: transitive + description: + name: shared_preferences + sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" + url: "https://pub.dev" + source: hosted + version: "2.3.5" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" + url: "https://pub.dev" + source: hosted + version: "3.1.0+1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + url: "https://pub.dev" + source: hosted + version: "0.6.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + url_launcher: + dependency: transitive + description: + name: url_launcher + sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c + url: "https://pub.dev" + source: hosted + version: "6.2.4" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "507dc655b1d9cb5ebc756032eb785f114e415f91557b73bf60b7e201dfedeb2f" + url: "https://pub.dev" + source: hosted + version: "6.2.2" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03" + url: "https://pub.dev" + source: hosted + version: "6.2.4" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + url: "https://pub.dev" + source: hosted + version: "3.1.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 + url: "https://pub.dev" + source: hosted + version: "3.1.0" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: a932c3a8082e118f80a475ce692fde89dc20fddb24c57360b96bc56f7035de1f + url: "https://pub.dev" + source: hosted + version: "2.3.1" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b + url: "https://pub.dev" + source: hosted + version: "2.2.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + url: "https://pub.dev" + source: hosted + version: "3.1.1" + uuid: + dependency: transitive + description: + name: uuid + sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8 + url: "https://pub.dev" + source: hosted + version: "4.3.3" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "18f6690295af52d081f6808f2f7c69f0eed6d7e23a71539d75f4aeb8f0062172" + url: "https://pub.dev" + source: hosted + version: "1.1.9+2" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "531d20465c10dfac7f5cd90b60bbe4dd9921f1ec4ca54c83ebb176dbacb7bb2d" + url: "https://pub.dev" + source: hosted + version: "1.1.9+2" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "03012b0a33775c5530576b70240308080e1d5050f0faf000118c20e6463bc0ad" + url: "https://pub.dev" + source: hosted + version: "1.1.9+2" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + web: + dependency: transitive + description: + name: web + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + url: "https://pub.dev" + source: hosted + version: "0.3.0" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + url: "https://pub.dev" + source: hosted + version: "2.4.0" + webview_flutter: + dependency: transitive + description: + name: webview_flutter + sha256: "71e1bfaef41016c8d5954291df5e9f8c6172f1f6ff3af01b5656456ddb11f94c" + url: "https://pub.dev" + source: hosted + version: "4.4.4" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: "161af93c2abaf94ef2192bffb53a3658b2d721a3bf99b69aa1e47814ee18cc96" + url: "https://pub.dev" + source: hosted + version: "3.13.2" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: d937581d6e558908d7ae3dc1989c4f87b786891ab47bb9df7de548a151779d8d + url: "https://pub.dev" + source: hosted + version: "2.10.0" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: "4d062ad505390ecef1c4bfb6001cd857a51e00912cc9dfb66edb1886a9ebd80c" + url: "https://pub.dev" + source: hosted + version: "3.10.2" + win32: + dependency: transitive + description: + name: win32 + sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" + url: "https://pub.dev" + source: hosted + version: "5.2.0" + window_manager: + dependency: transitive + description: + name: window_manager + sha256: dcc865277f26a7dad263a47d0e405d77e21f12cb71f30333a52710a408690bd7 + url: "https://pub.dev" + source: hosted + version: "0.3.7" + window_to_front: + dependency: transitive + description: + name: window_to_front + sha256: "7aef379752b7190c10479e12b5fd7c0b9d92adc96817d9e96c59937929512aee" + url: "https://pub.dev" + source: hosted + version: "0.0.3" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.dev" + source: hosted + version: "1.0.4" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" +sdks: + dart: ">=3.2.3 <4.0.0" + flutter: ">=3.16.0" diff --git a/package/example/my_controls/example/pubspec.yaml b/package/example/my_controls/example/pubspec.yaml new file mode 100644 index 000000000..b4561ae69 --- /dev/null +++ b/package/example/my_controls/example/pubspec.yaml @@ -0,0 +1,96 @@ +name: my_controls_demo +description: "A new Flutter project." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: '>=3.2.3 <4.0.0' + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + + flet: + path: ../../../ + + my_controls: + path: ../ + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^2.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/package/example/my_controls/example/windows/.gitignore b/package/example/my_controls/example/windows/.gitignore new file mode 100644 index 000000000..d492d0d98 --- /dev/null +++ b/package/example/my_controls/example/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/package/example/my_controls/example/windows/CMakeLists.txt b/package/example/my_controls/example/windows/CMakeLists.txt new file mode 100644 index 000000000..afa61d2e8 --- /dev/null +++ b/package/example/my_controls/example/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(my_controls_demo LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "my_controls_demo") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$<CONFIG:Debug>:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$<TARGET_FILE_DIR:${BINARY_NAME}>") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/package/example/my_controls/example/windows/flutter/CMakeLists.txt b/package/example/my_controls/example/windows/flutter/CMakeLists.txt new file mode 100644 index 000000000..903f4899d --- /dev/null +++ b/package/example/my_controls/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $<CONFIG> + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/package/example/my_controls/example/windows/flutter/generated_plugin_registrant.cc b/package/example/my_controls/example/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 000000000..55f8b87a0 --- /dev/null +++ b/package/example/my_controls/example/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,26 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include <audioplayers_windows/audioplayers_windows_plugin.h> +#include <screen_retriever/screen_retriever_plugin.h> +#include <url_launcher_windows/url_launcher_windows.h> +#include <window_manager/window_manager_plugin.h> +#include <window_to_front/window_to_front_plugin.h> + +void RegisterPlugins(flutter::PluginRegistry* registry) { + AudioplayersWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin")); + ScreenRetrieverPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); + WindowManagerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WindowManagerPlugin")); + WindowToFrontPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WindowToFrontPlugin")); +} diff --git a/package/example/my_controls/example/windows/flutter/generated_plugin_registrant.h b/package/example/my_controls/example/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 000000000..dc139d85a --- /dev/null +++ b/package/example/my_controls/example/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include <flutter/plugin_registry.h> + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/package/example/my_controls/example/windows/flutter/generated_plugins.cmake b/package/example/my_controls/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 000000000..eba8ac422 --- /dev/null +++ b/package/example/my_controls/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,28 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + audioplayers_windows + screen_retriever + url_launcher_windows + window_manager + window_to_front +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/package/example/my_controls/example/windows/runner/CMakeLists.txt b/package/example/my_controls/example/windows/runner/CMakeLists.txt new file mode 100644 index 000000000..394917c05 --- /dev/null +++ b/package/example/my_controls/example/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/package/example/my_controls/example/windows/runner/Runner.rc b/package/example/my_controls/example/windows/runner/Runner.rc new file mode 100644 index 000000000..a77feb905 --- /dev/null +++ b/package/example/my_controls/example/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "my_controls_demo" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "my_controls_demo" "\0" + VALUE "LegalCopyright", "Copyright (C) 2024 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "my_controls_demo.exe" "\0" + VALUE "ProductName", "my_controls_demo" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/package/example/my_controls/example/windows/runner/flutter_window.cpp b/package/example/my_controls/example/windows/runner/flutter_window.cpp new file mode 100644 index 000000000..955ee3038 --- /dev/null +++ b/package/example/my_controls/example/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include <optional> + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique<flutter::FlutterViewController>( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional<LRESULT> result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/package/example/my_controls/example/windows/runner/flutter_window.h b/package/example/my_controls/example/windows/runner/flutter_window.h new file mode 100644 index 000000000..6da0652f0 --- /dev/null +++ b/package/example/my_controls/example/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include <flutter/dart_project.h> +#include <flutter/flutter_view_controller.h> + +#include <memory> + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr<flutter::FlutterViewController> flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/package/example/my_controls/example/windows/runner/main.cpp b/package/example/my_controls/example/windows/runner/main.cpp new file mode 100644 index 000000000..90c072a2d --- /dev/null +++ b/package/example/my_controls/example/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include <flutter/dart_project.h> +#include <flutter/flutter_view_controller.h> +#include <windows.h> + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector<std::string> command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"my_controls_demo", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/package/example/my_controls/example/windows/runner/resource.h b/package/example/my_controls/example/windows/runner/resource.h new file mode 100644 index 000000000..66a65d1e4 --- /dev/null +++ b/package/example/my_controls/example/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/package/example/my_controls/example/windows/runner/resources/app_icon.ico b/package/example/my_controls/example/windows/runner/resources/app_icon.ico new file mode 100644 index 000000000..c04e20caf Binary files /dev/null and b/package/example/my_controls/example/windows/runner/resources/app_icon.ico differ diff --git a/package/example/my_controls/example/windows/runner/runner.exe.manifest b/package/example/my_controls/example/windows/runner/runner.exe.manifest new file mode 100644 index 000000000..a42ea7687 --- /dev/null +++ b/package/example/my_controls/example/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> + <application xmlns="urn:schemas-microsoft-com:asm.v3"> + <windowsSettings> + <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness> + </windowsSettings> + </application> + <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> + <application> + <!-- Windows 10 and Windows 11 --> + <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> + <!-- Windows 8.1 --> + <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> + <!-- Windows 8 --> + <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> + <!-- Windows 7 --> + <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> + </application> + </compatibility> +</assembly> diff --git a/package/example/my_controls/example/windows/runner/utils.cpp b/package/example/my_controls/example/windows/runner/utils.cpp new file mode 100644 index 000000000..b2b08734d --- /dev/null +++ b/package/example/my_controls/example/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include <flutter_windows.h> +#include <io.h> +#include <stdio.h> +#include <windows.h> + +#include <iostream> + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector<std::string> GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector<std::string>(); + } + + std::vector<std::string> command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length <= 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/package/example/my_controls/example/windows/runner/utils.h b/package/example/my_controls/example/windows/runner/utils.h new file mode 100644 index 000000000..3879d5475 --- /dev/null +++ b/package/example/my_controls/example/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include <string> +#include <vector> + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector<std::string>, +// encoded in UTF-8. Returns an empty std::vector<std::string> on failure. +std::vector<std::string> GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/package/example/my_controls/example/windows/runner/win32_window.cpp b/package/example/my_controls/example/windows/runner/win32_window.cpp new file mode 100644 index 000000000..60608d0fe --- /dev/null +++ b/package/example/my_controls/example/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include <dwmapi.h> +#include <flutter_windows.h> + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast<int>(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast<EnableNonClientDpiScaling*>( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast<LONG>(origin.x), + static_cast<LONG>(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams)); + + auto that = static_cast<Win32Window*>(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast<RECT*>(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast<Win32Window*>( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/package/example/my_controls/example/windows/runner/win32_window.h b/package/example/my_controls/example/windows/runner/win32_window.h new file mode 100644 index 000000000..e901dde68 --- /dev/null +++ b/package/example/my_controls/example/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include <windows.h> + +#include <functional> +#include <memory> +#include <string> + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/package/example/my_controls/lib/my_controls.dart b/package/example/my_controls/lib/my_controls.dart new file mode 100644 index 000000000..d558e00b1 --- /dev/null +++ b/package/example/my_controls/lib/my_controls.dart @@ -0,0 +1,4 @@ +library my_controls; + +export "src/create_control.dart" show createControl; +export "src/my_control.dart" show MyControl; diff --git a/package/example/my_controls/lib/src/create_control.dart b/package/example/my_controls/lib/src/create_control.dart new file mode 100644 index 000000000..615de3023 --- /dev/null +++ b/package/example/my_controls/lib/src/create_control.dart @@ -0,0 +1,15 @@ +import 'package:flet/flet.dart'; + +import 'my_control.dart'; + +CreateControlFactory createControl = (CreateControlArgs args) { + switch (args.control.type) { + case "my_org:my_control": + return MyControl( + control: args.control, + children: args.children, + parentDisabled: args.parentDisabled); + default: + return null; + } +}; diff --git a/package/example/my_controls/lib/src/my_control.dart b/package/example/my_controls/lib/src/my_control.dart new file mode 100644 index 000000000..a75aec98e --- /dev/null +++ b/package/example/my_controls/lib/src/my_control.dart @@ -0,0 +1,43 @@ +import 'package:flet/flet.dart'; +import 'package:flutter/material.dart'; + +class MyControl extends StatelessWidget { + final Control? parent; + final Control control; + final List<Control> children; + final bool parentDisabled; + + const MyControl( + {super.key, + this.parent, + required this.control, + required this.children, + required this.parentDisabled}); + + @override + Widget build(BuildContext context) { + debugPrint("Card build: ${control.id}"); + + var contentCtrls = + children.where((c) => c.name == "content" && c.isVisible); + bool disabled = control.isDisabled || parentDisabled; + + return constrainedControl( + context, + Card( + elevation: control.attrDouble("elevation"), + shape: parseOutlinedBorder(control, "shape"), + margin: parseEdgeInsets(control, "margin"), + color: HexColor.fromString( + Theme.of(context), control.attrString("color", "")!), + shadowColor: HexColor.fromString( + Theme.of(context), control.attrString("shadowColor", "")!), + surfaceTintColor: HexColor.fromString( + Theme.of(context), control.attrString("surfaceTintColor", "")!), + child: contentCtrls.isNotEmpty + ? createControl(control, contentCtrls.first.id, disabled) + : null), + parent, + control); + } +} diff --git a/package/example/my_controls/pubspec.yaml b/package/example/my_controls/pubspec.yaml new file mode 100644 index 000000000..3ac9d1b02 --- /dev/null +++ b/package/example/my_controls/pubspec.yaml @@ -0,0 +1,21 @@ +name: my_controls +description: "An example of custom controls for Flet" +version: 1.0.0 +homepage: +publish_to: none + +environment: + sdk: '>=3.2.3 <4.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + + flet: + path: ../../ + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 \ No newline at end of file diff --git a/package/lib/flet.dart b/package/lib/flet.dart index 51af540b1..328869fc3 100644 --- a/package/lib/flet.dart +++ b/package/lib/flet.dart @@ -1,7 +1,43 @@ library flet; +export 'src/control_factory.dart'; +export 'src/controls/create_control.dart'; +export 'src/controls/flet_control_stateful_mixin.dart'; +export 'src/controls/flet_control_stateless_mixin.dart'; export 'src/flet_app.dart'; export 'src/flet_app_errors_handler.dart'; +export 'src/flet_server.dart'; +export 'src/models/app_state.dart'; +export 'src/models/asset_src.dart'; +export 'src/models/control.dart'; +export 'src/models/control_ancestor_view_model.dart'; +export 'src/models/control_tree_view_model.dart'; +export 'src/models/control_view_model.dart'; +export 'src/models/controls_view_model.dart'; +export 'src/models/page_args_model.dart'; +export 'src/models/page_size_view_model.dart'; export 'src/utils.dart'; +export 'src/utils/alignment.dart'; +export 'src/utils/animations.dart'; +export 'src/utils/borders.dart'; +export 'src/utils/buttons.dart'; +export 'src/utils/collections.dart'; +export 'src/utils/colors.dart'; +export 'src/utils/dash_path.dart'; +export 'src/utils/debouncer.dart'; +export 'src/utils/drawing.dart'; +export 'src/utils/edge_insets.dart'; +export 'src/utils/gradient.dart'; +export 'src/utils/icons.dart'; +export 'src/utils/images.dart'; +export 'src/utils/material_state.dart'; +export 'src/utils/menu.dart'; +export 'src/utils/mouse.dart'; +export 'src/utils/numbers.dart'; export 'src/utils/platform_utils_non_web.dart' if (dart.library.js) "src/utils/platform_utils_web.dart"; +export 'src/utils/responsive.dart'; +export 'src/utils/shadows.dart'; +export 'src/utils/strings.dart'; +export 'src/utils/text.dart'; +export 'src/utils/textfield.dart'; diff --git a/package/lib/src/control_factory.dart b/package/lib/src/control_factory.dart new file mode 100644 index 000000000..1612ef377 --- /dev/null +++ b/package/lib/src/control_factory.dart @@ -0,0 +1,16 @@ +import 'package:flutter/widgets.dart'; + +import 'models/control.dart'; + +class CreateControlArgs { + final Key? key; + final Control? parent; + final Control control; + final List<Control> children; + final bool parentDisabled; + + CreateControlArgs( + this.key, this.parent, this.control, this.children, this.parentDisabled); +} + +typedef CreateControlFactory = Widget? Function(CreateControlArgs args); diff --git a/package/lib/src/controls/alert_dialog.dart b/package/lib/src/controls/alert_dialog.dart index 8e71159e4..37703eecc 100644 --- a/package/lib/src/controls/alert_dialog.dart +++ b/package/lib/src/controls/alert_dialog.dart @@ -1,18 +1,14 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_redux/flutter_redux.dart'; -import '../actions.dart'; -import '../flet_app_services.dart'; -import '../models/app_state.dart'; import '../models/control.dart'; -import '../protocol/update_control_props_payload.dart'; import '../utils/alignment.dart'; import '../utils/borders.dart'; import '../utils/edge_insets.dart'; import 'create_control.dart'; import 'cupertino_alert_dialog.dart'; import 'error.dart'; +import 'flet_control_stateful_mixin.dart'; +import 'flet_store_mixin.dart'; class AlertDialogControl extends StatefulWidget { final Control? parent; @@ -21,19 +17,21 @@ class AlertDialogControl extends StatefulWidget { final bool parentDisabled; final Widget? nextChild; - const AlertDialogControl( - {super.key, - this.parent, - required this.control, - required this.children, - required this.parentDisabled, - required this.nextChild}); + const AlertDialogControl({ + super.key, + this.parent, + required this.control, + required this.children, + required this.parentDisabled, + required this.nextChild, + }); @override State<AlertDialogControl> createState() => _AlertDialogControlState(); } -class _AlertDialogControlState extends State<AlertDialogControl> { +class _AlertDialogControlState extends State<AlertDialogControl> + with FletControlStatefulMixin, FletStoreMixin { Widget _createAlertDialog() { bool disabled = widget.control.isDisabled || widget.parentDisabled; var titleCtrls = @@ -73,77 +71,64 @@ class _AlertDialogControlState extends State<AlertDialogControl> { Widget build(BuildContext context) { debugPrint("AlertDialog build ($hashCode): ${widget.control.id}"); - bool adaptive = widget.control.attrBool("adaptive", false)!; - if (adaptive && - (defaultTargetPlatform == TargetPlatform.iOS || - defaultTargetPlatform == TargetPlatform.macOS)) { - return CupertinoAlertDialogControl( - control: widget.control, - parentDisabled: widget.parentDisabled, - children: widget.children, - nextChild: widget.nextChild, - ); - } - - var server = FletAppServices.of(context).server; - - bool lastOpen = widget.control.state["open"] ?? false; - - return StoreConnector<AppState, Function>( - distinct: true, - converter: (store) => store.dispatch, - builder: (context, dispatch) { - debugPrint("AlertDialog StoreConnector build: ${widget.control.id}"); - - var open = widget.control.attrBool("open", false)!; - var modal = widget.control.attrBool("modal", false)!; - - debugPrint("Current open state: $lastOpen"); - debugPrint("New open state: $open"); - - if (open && (open != lastOpen)) { - var dialog = _createAlertDialog(); - if (dialog is ErrorControl) { - return dialog; - } - - // close previous dialog - if (ModalRoute.of(context)?.isCurrent != true) { - Navigator.of(context).pop(); + return withPagePlatform((context, platform) { + bool adaptive = widget.control.attrBool("adaptive", false)!; + if (adaptive && + (platform == TargetPlatform.iOS || + platform == TargetPlatform.macOS)) { + return CupertinoAlertDialogControl( + control: widget.control, + parentDisabled: widget.parentDisabled, + children: widget.children, + nextChild: widget.nextChild, + ); + } + + bool lastOpen = widget.control.state["open"] ?? false; + + debugPrint("AlertDialog build: ${widget.control.id}"); + + var open = widget.control.attrBool("open", false)!; + var modal = widget.control.attrBool("modal", false)!; + + debugPrint("Current open state: $lastOpen"); + debugPrint("New open state: $open"); + + if (open && (open != lastOpen)) { + var dialog = _createAlertDialog(); + if (dialog is ErrorControl) { + return dialog; + } + + // close previous dialog + if (ModalRoute.of(context)?.isCurrent != true) { + Navigator.of(context).pop(); + } + + widget.control.state["open"] = open; + + WidgetsBinding.instance.addPostFrameCallback((_) { + showDialog( + barrierDismissible: !modal, + useRootNavigator: false, + context: context, + builder: (context) => _createAlertDialog()).then((value) { + lastOpen = widget.control.state["open"] ?? false; + debugPrint("Dialog should be dismissed ($hashCode): $lastOpen"); + bool shouldDismiss = lastOpen; + widget.control.state["open"] = false; + + if (shouldDismiss) { + updateControlProps(widget.control.id, {"open": "false"}); + sendControlEvent(widget.control.id, "dismiss", ""); } - - widget.control.state["open"] = open; - - WidgetsBinding.instance.addPostFrameCallback((_) { - showDialog( - barrierDismissible: !modal, - useRootNavigator: false, - context: context, - builder: (context) => _createAlertDialog()).then((value) { - lastOpen = widget.control.state["open"] ?? false; - debugPrint("Dialog should be dismissed ($hashCode): $lastOpen"); - bool shouldDismiss = lastOpen; - widget.control.state["open"] = false; - - if (shouldDismiss) { - List<Map<String, String>> props = [ - {"i": widget.control.id, "open": "false"} - ]; - dispatch(UpdateControlPropsAction( - UpdateControlPropsPayload(props: props))); - server.updateControlProps(props: props); - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "dismiss", - eventData: ""); - } - }); - }); - } else if (open != lastOpen && lastOpen) { - Navigator.of(context).pop(); - } - - return widget.nextChild ?? const SizedBox.shrink(); + }); }); + } else if (open != lastOpen && lastOpen) { + Navigator.of(context).pop(); + } + + return widget.nextChild ?? const SizedBox.shrink(); + }); } } diff --git a/package/lib/src/controls/app_bar.dart b/package/lib/src/controls/app_bar.dart index a8fe03226..864651e58 100644 --- a/package/lib/src/controls/app_bar.dart +++ b/package/lib/src/controls/app_bar.dart @@ -1,12 +1,15 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import '../models/control.dart'; import '../utils/colors.dart'; import 'create_control.dart'; import 'cupertino_app_bar.dart'; +import 'flet_control_stateless_mixin.dart'; +import 'flet_store_mixin.dart'; -class AppBarControl extends StatelessWidget implements PreferredSizeWidget { +class AppBarControl extends StatelessWidget + with FletControlStatelessMixin, FletStoreMixin + implements PreferredSizeWidget { final Control? parent; final Control control; final bool parentDisabled; @@ -25,51 +28,54 @@ class AppBarControl extends StatelessWidget implements PreferredSizeWidget { Widget build(BuildContext context) { debugPrint("AppBar build: ${control.id}"); - bool adaptive = control.attrBool("adaptive", false)!; - if (adaptive && - (defaultTargetPlatform == TargetPlatform.iOS || - defaultTargetPlatform == TargetPlatform.macOS)) { - return CupertinoAppBarControl( - control: control, - parentDisabled: parentDisabled, - children: children, - bgcolor: HexColor.fromString( - Theme.of(context), control.attrString("bgcolor", "")!)); - } + return withPagePlatform((context, platform) { + bool adaptive = control.attrBool("adaptive", false)!; + if (adaptive && + (platform == TargetPlatform.iOS || + platform == TargetPlatform.macOS)) { + return CupertinoAppBarControl( + control: control, + parentDisabled: parentDisabled, + children: children, + bgcolor: HexColor.fromString( + Theme.of(context), control.attrString("bgcolor", "")!)); + } - var leadingCtrls = - children.where((c) => c.name == "leading" && c.isVisible); - var titleCtrls = children.where((c) => c.name == "title" && c.isVisible); - var actionCtrls = children.where((c) => c.name == "action" && c.isVisible); + var leadingCtrls = + children.where((c) => c.name == "leading" && c.isVisible); + var titleCtrls = children.where((c) => c.name == "title" && c.isVisible); + var actionCtrls = + children.where((c) => c.name == "action" && c.isVisible); - var leadingWidth = control.attrDouble("leadingWidth"); - var elevation = control.attrDouble("elevation"); - var centerTitle = control.attrBool("centerTitle", false)!; - var automaticallyImplyLeading = - control.attrBool("automaticallyImplyLeading", true)!; - var color = HexColor.fromString( - Theme.of(context), control.attrString("color", "")!); - var bgcolor = HexColor.fromString( - Theme.of(context), control.attrString("bgcolor", "")!); + var leadingWidth = control.attrDouble("leadingWidth"); + var elevation = control.attrDouble("elevation"); + var centerTitle = control.attrBool("centerTitle", false)!; + var automaticallyImplyLeading = + control.attrBool("automaticallyImplyLeading", true)!; + var color = HexColor.fromString( + Theme.of(context), control.attrString("color", "")!); + var bgcolor = HexColor.fromString( + Theme.of(context), control.attrString("bgcolor", "")!); - return AppBar( - leading: leadingCtrls.isNotEmpty - ? createControl(control, leadingCtrls.first.id, control.isDisabled) - : null, - leadingWidth: leadingWidth, - automaticallyImplyLeading: automaticallyImplyLeading, - title: titleCtrls.isNotEmpty - ? createControl(control, titleCtrls.first.id, control.isDisabled) - : null, - centerTitle: centerTitle, - toolbarHeight: preferredSize.height, - foregroundColor: color, - backgroundColor: bgcolor, - elevation: elevation, - actions: actionCtrls - .map((c) => createControl(control, c.id, control.isDisabled)) - .toList(), - ); + return AppBar( + leading: leadingCtrls.isNotEmpty + ? createControl(control, leadingCtrls.first.id, control.isDisabled) + : null, + leadingWidth: leadingWidth, + automaticallyImplyLeading: automaticallyImplyLeading, + title: titleCtrls.isNotEmpty + ? createControl(control, titleCtrls.first.id, control.isDisabled) + : null, + centerTitle: centerTitle, + toolbarHeight: preferredSize.height, + foregroundColor: color, + backgroundColor: bgcolor, + elevation: elevation, + actions: actionCtrls + .map((c) => createControl(control, c.id, control.isDisabled)) + .toList(), + ); + }); } @override diff --git a/package/lib/src/controls/audio.dart b/package/lib/src/controls/audio.dart index 60f075b51..629558f36 100644 --- a/package/lib/src/controls/audio.dart +++ b/package/lib/src/controls/audio.dart @@ -5,34 +5,30 @@ import 'package:audioplayers/audioplayers.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; -import 'package:flutter_redux/flutter_redux.dart'; -import '../flet_app_services.dart'; -import '../flet_server.dart'; -import '../models/app_state.dart'; import '../models/control.dart'; -import '../models/page_args_model.dart'; import '../utils/images.dart'; import 'error.dart'; +import 'flet_control_stateful_mixin.dart'; +import 'flet_store_mixin.dart'; class AudioControl extends StatefulWidget { final Control? parent; final Control control; - final dynamic dispatch; final Widget? nextChild; const AudioControl( {super.key, required this.parent, required this.control, - required this.dispatch, required this.nextChild}); @override State<AudioControl> createState() => _AudioControlState(); } -class _AudioControlState extends State<AudioControl> { +class _AudioControlState extends State<AudioControl> + with FletControlStatefulMixin, FletStoreMixin { AudioPlayer? player; void Function(Duration)? _onDurationChanged; void Function(PlayerState)? _onStateChanged; @@ -44,7 +40,6 @@ class _AudioControlState extends State<AudioControl> { StreamSubscription? _onStateChangedSubscription; StreamSubscription? _onPositionChangedSubscription; StreamSubscription? _onSeekCompleteSubscription; - FletServer? _server; @override void initState() { @@ -86,7 +81,7 @@ class _AudioControlState extends State<AudioControl> { void _onRemove() { debugPrint("Audio.remove($hashCode)"); widget.control.state["player"]?.dispose(); - _server?.controlInvokeMethods.remove(widget.control.id); + unsubscribeMethods(widget.control.id); } @override @@ -120,8 +115,6 @@ class _AudioControlState extends State<AudioControl> { bool onPositionChanged = widget.control.attrBool("onPositionChanged", false)!; - var server = FletAppServices.of(context).server; - final String prevSrc = widget.control.state["src"] ?? ""; final String prevSrcBase64 = widget.control.state["srcBase64"] ?? ""; final ReleaseMode? prevReleaseMode = widget.control.state["releaseMode"]; @@ -129,147 +122,128 @@ class _AudioControlState extends State<AudioControl> { final double? prevBalance = widget.control.state["balance"]; final double? prevPlaybackRate = widget.control.state["playbackRate"]; - return StoreConnector<AppState, PageArgsModel>( - distinct: true, - converter: (store) => PageArgsModel.fromStore(store), - builder: (context, pageArgs) { - _onDurationChanged = (duration) { - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "duration_changed", - eventData: duration.inMilliseconds.toString()); - }; - - _onStateChanged = (state) { - debugPrint("Audio($hashCode) - state_changed: ${state.name}"); - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "state_changed", - eventData: state.name.toString()); - }; + return withPageArgs((context, pageArgs) { + _onDurationChanged = (duration) { + sendControlEvent(widget.control.id, "duration_changed", + duration.inMilliseconds.toString()); + }; + + _onStateChanged = (state) { + debugPrint("Audio($hashCode) - state_changed: ${state.name}"); + sendControlEvent( + widget.control.id, "state_changed", state.name.toString()); + }; + + if (onPositionChanged) { + _onPositionChanged = (duration) { + sendControlEvent( + widget.control.id, "position_changed", duration.toString()); + }; + } - if (onPositionChanged) { - _onPositionChanged = (duration) { - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "position_changed", - eventData: duration.toString()); - }; + _onSeekComplete = () { + sendControlEvent(widget.control.id, "seek_complete", ""); + }; + + () async { + debugPrint("Audio ($hashCode) src=$src, prevSrc=$prevSrc"); + debugPrint( + "Audio ($hashCode) srcBase64=$srcBase64, prevSrcBase64=$prevSrcBase64"); + + bool srcChanged = false; + if (src != "" && src != prevSrc) { + widget.control.state["src"] = src; + srcChanged = true; + + // URL or file? + var assetSrc = + getAssetSrc(src, pageArgs.pageUri!, pageArgs.assetsDir); + if (assetSrc.isFile) { + await player?.setSourceDeviceFile(assetSrc.path); + } else { + await player?.setSourceUrl(assetSrc.path); } - - _onSeekComplete = () { - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "seek_complete", - eventData: ""); - }; - - () async { - debugPrint("Audio ($hashCode) src=$src, prevSrc=$prevSrc"); - debugPrint( - "Audio ($hashCode) srcBase64=$srcBase64, prevSrcBase64=$prevSrcBase64"); - - bool srcChanged = false; - if (src != "" && src != prevSrc) { - widget.control.state["src"] = src; - srcChanged = true; - - // URL or file? - var assetSrc = - getAssetSrc(src, pageArgs.pageUri!, pageArgs.assetsDir); - if (assetSrc.isFile) { - await player?.setSourceDeviceFile(assetSrc.path); - } else { - await player?.setSourceUrl(assetSrc.path); - } - } else if (srcBase64 != "" && srcBase64 != prevSrcBase64) { - widget.control.state["srcBase64"] = srcBase64; - srcChanged = true; - await player?.setSourceBytes(base64Decode(srcBase64)); - } - - if (srcChanged) { - debugPrint("Audio.srcChanged!"); - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "loaded", - eventData: ""); - } - - if (releaseMode != null && releaseMode != prevReleaseMode) { - debugPrint("Audio.setReleaseMode($releaseMode)"); - widget.control.state["releaseMode"] = releaseMode; - await player?.setReleaseMode(releaseMode); - } - - if (volume != null && - volume != prevVolume && - volume >= 0 && - volume <= 1) { - widget.control.state["volume"] = volume; - debugPrint("Audio.setVolume($volume)"); - await player?.setVolume(volume); - } - - if (playbackRate != null && - playbackRate != prevPlaybackRate && - playbackRate >= 0 && - playbackRate <= 2) { - widget.control.state["playbackRate"] = playbackRate; - debugPrint("Audio.setPlaybackRate($playbackRate)"); - await player?.setPlaybackRate(playbackRate); - } - - if (!kIsWeb && - balance != null && - balance != prevBalance && - balance >= -1 && - balance <= 1) { - widget.control.state["balance"] = balance; - debugPrint("Audio.setBalance($balance)"); - await player?.setBalance(balance); - } - - if (srcChanged && autoplay) { - debugPrint("Audio.resume($srcChanged, $autoplay)"); + } else if (srcBase64 != "" && srcBase64 != prevSrcBase64) { + widget.control.state["srcBase64"] = srcBase64; + srcChanged = true; + await player?.setSourceBytes(base64Decode(srcBase64)); + } + + if (srcChanged) { + debugPrint("Audio.srcChanged!"); + sendControlEvent(widget.control.id, "loaded", ""); + } + + if (releaseMode != null && releaseMode != prevReleaseMode) { + debugPrint("Audio.setReleaseMode($releaseMode)"); + widget.control.state["releaseMode"] = releaseMode; + await player?.setReleaseMode(releaseMode); + } + + if (volume != null && + volume != prevVolume && + volume >= 0 && + volume <= 1) { + widget.control.state["volume"] = volume; + debugPrint("Audio.setVolume($volume)"); + await player?.setVolume(volume); + } + + if (playbackRate != null && + playbackRate != prevPlaybackRate && + playbackRate >= 0 && + playbackRate <= 2) { + widget.control.state["playbackRate"] = playbackRate; + debugPrint("Audio.setPlaybackRate($playbackRate)"); + await player?.setPlaybackRate(playbackRate); + } + + if (!kIsWeb && + balance != null && + balance != prevBalance && + balance >= -1 && + balance <= 1) { + widget.control.state["balance"] = balance; + debugPrint("Audio.setBalance($balance)"); + await player?.setBalance(balance); + } + + if (srcChanged && autoplay) { + debugPrint("Audio.resume($srcChanged, $autoplay)"); + await player?.resume(); + } + + subscribeMethods(widget.control.id, (methodName, args) async { + switch (methodName) { + case "play": + await player?.seek(const Duration(milliseconds: 0)); await player?.resume(); - } - - _server = server; - _server?.controlInvokeMethods[widget.control.id] = - (methodName, args) async { - switch (methodName) { - case "play": - await player?.seek(const Duration(milliseconds: 0)); - await player?.resume(); - break; - case "resume": - await player?.resume(); - break; - case "pause": - await player?.pause(); - break; - case "release": - await player?.release(); - break; - case "seek": - await player?.seek(Duration( - milliseconds: int.tryParse(args["position"] ?? "") ?? 0)); - break; - case "get_duration": - return (await player?.getDuration()) - ?.inMilliseconds - .toString(); - case "get_current_position": - return (await player?.getCurrentPosition()) - ?.inMilliseconds - .toString(); - } - return null; - }; - }(); - - return const SizedBox.shrink(); + break; + case "resume": + await player?.resume(); + break; + case "pause": + await player?.pause(); + break; + case "release": + await player?.release(); + break; + case "seek": + await player?.seek(Duration( + milliseconds: int.tryParse(args["position"] ?? "") ?? 0)); + break; + case "get_duration": + return (await player?.getDuration())?.inMilliseconds.toString(); + case "get_current_position": + return (await player?.getCurrentPosition()) + ?.inMilliseconds + .toString(); + } + return null; }); + }(); + + return const SizedBox.shrink(); + }); } } diff --git a/package/lib/src/controls/banner.dart b/package/lib/src/controls/banner.dart index 52337d85e..0f4de50f0 100644 --- a/package/lib/src/controls/banner.dart +++ b/package/lib/src/controls/banner.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter_redux/flutter_redux.dart'; -import '../models/app_state.dart'; import '../models/control.dart'; import '../utils/colors.dart'; import '../utils/edge_insets.dart'; @@ -65,38 +63,33 @@ class _BannerControlState extends State<BannerControl> { Widget build(BuildContext context) { debugPrint("Banner build: ${widget.control.id}"); - return StoreConnector<AppState, Function>( - distinct: true, - converter: (store) => store.dispatch, - builder: (context, dispatch) { - debugPrint("Banner StoreConnector build: ${widget.control.id}"); + debugPrint("Banner build: ${widget.control.id}"); - var open = widget.control.attrBool("open", false)!; + var open = widget.control.attrBool("open", false)!; - debugPrint("Current open state: $_open"); - debugPrint("New open state: $open"); + debugPrint("Current open state: $_open"); + debugPrint("New open state: $open"); - if (open && (open != _open)) { - var banner = _createBanner(); - if (banner is ErrorControl) { - return banner; - } + if (open && (open != _open)) { + var banner = _createBanner(); + if (banner is ErrorControl) { + return banner; + } - WidgetsBinding.instance.addPostFrameCallback((_) { - ScaffoldMessenger.of(context).removeCurrentMaterialBanner(); + WidgetsBinding.instance.addPostFrameCallback((_) { + ScaffoldMessenger.of(context).removeCurrentMaterialBanner(); - ScaffoldMessenger.of(context) - .showMaterialBanner(banner as MaterialBanner); - }); - } else if (open != _open && _open) { - WidgetsBinding.instance.addPostFrameCallback((_) { - ScaffoldMessenger.of(context).removeCurrentMaterialBanner(); - }); - } + ScaffoldMessenger.of(context) + .showMaterialBanner(banner as MaterialBanner); + }); + } else if (open != _open && _open) { + WidgetsBinding.instance.addPostFrameCallback((_) { + ScaffoldMessenger.of(context).removeCurrentMaterialBanner(); + }); + } - _open = open; + _open = open; - return widget.nextChild ?? const SizedBox.shrink(); - }); + return widget.nextChild ?? const SizedBox.shrink(); } } diff --git a/package/lib/src/controls/barchart.dart b/package/lib/src/controls/barchart.dart index c7651f3d2..502b09275 100644 --- a/package/lib/src/controls/barchart.dart +++ b/package/lib/src/controls/barchart.dart @@ -1,18 +1,13 @@ import 'dart:convert'; import 'package:collection/collection.dart'; +import 'package:equatable/equatable.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; +import 'package:redux/redux.dart'; -import '../flet_app_services.dart'; import '../models/app_state.dart'; -import '../models/barchart_event_data.dart'; -import '../models/barchart_group_view_model.dart'; -import '../models/barchart_rod_stack_item_view_model.dart'; -import '../models/barchart_rod_view_model.dart'; -import '../models/barchart_view_model.dart'; -import '../models/chart_axis_view_model.dart'; import '../models/control.dart'; import '../utils/animations.dart'; import '../utils/borders.dart'; @@ -20,7 +15,142 @@ import '../utils/charts.dart'; import '../utils/colors.dart'; import '../utils/gradient.dart'; import '../utils/text.dart'; +import 'charts.dart'; import 'create_control.dart'; +import 'flet_control_stateful_mixin.dart'; + +class BarChartEventData extends Equatable { + final String eventType; + final int? groupIndex; + final int? rodIndex; + final int? stackItemIndex; + + const BarChartEventData( + {required this.eventType, + required this.groupIndex, + required this.rodIndex, + required this.stackItemIndex}); + + Map<String, dynamic> toJson() => <String, dynamic>{ + 'type': eventType, + 'group_index': groupIndex, + 'rod_index': rodIndex, + 'stack_item_index': stackItemIndex + }; + + @override + List<Object?> get props => [eventType, groupIndex, rodIndex, stackItemIndex]; +} + +class BarChartGroupViewModel extends Equatable { + final Control control; + final List<BarChartRodViewModel> barRods; + + const BarChartGroupViewModel({required this.control, required this.barRods}); + + static BarChartGroupViewModel fromStore( + Store<AppState> store, Control control) { + return BarChartGroupViewModel( + control: control, + barRods: store.state.controls[control.id]!.childIds + .map((childId) => store.state.controls[childId]) + .whereNotNull() + .where((c) => c.isVisible) + .map((c) => BarChartRodViewModel.fromStore(store, c)) + .toList()); + } + + @override + List<Object?> get props => [control, barRods]; +} + +class BarChartRodStackItemViewModel extends Equatable { + final Control control; + + const BarChartRodStackItemViewModel({required this.control}); + + static BarChartRodStackItemViewModel fromStore( + Store<AppState> store, Control control) { + return BarChartRodStackItemViewModel(control: control); + } + + @override + List<Object?> get props => [control]; +} + +class BarChartRodViewModel extends Equatable { + final Control control; + final List<BarChartRodStackItemViewModel> rodStackItems; + + const BarChartRodViewModel( + {required this.control, required this.rodStackItems}); + + static BarChartRodViewModel fromStore( + Store<AppState> store, Control control) { + return BarChartRodViewModel( + control: control, + rodStackItems: store.state.controls[control.id]!.childIds + .map((childId) => store.state.controls[childId]) + .whereNotNull() + .where((c) => c.isVisible) + .map((c) => BarChartRodStackItemViewModel.fromStore(store, c)) + .toList()); + } + + @override + List<Object?> get props => [control, rodStackItems]; +} + +class BarChartViewModel extends Equatable { + final Control control; + final ChartAxisViewModel? leftAxis; + final ChartAxisViewModel? topAxis; + final ChartAxisViewModel? rightAxis; + final ChartAxisViewModel? bottomAxis; + final List<BarChartGroupViewModel> barGroups; + + const BarChartViewModel( + {required this.control, + required this.leftAxis, + required this.topAxis, + required this.rightAxis, + required this.bottomAxis, + required this.barGroups}); + + static BarChartViewModel fromStore( + Store<AppState> store, Control control, List<Control> children) { + var leftAxisCtrls = + children.where((c) => c.type == "axis" && c.name == "l" && c.isVisible); + var topAxisCtrls = + children.where((c) => c.type == "axis" && c.name == "t" && c.isVisible); + var rightAxisCtrls = + children.where((c) => c.type == "axis" && c.name == "r" && c.isVisible); + var bottomAxisCtrls = + children.where((c) => c.type == "axis" && c.name == "b" && c.isVisible); + return BarChartViewModel( + control: control, + leftAxis: leftAxisCtrls.isNotEmpty + ? ChartAxisViewModel.fromStore(store, leftAxisCtrls.first) + : null, + topAxis: topAxisCtrls.isNotEmpty + ? ChartAxisViewModel.fromStore(store, topAxisCtrls.first) + : null, + rightAxis: rightAxisCtrls.isNotEmpty + ? ChartAxisViewModel.fromStore(store, rightAxisCtrls.first) + : null, + bottomAxis: bottomAxisCtrls.isNotEmpty + ? ChartAxisViewModel.fromStore(store, bottomAxisCtrls.first) + : null, + barGroups: children + .where((c) => c.type == "group" && c.isVisible) + .map((c) => BarChartGroupViewModel.fromStore(store, c)) + .toList()); + } + + @override + List<Object?> get props => + [control, leftAxis, rightAxis, topAxis, bottomAxis, barGroups]; +} class BarChartControl extends StatefulWidget { final Control? parent; @@ -39,7 +169,8 @@ class BarChartControl extends StatefulWidget { State<BarChartControl> createState() => _BarChartControlState(); } -class _BarChartControlState extends State<BarChartControl> { +class _BarChartControlState extends State<BarChartControl> + with FletControlStatefulMixin { BarChartEventData? _eventData; @override @@ -149,10 +280,8 @@ class _BarChartControlState extends State<BarChartControl> { _eventData = eventData; debugPrint( "BarChart ${widget.control.id} ${eventData.eventType}"); - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "chart_event", - eventData: json.encode(eventData)); + sendControlEvent(widget.control.id, "chart_event", + json.encode(eventData)); } } : null, diff --git a/package/lib/src/controls/bottom_app_bar.dart b/package/lib/src/controls/bottom_app_bar.dart index 1fb333db5..13f91a161 100644 --- a/package/lib/src/controls/bottom_app_bar.dart +++ b/package/lib/src/controls/bottom_app_bar.dart @@ -1,13 +1,12 @@ -import 'package:flet/src/controls/error.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_redux/flutter_redux.dart'; -import '../models/app_state.dart'; import '../models/control.dart'; -import '../models/controls_view_model.dart'; import '../utils/colors.dart'; import '../utils/edge_insets.dart'; import 'create_control.dart'; +import 'error.dart'; +import 'flet_control_stateful_mixin.dart'; +import 'flet_store_mixin.dart'; class BottomAppBarControl extends StatefulWidget { final Control? parent; @@ -26,7 +25,8 @@ class BottomAppBarControl extends StatefulWidget { State<BottomAppBarControl> createState() => _BottomAppBarControlState(); } -class _BottomAppBarControlState extends State<BottomAppBarControl> { +class _BottomAppBarControlState extends State<BottomAppBarControl> + with FletControlStatefulMixin, FletStoreMixin { @override Widget build(BuildContext context) { debugPrint("BottomAppBarControl build: ${widget.control.id}"); @@ -54,32 +54,28 @@ class _BottomAppBarControlState extends State<BottomAppBarControl> { e.name.toLowerCase() == widget.control.attrString("clipBehavior", "")!.toLowerCase(), orElse: () => Clip.none); - var bottomAppBar = StoreConnector<AppState, ControlsViewModel>( - distinct: true, - converter: (store) => ControlsViewModel.fromStore( - store, - widget.children - .where((c) => c.isVisible && c.name == null) - .map((c) => c.id)), - builder: (content, viewModel) { - return BottomAppBar( - clipBehavior: clipBehavior, - padding: parseEdgeInsets(widget.control, "padding"), - height: widget.control.attrDouble("height"), - elevation: elevation, - shape: shape, - shadowColor: HexColor.fromString(Theme.of(context), - widget.control.attrString("shadowColor", "")!), - surfaceTintColor: HexColor.fromString(Theme.of(context), - widget.control.attrString("surfaceTintColor", "")!), - color: HexColor.fromString( - Theme.of(context), widget.control.attrString("bgColor", "")!), - notchMargin: widget.control.attrDouble("notchMargin", 4.0)!, - child: contentCtrls.isNotEmpty - ? createControl(widget.control, contentCtrls.first.id, disabled) - : null, - ); - }); + var bottomAppBar = withControls( + widget.children + .where((c) => c.isVisible && c.name == null) + .map((c) => c.id), (content, viewModel) { + return BottomAppBar( + clipBehavior: clipBehavior, + padding: parseEdgeInsets(widget.control, "padding"), + height: widget.control.attrDouble("height"), + elevation: elevation, + shape: shape, + shadowColor: HexColor.fromString( + Theme.of(context), widget.control.attrString("shadowColor", "")!), + surfaceTintColor: HexColor.fromString(Theme.of(context), + widget.control.attrString("surfaceTintColor", "")!), + color: HexColor.fromString( + Theme.of(context), widget.control.attrString("bgColor", "")!), + notchMargin: widget.control.attrDouble("notchMargin", 4.0)!, + child: contentCtrls.isNotEmpty + ? createControl(widget.control, contentCtrls.first.id, disabled) + : null, + ); + }); return constrainedControl( context, bottomAppBar, widget.parent, widget.control); diff --git a/package/lib/src/controls/bottom_sheet.dart b/package/lib/src/controls/bottom_sheet.dart index b615c28dd..e79883db2 100644 --- a/package/lib/src/controls/bottom_sheet.dart +++ b/package/lib/src/controls/bottom_sheet.dart @@ -1,19 +1,16 @@ import 'package:flutter/material.dart'; -import '../actions.dart'; -import '../flet_app_services.dart'; import '../models/control.dart'; -import '../protocol/update_control_props_payload.dart'; import '../utils/colors.dart'; import 'create_control.dart'; import 'error.dart'; +import 'flet_control_stateful_mixin.dart'; class BottomSheetControl extends StatefulWidget { final Control? parent; final Control control; final List<Control> children; final bool parentDisabled; - final dynamic dispatch; final Widget? nextChild; const BottomSheetControl( @@ -22,20 +19,18 @@ class BottomSheetControl extends StatefulWidget { required this.control, required this.children, required this.parentDisabled, - required this.dispatch, required this.nextChild}); @override State<BottomSheetControl> createState() => _BottomSheetControlState(); } -class _BottomSheetControlState extends State<BottomSheetControl> { +class _BottomSheetControlState extends State<BottomSheetControl> + with FletControlStatefulMixin { @override Widget build(BuildContext context) { debugPrint("BottomSheet build: ${widget.control.id}"); - var server = FletAppServices.of(context).server; - bool lastOpen = widget.control.state["open"] ?? false; bool disabled = widget.control.isDisabled || widget.parentDisabled; @@ -51,12 +46,7 @@ class _BottomSheetControlState extends State<BottomSheetControl> { widget.control.attrBool("maintainBottomViewInsetsPadding", true)!; void resetOpenState() { - List<Map<String, String>> props = [ - {"i": widget.control.id, "open": "false"} - ]; - widget.dispatch( - UpdateControlPropsAction(UpdateControlPropsPayload(props: props))); - server.updateControlProps(props: props); + updateControlProps(widget.control.id, {"open": "false"}); } if (open && !lastOpen) { @@ -110,10 +100,7 @@ class _BottomSheetControlState extends State<BottomSheetControl> { if (shouldDismiss) { resetOpenState(); - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "dismiss", - eventData: ""); + sendControlEvent(widget.control.id, "dismiss", ""); } }); }); diff --git a/package/lib/src/controls/canvas.dart b/package/lib/src/controls/canvas.dart index 7c2cb03c2..1ac733710 100644 --- a/package/lib/src/controls/canvas.dart +++ b/package/lib/src/controls/canvas.dart @@ -2,12 +2,12 @@ import 'dart:convert'; import 'dart:ui' as ui; import 'package:collection/collection.dart'; +import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; +import 'package:redux/redux.dart'; -import '../flet_app_services.dart'; import '../models/app_state.dart'; -import '../models/canvas_view_model.dart'; import '../models/control.dart'; import '../models/control_tree_view_model.dart'; import '../utils/alignment.dart'; @@ -19,6 +19,34 @@ import '../utils/numbers.dart'; import '../utils/text.dart'; import '../utils/transforms.dart'; import 'create_control.dart'; +import 'flet_control_stateful_mixin.dart'; + +class CanvasViewModel extends Equatable { + final Control control; + final Control? child; + final List<ControlTreeViewModel> shapes; + + const CanvasViewModel( + {required this.control, required this.child, required this.shapes}); + + static CanvasViewModel fromStore( + Store<AppState> store, Control control, List<Control> children) { + return CanvasViewModel( + control: control, + child: store.state.controls[control.id]!.childIds + .map((childId) => store.state.controls[childId]) + .whereNotNull() + .where((c) => c.name == "content" && c.isVisible) + .firstOrNull, + shapes: children + .where((c) => c.name != "content" && c.isVisible) + .map((c) => ControlTreeViewModel.fromStore(store, c)) + .toList()); + } + + @override + List<Object?> get props => [control, shapes]; +} typedef CanvasControlOnPaintCallback = void Function(Size size); @@ -39,7 +67,8 @@ class CanvasControl extends StatefulWidget { State<CanvasControl> createState() => _CanvasControlState(); } -class _CanvasControlState extends State<CanvasControl> { +class _CanvasControlState extends State<CanvasControl> + with FletControlStatefulMixin { int _lastResize = DateTime.now().millisecondsSinceEpoch; Size? _lastSize; @@ -71,11 +100,8 @@ class _CanvasControlState extends State<CanvasControl> { _lastSize == null) { _lastResize = now; _lastSize = size; - FletAppServices.of(context).server.sendPageEvent( - eventTarget: viewModel.control.id, - eventName: "resize", - eventData: - json.encode({"w": size.width, "h": size.height})); + sendControlEvent(viewModel.control.id, "resize", + json.encode({"w": size.width, "h": size.height})); } } }, diff --git a/package/lib/src/models/chart_axis_view_model.dart b/package/lib/src/controls/charts.dart similarity index 60% rename from package/lib/src/models/chart_axis_view_model.dart rename to package/lib/src/controls/charts.dart index bfd32655c..9e8c094a2 100644 --- a/package/lib/src/models/chart_axis_view_model.dart +++ b/package/lib/src/controls/charts.dart @@ -2,9 +2,29 @@ import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; import 'package:redux/redux.dart'; -import 'app_state.dart'; -import 'chart_axis_label_view_model.dart'; -import 'control.dart'; +import '../models/app_state.dart'; +import '../models/control.dart'; + +class ChartAxisLabelViewModel extends Equatable { + final double value; + final Control? control; + + const ChartAxisLabelViewModel({required this.value, required this.control}); + + static ChartAxisLabelViewModel fromStore( + Store<AppState> store, Control control) { + return ChartAxisLabelViewModel( + value: control.attrDouble("value")!, + control: store.state.controls[control.id]!.childIds + .map((childId) => store.state.controls[childId]) + .whereNotNull() + .where((c) => c.isVisible) + .firstOrNull); + } + + @override + List<Object?> get props => [value, control]; +} class ChartAxisViewModel extends Equatable { final Control control; diff --git a/package/lib/src/controls/checkbox.dart b/package/lib/src/controls/checkbox.dart index 38827c74c..c9a3134a6 100644 --- a/package/lib/src/controls/checkbox.dart +++ b/package/lib/src/controls/checkbox.dart @@ -1,14 +1,11 @@ -import 'package:flet/src/controls/cupertino_checkbox.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import '../actions.dart'; -import '../flet_app_services.dart'; import '../models/control.dart'; -import '../protocol/update_control_props_payload.dart'; -import '../utils/buttons.dart'; import '../utils/colors.dart'; import 'create_control.dart'; +import 'cupertino_checkbox.dart'; +import 'flet_control_stateful_mixin.dart'; +import 'flet_store_mixin.dart'; import 'list_tile.dart'; enum LabelPosition { right, left } @@ -17,20 +14,19 @@ class CheckboxControl extends StatefulWidget { final Control? parent; final Control control; final bool parentDisabled; - final dynamic dispatch; const CheckboxControl( {super.key, this.parent, required this.control, - required this.parentDisabled, - required this.dispatch}); + required this.parentDisabled}); @override State<CheckboxControl> createState() => _CheckboxControlState(); } -class _CheckboxControlState extends State<CheckboxControl> { +class _CheckboxControlState extends State<CheckboxControl> + with FletControlStatefulMixin, FletStoreMixin { bool? _value; bool _tristate = false; late final FocusNode _focusNode; @@ -43,10 +39,8 @@ class _CheckboxControlState extends State<CheckboxControl> { } void _onFocusChange() { - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, - eventName: _focusNode.hasFocus ? "focus" : "blur", - eventData: ""); + sendControlEvent( + widget.control.id, _focusNode.hasFocus ? "focus" : "blur", ""); } @override @@ -70,92 +64,84 @@ class _CheckboxControlState extends State<CheckboxControl> { void _onChange(bool? value) { var svalue = value != null ? value.toString() : ""; - setState(() { - _value = value; - }); - List<Map<String, String>> props = [ - {"i": widget.control.id, "value": svalue} - ]; - widget.dispatch( - UpdateControlPropsAction(UpdateControlPropsPayload(props: props))); - var server = FletAppServices.of(context).server; - server.updateControlProps(props: props); - server.sendPageEvent( - eventTarget: widget.control.id, eventName: "change", eventData: svalue); + _value = value; + updateControlProps(widget.control.id, {"value": svalue}); + sendControlEvent(widget.control.id, "change", svalue); } @override Widget build(BuildContext context) { debugPrint("Checkbox build: ${widget.control.id}"); - bool adaptive = widget.control.attrBool("adaptive", false)!; - if (adaptive && - (defaultTargetPlatform == TargetPlatform.iOS || - defaultTargetPlatform == TargetPlatform.macOS)) { - return CupertinoCheckboxControl( - control: widget.control, - parentDisabled: widget.parentDisabled, - dispatch: widget.dispatch); - } - - String label = widget.control.attrString("label", "")!; - LabelPosition labelPosition = LabelPosition.values.firstWhere( - (p) => - p.name.toLowerCase() == - widget.control.attrString("labelPosition", "")!.toLowerCase(), - orElse: () => LabelPosition.right); - _tristate = widget.control.attrBool("tristate", false)!; - bool autofocus = widget.control.attrBool("autofocus", false)!; - - bool disabled = widget.control.isDisabled || widget.parentDisabled; - - debugPrint("Checkbox StoreConnector build: ${widget.control.id}"); - bool? value = widget.control.attrBool("value", _tristate ? null : false); - if (_value != value) { - _value = value; - } - - var checkbox = Checkbox( - autofocus: autofocus, - focusNode: _focusNode, - value: _value, - activeColor: HexColor.fromString( - Theme.of(context), widget.control.attrString("activeColor", "")!), - focusColor: HexColor.fromString( - Theme.of(context), widget.control.attrString("focusColor", "")!), - hoverColor: HexColor.fromString( - Theme.of(context), widget.control.attrString("hoverColor", "")!), - overlayColor: parseMaterialStateColor( - Theme.of(context), widget.control, "overlayColor"), - checkColor: HexColor.fromString( - Theme.of(context), widget.control.attrString("checkColor", "")!), - fillColor: parseMaterialStateColor( - Theme.of(context), widget.control, "fillColor"), - tristate: _tristate, - onChanged: !disabled - ? (bool? value) { - _onChange(value); - } - : null); - - ListTileClicks.of(context)?.notifier.addListener(() { - _toggleValue(); + return withPagePlatform((context, platform) { + bool adaptive = widget.control.attrBool("adaptive", false)!; + if (adaptive && + (platform == TargetPlatform.iOS || + platform == TargetPlatform.macOS)) { + return CupertinoCheckboxControl( + control: widget.control, parentDisabled: widget.parentDisabled); + } + + String label = widget.control.attrString("label", "")!; + LabelPosition labelPosition = LabelPosition.values.firstWhere( + (p) => + p.name.toLowerCase() == + widget.control.attrString("labelPosition", "")!.toLowerCase(), + orElse: () => LabelPosition.right); + _tristate = widget.control.attrBool("tristate", false)!; + bool autofocus = widget.control.attrBool("autofocus", false)!; + + bool disabled = widget.control.isDisabled || widget.parentDisabled; + + debugPrint("Checkbox build: ${widget.control.id}"); + + bool? value = widget.control.attrBool("value", _tristate ? null : false); + if (_value != value) { + _value = value; + } + + var checkbox = Checkbox( + autofocus: autofocus, + focusNode: _focusNode, + value: _value, + activeColor: HexColor.fromString( + Theme.of(context), widget.control.attrString("activeColor", "")!), + focusColor: HexColor.fromString( + Theme.of(context), widget.control.attrString("focusColor", "")!), + hoverColor: HexColor.fromString( + Theme.of(context), widget.control.attrString("hoverColor", "")!), + overlayColor: parseMaterialStateColor( + Theme.of(context), widget.control, "overlayColor"), + checkColor: HexColor.fromString( + Theme.of(context), widget.control.attrString("checkColor", "")!), + fillColor: parseMaterialStateColor( + Theme.of(context), widget.control, "fillColor"), + tristate: _tristate, + onChanged: !disabled + ? (bool? value) { + _onChange(value); + } + : null); + + ListTileClicks.of(context)?.notifier.addListener(() { + _toggleValue(); + }); + + Widget result = checkbox; + if (label != "") { + var labelWidget = disabled + ? Text(label, + style: TextStyle(color: Theme.of(context).disabledColor)) + : MouseRegion(cursor: SystemMouseCursors.click, child: Text(label)); + result = MergeSemantics( + child: GestureDetector( + onTap: !disabled ? _toggleValue : null, + child: labelPosition == LabelPosition.right + ? Row(children: [checkbox, labelWidget]) + : Row(children: [labelWidget, checkbox]))); + } + + return constrainedControl(context, result, widget.parent, widget.control); }); - - Widget result = checkbox; - if (label != "") { - var labelWidget = disabled - ? Text(label, - style: TextStyle(color: Theme.of(context).disabledColor)) - : MouseRegion(cursor: SystemMouseCursors.click, child: Text(label)); - result = MergeSemantics( - child: GestureDetector( - onTap: !disabled ? _toggleValue : null, - child: labelPosition == LabelPosition.right - ? Row(children: [checkbox, labelWidget]) - : Row(children: [labelWidget, checkbox]))); - } - - return constrainedControl(context, result, widget.parent, widget.control); } } diff --git a/package/lib/src/controls/chip.dart b/package/lib/src/controls/chip.dart index 1d1b3ab3c..c3c5caccc 100644 --- a/package/lib/src/controls/chip.dart +++ b/package/lib/src/controls/chip.dart @@ -1,36 +1,33 @@ import 'package:flutter/material.dart'; import '../models/control.dart'; +import '../utils/borders.dart'; import '../utils/colors.dart'; -import 'create_control.dart'; -import 'error.dart'; -import 'package:flet/src/flet_app_services.dart'; -import '../actions.dart'; -import '../protocol/update_control_props_payload.dart'; import '../utils/edge_insets.dart'; import '../utils/text.dart'; -import '../utils/borders.dart'; +import 'create_control.dart'; +import 'error.dart'; +import 'flet_control_stateful_mixin.dart'; class ChipControl extends StatefulWidget { final Control? parent; final Control control; final List<Control> children; final bool parentDisabled; - final dynamic dispatch; const ChipControl( {super.key, this.parent, required this.control, required this.children, - required this.parentDisabled, - required this.dispatch}); + required this.parentDisabled}); @override State<ChipControl> createState() => _ChipControlState(); } -class _ChipControlState extends State<ChipControl> { +class _ChipControlState extends State<ChipControl> + with FletControlStatefulMixin { bool _selected = false; late final FocusNode _focusNode; @@ -52,27 +49,14 @@ class _ChipControlState extends State<ChipControl> { void _onSelect(bool selected) { var strSelected = selected.toString(); debugPrint(strSelected); - setState(() { - _selected = selected; - }); - List<Map<String, String>> props = [ - {"i": widget.control.id, "selected": strSelected} - ]; - widget.dispatch( - UpdateControlPropsAction(UpdateControlPropsPayload(props: props))); - final server = FletAppServices.of(context).server; - server.updateControlProps(props: props); - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "select", - eventData: strSelected); + _selected = selected; + updateControlProps(widget.control.id, {"selected": strSelected}); + sendControlEvent(widget.control.id, "select", strSelected); } void _onFocusChange() { - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, - eventName: _focusNode.hasFocus ? "focus" : "blur", - eventData: ""); + sendControlEvent( + widget.control.id, _focusNode.hasFocus ? "focus" : "blur", ""); } @override @@ -99,7 +83,6 @@ class _ChipControlState extends State<ChipControl> { var disabledColor = HexColor.fromString( Theme.of(context), widget.control.attrString("disabledColor", "")!); - final server = FletAppServices.of(context).server; bool onClick = widget.control.attrBool("onclick", false)!; bool onDelete = widget.control.attrBool("onDelete", false)!; bool onSelect = widget.control.attrBool("onSelect", false)!; @@ -123,20 +106,14 @@ class _ChipControlState extends State<ChipControl> { Function()? onClickHandler = onClick && !disabled ? () { debugPrint("Chip ${widget.control.id} clicked!"); - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "click", - eventData: ""); + sendControlEvent(widget.control.id, "click", ""); } : null; Function()? onDeleteHandler = onDelete && !disabled ? () { debugPrint("Chip ${widget.control.id} deleted!"); - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "delete", - eventData: ""); + sendControlEvent(widget.control.id, "delete", ""); } : null; diff --git a/package/lib/src/controls/clipboard.dart b/package/lib/src/controls/clipboard.dart index 2ed6b821d..961b74ae3 100644 --- a/package/lib/src/controls/clipboard.dart +++ b/package/lib/src/controls/clipboard.dart @@ -1,9 +1,8 @@ -import 'package:flet/src/flet_server.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; -import '../flet_app_services.dart'; import '../models/control.dart'; +import 'flet_control_stateful_mixin.dart'; class ClipboardControl extends StatefulWidget { final Control? parent; @@ -20,12 +19,11 @@ class ClipboardControl extends StatefulWidget { State<ClipboardControl> createState() => _ClipboardControlState(); } -class _ClipboardControlState extends State<ClipboardControl> { - FletServer? _server; - +class _ClipboardControlState extends State<ClipboardControl> + with FletControlStatefulMixin { @override void deactivate() { - _server?.controlInvokeMethods.remove(widget.control.id); + unsubscribeMethods(widget.control.id); super.deactivate(); } @@ -33,9 +31,7 @@ class _ClipboardControlState extends State<ClipboardControl> { Widget build(BuildContext context) { debugPrint("Clipboard build: ${widget.control.id}"); - _server = FletAppServices.of(context).server; - _server?.controlInvokeMethods[widget.control.id] = - (methodName, args) async { + subscribeMethods(widget.control.id, (methodName, args) async { switch (methodName) { case "set_data": await Clipboard.setData(ClipboardData(text: args["data"]!)); @@ -44,7 +40,7 @@ class _ClipboardControlState extends State<ClipboardControl> { return (await Clipboard.getData(Clipboard.kTextPlain))?.text; } return null; - }; + }); return widget.nextChild ?? const SizedBox.shrink(); } diff --git a/package/lib/src/controls/column.dart b/package/lib/src/controls/column.dart index 4b954c6fc..0ae50bc72 100644 --- a/package/lib/src/controls/column.dart +++ b/package/lib/src/controls/column.dart @@ -11,15 +11,13 @@ class ColumnControl extends StatelessWidget { final Control control; final bool parentDisabled; final List<Control> children; - final dynamic dispatch; const ColumnControl( {super.key, this.parent, required this.control, required this.children, - required this.parentDisabled, - required this.dispatch}); + required this.parentDisabled}); @override Widget build(BuildContext context) { @@ -73,7 +71,6 @@ class ColumnControl extends StatelessWidget { child = ScrollableControl( control: control, scrollDirection: wrap ? Axis.horizontal : Axis.vertical, - dispatch: dispatch, child: child, ); diff --git a/package/lib/src/controls/container.dart b/package/lib/src/controls/container.dart index e91a326da..9db69816f 100644 --- a/package/lib/src/controls/container.dart +++ b/package/lib/src/controls/container.dart @@ -2,15 +2,9 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:collection/collection.dart'; -import 'package:flet/src/utils/shadows.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_redux/flutter_redux.dart'; -import '../flet_app_services.dart'; -import '../models/app_state.dart'; import '../models/control.dart'; -import '../models/page_args_model.dart'; -import '../protocol/container_tap_event.dart'; import '../utils/alignment.dart'; import '../utils/animations.dart'; import '../utils/borders.dart'; @@ -19,10 +13,34 @@ import '../utils/edge_insets.dart'; import '../utils/gradient.dart'; import '../utils/images.dart'; import '../utils/launch_url.dart'; +import '../utils/shadows.dart'; import 'create_control.dart'; import 'error.dart'; +import 'flet_control_stateless_mixin.dart'; +import 'flet_store_mixin.dart'; -class ContainerControl extends StatelessWidget { +class ContainerTapEvent { + final double localX; + final double localY; + final double globalX; + final double globalY; + + ContainerTapEvent( + {required this.localX, + required this.localY, + required this.globalX, + required this.globalY}); + + Map<String, dynamic> toJson() => <String, dynamic>{ + 'lx': localX, + 'ly': localY, + 'gx': globalX, + 'gy': globalY + }; +} + +class ContainerControl extends StatelessWidget + with FletControlStatelessMixin, FletStoreMixin { final Control? parent; final Control control; final List<Control> children; @@ -64,249 +82,228 @@ class ContainerControl extends StatelessWidget { var animation = parseAnimation(control, "animate"); var blur = parseBlur(control, "blur"); - final server = FletAppServices.of(context).server; - - return StoreConnector<AppState, PageArgsModel>( - distinct: true, - converter: (store) => PageArgsModel.fromStore(store), - builder: (context, pageArgs) { - DecorationImage? image; + return withPageArgs((context, pageArgs) { + DecorationImage? image; - if (imageSrcBase64 != "") { - try { - Uint8List bytes = base64Decode(imageSrcBase64); - image = DecorationImage( - image: MemoryImage(bytes), - repeat: imageRepeat, - fit: imageFit, - opacity: imageOpacity); - } catch (ex) { - return ErrorControl("Error decoding base64: ${ex.toString()}"); - } - } else if (imageSrc != "") { - var assetSrc = - getAssetSrc(imageSrc, pageArgs.pageUri!, pageArgs.assetsDir); + if (imageSrcBase64 != "") { + try { + Uint8List bytes = base64Decode(imageSrcBase64); + image = DecorationImage( + image: MemoryImage(bytes), + repeat: imageRepeat, + fit: imageFit, + opacity: imageOpacity); + } catch (ex) { + return ErrorControl("Error decoding base64: ${ex.toString()}"); + } + } else if (imageSrc != "") { + var assetSrc = + getAssetSrc(imageSrc, pageArgs.pageUri!, pageArgs.assetsDir); - image = DecorationImage( - image: assetSrc.isFile - ? getFileImageProvider(assetSrc.path) - : NetworkImage(assetSrc.path), - repeat: imageRepeat, - fit: imageFit, - opacity: imageOpacity); - } + image = DecorationImage( + image: assetSrc.isFile + ? getFileImageProvider(assetSrc.path) + : NetworkImage(assetSrc.path), + repeat: imageRepeat, + fit: imageFit, + opacity: imageOpacity); + } - var gradient = parseGradient(Theme.of(context), control, "gradient"); - var blendMode = BlendMode.values.firstWhereOrNull((e) => + var gradient = parseGradient(Theme.of(context), control, "gradient"); + var blendMode = BlendMode.values.firstWhereOrNull((e) => + e.name.toLowerCase() == + control.attrString("blendMode", "")!.toLowerCase()); + var shape = BoxShape.values.firstWhere( + (e) => e.name.toLowerCase() == - control.attrString("blendMode", "")!.toLowerCase()); - var shape = BoxShape.values.firstWhere( - (e) => - e.name.toLowerCase() == - control.attrString("shape", "")!.toLowerCase(), - orElse: () => BoxShape.rectangle); + control.attrString("shape", "")!.toLowerCase(), + orElse: () => BoxShape.rectangle); - var borderRadius = parseBorderRadius(control, "borderRadius"); + var borderRadius = parseBorderRadius(control, "borderRadius"); - var clipBehavior = Clip.values.firstWhere( - (e) => - e.name.toLowerCase() == - control.attrString("clipBehavior", "")!.toLowerCase(), - orElse: () => borderRadius != null ? Clip.antiAlias : Clip.none); - - var boxDecor = BoxDecoration( - color: bgColor, - gradient: gradient, - image: image, - backgroundBlendMode: - bgColor != null || gradient != null ? blendMode : null, - border: parseBorder(Theme.of(context), control, "border"), - borderRadius: borderRadius, - shape: shape, - boxShadow: parseBoxShadow(Theme.of(context), control, "shadow")); + var clipBehavior = Clip.values.firstWhere( + (e) => + e.name.toLowerCase() == + control.attrString("clipBehavior", "")!.toLowerCase(), + orElse: () => borderRadius != null ? Clip.antiAlias : Clip.none); - Widget? result; + var boxDecor = BoxDecoration( + color: bgColor, + gradient: gradient, + image: image, + backgroundBlendMode: + bgColor != null || gradient != null ? blendMode : null, + border: parseBorder(Theme.of(context), control, "border"), + borderRadius: borderRadius, + shape: shape, + boxShadow: parseBoxShadow(Theme.of(context), control, "shadow")); - if ((onClick || url != "" || onLongPress || onHover) && - ink && - !disabled) { - var ink = Ink( - decoration: boxDecor, - child: InkWell( - // Dummy callback to enable widget - // see https://github.com/flutter/flutter/issues/50116#issuecomment-582047374 - // and https://github.com/flutter/flutter/blob/eed80afe2c641fb14b82a22279d2d78c19661787/packages/flutter/lib/src/material/ink_well.dart#L1125-L1129 - onTap: onHover ? () {} : null, - onTapDown: onClick || url != "" - ? (details) { - debugPrint("Container ${control.id} clicked!"); - if (url != "") { - openWebBrowser(url, webWindowName: urlTarget); - } - if (onClick) { - server.sendPageEvent( - eventTarget: control.id, - eventName: "click", - eventData: json.encode(ContainerTapEvent( - localX: details.localPosition.dx, - localY: details.localPosition.dy, - globalX: details.globalPosition.dx, - globalY: details.globalPosition.dy) - .toJson())); - } - } - : null, - onLongPress: onLongPress - ? () { - debugPrint("Container ${control.id} long pressed!"); - server.sendPageEvent( - eventTarget: control.id, - eventName: "long_press", - eventData: ""); - } - : null, - onHover: onHover - ? (value) { - debugPrint("Container ${control.id} hovered!"); - server.sendPageEvent( - eventTarget: control.id, - eventName: "hover", - eventData: value.toString()); - } - : null, - borderRadius: borderRadius, - child: Container( - padding: parseEdgeInsets(control, "padding"), - alignment: parseAlignment(control, "alignment"), - clipBehavior: Clip.none, - child: child, - ), - )); + Widget? result; - result = animation == null - ? Container( - width: control.attrDouble("width"), - height: control.attrDouble("height"), - margin: parseEdgeInsets(control, "margin"), - clipBehavior: Clip.none, - child: ink, - ) - : AnimatedContainer( - duration: animation.duration, - curve: animation.curve, - width: control.attrDouble("width"), - height: control.attrDouble("height"), - margin: parseEdgeInsets(control, "margin"), - clipBehavior: clipBehavior, - onEnd: control.attrBool("onAnimationEnd", false)! - ? () { - server.sendPageEvent( - eventTarget: control.id, - eventName: "animation_end", - eventData: "container"); - } - : null, - child: ink); - } else { - result = animation == null - ? Container( - width: control.attrDouble("width"), - height: control.attrDouble("height"), - padding: parseEdgeInsets(control, "padding"), - margin: parseEdgeInsets(control, "margin"), - alignment: parseAlignment(control, "alignment"), - decoration: boxDecor, - clipBehavior: clipBehavior, - child: child) - : AnimatedContainer( - duration: animation.duration, - curve: animation.curve, - width: control.attrDouble("width"), - height: control.attrDouble("height"), - padding: parseEdgeInsets(control, "padding"), - margin: parseEdgeInsets(control, "margin"), - alignment: parseAlignment(control, "alignment"), - decoration: boxDecor, - clipBehavior: clipBehavior, - onEnd: control.attrBool("onAnimationEnd", false)! - ? () { - server.sendPageEvent( - eventTarget: control.id, - eventName: "animation_end", - eventData: "container"); - } - : null, - child: child); + if ((onClick || url != "" || onLongPress || onHover) && + ink && + !disabled) { + var ink = Ink( + decoration: boxDecor, + child: InkWell( + // Dummy callback to enable widget + // see https://github.com/flutter/flutter/issues/50116#issuecomment-582047374 + // and https://github.com/flutter/flutter/blob/eed80afe2c641fb14b82a22279d2d78c19661787/packages/flutter/lib/src/material/ink_well.dart#L1125-L1129 + onTap: onHover ? () {} : null, + onTapDown: onClick || url != "" + ? (details) { + debugPrint("Container ${control.id} clicked!"); + if (url != "") { + openWebBrowser(url, webWindowName: urlTarget); + } + if (onClick) { + sendControlEvent( + context, + control.id, + "click", + json.encode(ContainerTapEvent( + localX: details.localPosition.dx, + localY: details.localPosition.dy, + globalX: details.globalPosition.dx, + globalY: details.globalPosition.dy) + .toJson())); + } + } + : null, + onLongPress: onLongPress + ? () { + debugPrint("Container ${control.id} long pressed!"); + sendControlEvent(context, control.id, "long_press", ""); + } + : null, + onHover: onHover + ? (value) { + debugPrint("Container ${control.id} hovered!"); + sendControlEvent( + context, control.id, "hover", value.toString()); + } + : null, + borderRadius: borderRadius, + child: Container( + padding: parseEdgeInsets(control, "padding"), + alignment: parseAlignment(control, "alignment"), + clipBehavior: Clip.none, + child: child, + ), + )); - if ((onClick || onLongPress || onHover || url != "") && !disabled) { - result = MouseRegion( - cursor: onClick || url != "" - ? SystemMouseCursors.click - : MouseCursor.defer, - onEnter: onHover - ? (value) { - debugPrint( - "Container's mouse region ${control.id} entered!"); - server.sendPageEvent( - eventTarget: control.id, - eventName: "hover", - eventData: "true"); + result = animation == null + ? Container( + width: control.attrDouble("width"), + height: control.attrDouble("height"), + margin: parseEdgeInsets(control, "margin"), + clipBehavior: Clip.none, + child: ink, + ) + : AnimatedContainer( + duration: animation.duration, + curve: animation.curve, + width: control.attrDouble("width"), + height: control.attrDouble("height"), + margin: parseEdgeInsets(control, "margin"), + clipBehavior: clipBehavior, + onEnd: control.attrBool("onAnimationEnd", false)! + ? () { + sendControlEvent( + context, control.id, "animation_end", "container"); } : null, - onExit: onHover - ? (value) { - debugPrint( - "Container's mouse region ${control.id} exited!"); - server.sendPageEvent( - eventTarget: control.id, - eventName: "hover", - eventData: "false"); + child: ink); + } else { + result = animation == null + ? Container( + width: control.attrDouble("width"), + height: control.attrDouble("height"), + padding: parseEdgeInsets(control, "padding"), + margin: parseEdgeInsets(control, "margin"), + alignment: parseAlignment(control, "alignment"), + decoration: boxDecor, + clipBehavior: clipBehavior, + child: child) + : AnimatedContainer( + duration: animation.duration, + curve: animation.curve, + width: control.attrDouble("width"), + height: control.attrDouble("height"), + padding: parseEdgeInsets(control, "padding"), + margin: parseEdgeInsets(control, "margin"), + alignment: parseAlignment(control, "alignment"), + decoration: boxDecor, + clipBehavior: clipBehavior, + onEnd: control.attrBool("onAnimationEnd", false)! + ? () { + sendControlEvent( + context, control.id, "animation_end", "container"); } : null, - child: GestureDetector( - onTapDown: onClick || url != "" - ? (details) { - debugPrint("Container ${control.id} clicked!"); - if (url != "") { - openWebBrowser(url, webWindowName: urlTarget); - } - if (onClick) { - server.sendPageEvent( - eventTarget: control.id, - eventName: "click", - eventData: json.encode(ContainerTapEvent( - localX: details.localPosition.dx, - localY: details.localPosition.dy, - globalX: details.globalPosition.dx, - globalY: details.globalPosition.dy) - .toJson())); - } - } - : null, - onLongPress: onLongPress - ? () { - debugPrint("Container ${control.id} clicked!"); - server.sendPageEvent( - eventTarget: control.id, - eventName: "long_press", - eventData: ""); - } - : null, - child: result, - ), - ); - } - } + child: child); + + if ((onClick || onLongPress || onHover || url != "") && !disabled) { + result = MouseRegion( + cursor: onClick || url != "" + ? SystemMouseCursors.click + : MouseCursor.defer, + onEnter: onHover + ? (value) { + debugPrint( + "Container's mouse region ${control.id} entered!"); + sendControlEvent(context, control.id, "hover", "true"); + } + : null, + onExit: onHover + ? (value) { + debugPrint( + "Container's mouse region ${control.id} exited!"); + sendControlEvent(context, control.id, "hover", "false"); + } + : null, + child: GestureDetector( + onTapDown: onClick || url != "" + ? (details) { + debugPrint("Container ${control.id} clicked!"); + if (url != "") { + openWebBrowser(url, webWindowName: urlTarget); + } + if (onClick) { + sendControlEvent( + context, + control.id, + "click", + json.encode(ContainerTapEvent( + localX: details.localPosition.dx, + localY: details.localPosition.dy, + globalX: details.globalPosition.dx, + globalY: details.globalPosition.dy) + .toJson())); + } + } + : null, + onLongPress: onLongPress + ? () { + debugPrint("Container ${control.id} clicked!"); + sendControlEvent(context, control.id, "long_press", ""); + } + : null, + child: result, + ), + ); + } + } - if (blur != null) { - result = borderRadius != null - ? ClipRRect( - borderRadius: borderRadius, - child: BackdropFilter(filter: blur, child: result)) - : ClipRect(child: BackdropFilter(filter: blur, child: result)); - } + if (blur != null) { + result = borderRadius != null + ? ClipRRect( + borderRadius: borderRadius, + child: BackdropFilter(filter: blur, child: result)) + : ClipRect(child: BackdropFilter(filter: blur, child: result)); + } - return constrainedControl(context, result, parent, control); - }); + return constrainedControl(context, result, parent, control); + }); } } diff --git a/package/lib/src/controls/create_control.dart b/package/lib/src/controls/create_control.dart index 0d6dc9bbb..ed67b6342 100644 --- a/package/lib/src/controls/create_control.dart +++ b/package/lib/src/controls/create_control.dart @@ -4,6 +4,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; +import '../control_factory.dart'; import '../flet_app_services.dart'; import '../flet_server.dart'; import '../models/app_state.dart'; @@ -133,8 +134,19 @@ Widget createControl(Control? parent, String id, bool parentDisabled, } } - // create control widget - var widget = createWidget(controlKey, controlView, parent, parentDisabled, + Widget? widget; + + for (var createControlFactory + in FletAppServices.of(context).createControlFactories) { + widget = createControlFactory(CreateControlArgs(controlKey, parent, + controlView.control, controlView.children, parentDisabled)); + if (widget != null) { + break; + } + } + + // try creating Flet built-in widget + widget ??= createWidget(controlKey, controlView, parent, parentDisabled, nextChild, FletAppServices.of(context).server); // no theme defined? return widget! @@ -153,7 +165,7 @@ Widget createControl(Control? parent, String id, bool parentDisabled, return Theme( data: parseTheme(controlView.control, "theme", brightness, parentTheme: parentTheme), - child: widget); + child: widget!); } if (themeMode == ThemeMode.system) { @@ -210,10 +222,7 @@ Widget createWidget(Key? key, ControlViewModel controlView, Control? parent, parentDisabled: parentDisabled); case "audio": return AudioControl( - parent: parent, - control: controlView.control, - dispatch: controlView.dispatch, - nextChild: nextChild); + parent: parent, control: controlView.control, nextChild: nextChild); case "divider": return DividerControl( key: key, parent: parent, control: controlView.control); @@ -258,16 +267,14 @@ Widget createWidget(Key? key, ControlViewModel controlView, Control? parent, parent: parent, control: controlView.control, children: controlView.children, - parentDisabled: parentDisabled, - dispatch: controlView.dispatch); + parentDisabled: parentDisabled); case "pagelet": return PageletControl( key: key, parent: parent, control: controlView.control, children: controlView.children, - parentDisabled: parentDisabled, - dispatch: controlView.dispatch); + parentDisabled: parentDisabled); case "progressring": return ProgressRingControl( key: key, parent: parent, control: controlView.control); @@ -325,21 +332,18 @@ Widget createWidget(Key? key, ControlViewModel controlView, Control? parent, parentDisabled: parentDisabled); case "column": return ColumnControl( - key: key, - parent: parent, - control: controlView.control, - children: controlView.children, - parentDisabled: parentDisabled, - dispatch: controlView.dispatch, - ); + key: key, + parent: parent, + control: controlView.control, + children: controlView.children, + parentDisabled: parentDisabled); case "row": return RowControl( key: key, parent: parent, control: controlView.control, children: controlView.children, - parentDisabled: parentDisabled, - dispatch: controlView.dispatch); + parentDisabled: parentDisabled); case "responsiverow": return ResponsiveRowControl( key: key, @@ -374,16 +378,14 @@ Widget createWidget(Key? key, ControlViewModel controlView, Control? parent, parent: parent, control: controlView.control, children: controlView.children, - parentDisabled: parentDisabled, - dispatch: controlView.dispatch); + parentDisabled: parentDisabled); case "expansionpanellist": return ExpansionPanelListControl( key: key, parent: parent, control: controlView.control, children: controlView.children, - parentDisabled: parentDisabled, - dispatch: controlView.dispatch); + parentDisabled: parentDisabled); case "stack": return StackControl( key: key, @@ -404,7 +406,6 @@ Widget createWidget(Key? key, ControlViewModel controlView, Control? parent, control: controlView.control, children: controlView.children, parentDisabled: parentDisabled, - dispatch: controlView.dispatch, ); case "timepicker": return TimePickerControl( @@ -412,7 +413,6 @@ Widget createWidget(Key? key, ControlViewModel controlView, Control? parent, control: controlView.control, children: controlView.children, parentDisabled: parentDisabled, - dispatch: controlView.dispatch, ); case "draggable": return DraggableControl( @@ -511,17 +511,14 @@ Widget createWidget(Key? key, ControlViewModel controlView, Control? parent, parent: parent, control: controlView.control, children: controlView.children, - parentDisabled: parentDisabled, - dispatch: controlView.dispatch); + parentDisabled: parentDisabled); case "gridview": return GridViewControl( - key: key, - parent: parent, - control: controlView.control, - children: controlView.children, - parentDisabled: parentDisabled, - dispatch: controlView.dispatch, - ); + key: key, + parent: parent, + control: controlView.control, + children: controlView.children, + parentDisabled: parentDisabled); case "textfield": return TextFieldControl( key: key, @@ -542,60 +539,49 @@ Widget createWidget(Key? key, ControlViewModel controlView, Control? parent, parent: parent, control: controlView.control, children: controlView.children, - parentDisabled: parentDisabled, - dispatch: controlView.dispatch); + parentDisabled: parentDisabled); case "checkbox": return CheckboxControl( key: key, parent: parent, control: controlView.control, - parentDisabled: parentDisabled, - dispatch: controlView.dispatch); + parentDisabled: parentDisabled); case "cupertinocheckbox": return CupertinoCheckboxControl( key: key, parent: parent, control: controlView.control, - parentDisabled: parentDisabled, - dispatch: controlView.dispatch); + parentDisabled: parentDisabled); case "switch": return SwitchControl( key: key, parent: parent, control: controlView.control, - parentDisabled: parentDisabled, - dispatch: controlView.dispatch); + parentDisabled: parentDisabled); case "cupertinoswitch": return CupertinoSwitchControl( key: key, parent: parent, control: controlView.control, - parentDisabled: parentDisabled, - dispatch: controlView.dispatch); + parentDisabled: parentDisabled); case "slider": return SliderControl( - key: key, - parent: parent, - control: controlView.control, - parentDisabled: parentDisabled, - dispatch: controlView.dispatch, - ); + key: key, + parent: parent, + control: controlView.control, + parentDisabled: parentDisabled); case "cupertinoslider": return CupertinoSliderControl( - key: key, - parent: parent, - control: controlView.control, - parentDisabled: parentDisabled, - dispatch: controlView.dispatch, - ); + key: key, + parent: parent, + control: controlView.control, + parentDisabled: parentDisabled); case "rangeslider": return RangeSliderControl( - key: key, - parent: parent, - control: controlView.control, - parentDisabled: parentDisabled, - dispatch: controlView.dispatch, - ); + key: key, + parent: parent, + control: controlView.control, + parentDisabled: parentDisabled); case "radiogroup": return RadioGroupControl( key: key, @@ -608,22 +594,19 @@ Widget createWidget(Key? key, ControlViewModel controlView, Control? parent, key: key, parent: parent, control: controlView.control, - parentDisabled: parentDisabled, - dispatch: controlView.dispatch); + parentDisabled: parentDisabled); case "cupertinoradio": return CupertinoRadioControl( key: key, parent: parent, control: controlView.control, - parentDisabled: parentDisabled, - dispatch: controlView.dispatch); + parentDisabled: parentDisabled); case "dropdown": return DropdownControl( key: key, parent: parent, control: controlView.control, - parentDisabled: parentDisabled, - dispatch: controlView.dispatch); + parentDisabled: parentDisabled); case "snackbar": return SnackBarControl( parent: parent, @@ -636,8 +619,7 @@ Widget createWidget(Key? key, ControlViewModel controlView, Control? parent, parent: parent, control: controlView.control, children: controlView.children, - parentDisabled: parentDisabled, - server: server); + parentDisabled: parentDisabled); case "alertdialog": return AlertDialogControl( parent: parent, @@ -658,7 +640,6 @@ Widget createWidget(Key? key, ControlViewModel controlView, Control? parent, control: controlView.control, children: controlView.children, parentDisabled: parentDisabled, - dispatch: controlView.dispatch, nextChild: nextChild); case "banner": return BannerControl( @@ -673,30 +654,26 @@ Widget createWidget(Key? key, ControlViewModel controlView, Control? parent, parent: parent, control: controlView.control, children: controlView.children, - parentDisabled: parentDisabled, - dispatch: controlView.dispatch); + parentDisabled: parentDisabled); case "navigationrail": return NavigationRailControl( key: key, parent: parent, control: controlView.control, children: controlView.children, - parentDisabled: parentDisabled, - dispatch: controlView.dispatch); + parentDisabled: parentDisabled); case "navigationbar": return NavigationBarControl( parent: parent, control: controlView.control, children: controlView.children, - parentDisabled: parentDisabled, - dispatch: controlView.dispatch); + parentDisabled: parentDisabled); case "cupertinonavigationbar": return CupertinoNavigationBarControl( parent: parent, control: controlView.control, children: controlView.children, - parentDisabled: parentDisabled, - dispatch: controlView.dispatch); + parentDisabled: parentDisabled); case "bottomappbar": return BottomAppBarControl( parent: parent, diff --git a/package/lib/src/controls/cupertino_alert_dialog.dart b/package/lib/src/controls/cupertino_alert_dialog.dart index 91d09c8f9..8dee779a8 100644 --- a/package/lib/src/controls/cupertino_alert_dialog.dart +++ b/package/lib/src/controls/cupertino_alert_dialog.dart @@ -1,14 +1,10 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_redux/flutter_redux.dart'; -import '../actions.dart'; -import '../flet_app_services.dart'; -import '../models/app_state.dart'; import '../models/control.dart'; -import '../protocol/update_control_props_payload.dart'; import 'create_control.dart'; import 'error.dart'; +import 'flet_control_stateful_mixin.dart'; class CupertinoAlertDialogControl extends StatefulWidget { final Control? parent; @@ -31,7 +27,7 @@ class CupertinoAlertDialogControl extends StatefulWidget { } class _CupertinoAlertDialogControlState - extends State<CupertinoAlertDialogControl> { + extends State<CupertinoAlertDialogControl> with FletControlStatefulMixin { Widget _createCupertinoAlertDialog() { bool disabled = widget.control.isDisabled || widget.parentDisabled; var titleCtrls = @@ -62,66 +58,50 @@ class _CupertinoAlertDialogControlState Widget build(BuildContext context) { debugPrint("CupertinoAlertDialog build ($hashCode): ${widget.control.id}"); - var server = FletAppServices.of(context).server; - bool lastOpen = widget.control.state["open"] ?? false; - return StoreConnector<AppState, Function>( - distinct: true, - converter: (store) => store.dispatch, - builder: (context, dispatch) { - debugPrint( - "CupertinoAlertDialog StoreConnector build: ${widget.control.id}"); - - var open = widget.control.attrBool("open", false)!; - var modal = widget.control.attrBool("modal", false)!; - - debugPrint("Current open state: $lastOpen"); - debugPrint("New open state: $open"); - - if (open && (open != lastOpen)) { - var dialog = _createCupertinoAlertDialog(); - if (dialog is ErrorControl) { - return dialog; - } - - // close previous dialog - if (ModalRoute.of(context)?.isCurrent != true) { - Navigator.of(context).pop(); - } - - widget.control.state["open"] = open; - - WidgetsBinding.instance.addPostFrameCallback((_) { - showDialog( - barrierDismissible: !modal, - useRootNavigator: false, - context: context, - builder: (context) => dialog).then((value) { - lastOpen = widget.control.state["open"] ?? false; - debugPrint("Dialog should be dismissed ($hashCode): $lastOpen"); - bool shouldDismiss = lastOpen; - widget.control.state["open"] = false; - - if (shouldDismiss) { - List<Map<String, String>> props = [ - {"i": widget.control.id, "open": "false"} - ]; - dispatch(UpdateControlPropsAction( - UpdateControlPropsPayload(props: props))); - server.updateControlProps(props: props); - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "dismiss", - eventData: ""); - } - }); - }); - } else if (open != lastOpen && lastOpen) { - Navigator.of(context).pop(); + debugPrint("CupertinoAlertDialog build: ${widget.control.id}"); + + var open = widget.control.attrBool("open", false)!; + var modal = widget.control.attrBool("modal", false)!; + + debugPrint("Current open state: $lastOpen"); + debugPrint("New open state: $open"); + + if (open && (open != lastOpen)) { + var dialog = _createCupertinoAlertDialog(); + if (dialog is ErrorControl) { + return dialog; + } + + // close previous dialog + if (ModalRoute.of(context)?.isCurrent != true) { + Navigator.of(context).pop(); + } + + widget.control.state["open"] = open; + + WidgetsBinding.instance.addPostFrameCallback((_) { + showDialog( + barrierDismissible: !modal, + useRootNavigator: false, + context: context, + builder: (context) => dialog).then((value) { + lastOpen = widget.control.state["open"] ?? false; + debugPrint("Dialog should be dismissed ($hashCode): $lastOpen"); + bool shouldDismiss = lastOpen; + widget.control.state["open"] = false; + + if (shouldDismiss) { + updateControlProps(widget.control.id, {"open": "false"}); + sendControlEvent(widget.control.id, "dismiss", ""); } - - return widget.nextChild ?? const SizedBox.shrink(); }); + }); + } else if (open != lastOpen && lastOpen) { + Navigator.of(context).pop(); + } + + return widget.nextChild ?? const SizedBox.shrink(); } } diff --git a/package/lib/src/controls/cupertino_checkbox.dart b/package/lib/src/controls/cupertino_checkbox.dart index b669b73bd..822fa4760 100644 --- a/package/lib/src/controls/cupertino_checkbox.dart +++ b/package/lib/src/controls/cupertino_checkbox.dart @@ -1,12 +1,10 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import '../actions.dart'; -import '../flet_app_services.dart'; import '../models/control.dart'; -import '../protocol/update_control_props_payload.dart'; import '../utils/colors.dart'; import 'create_control.dart'; +import 'flet_control_stateful_mixin.dart'; import 'list_tile.dart'; enum LabelPosition { right, left } @@ -15,20 +13,19 @@ class CupertinoCheckboxControl extends StatefulWidget { final Control? parent; final Control control; final bool parentDisabled; - final dynamic dispatch; const CupertinoCheckboxControl( {super.key, this.parent, required this.control, - required this.parentDisabled, - required this.dispatch}); + required this.parentDisabled}); @override State<CupertinoCheckboxControl> createState() => _CheckboxControlState(); } -class _CheckboxControlState extends State<CupertinoCheckboxControl> { +class _CheckboxControlState extends State<CupertinoCheckboxControl> + with FletControlStatefulMixin { bool? _value; bool _tristate = false; late final FocusNode _focusNode; @@ -41,10 +38,8 @@ class _CheckboxControlState extends State<CupertinoCheckboxControl> { } void _onFocusChange() { - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, - eventName: _focusNode.hasFocus ? "focus" : "blur", - eventData: ""); + sendControlEvent( + widget.control.id, _focusNode.hasFocus ? "focus" : "blur", ""); } @override @@ -68,18 +63,9 @@ class _CheckboxControlState extends State<CupertinoCheckboxControl> { void _onChange(bool? value) { var svalue = value != null ? value.toString() : ""; - setState(() { - _value = value; - }); - List<Map<String, String>> props = [ - {"i": widget.control.id, "value": svalue} - ]; - widget.dispatch( - UpdateControlPropsAction(UpdateControlPropsPayload(props: props))); - var server = FletAppServices.of(context).server; - server.updateControlProps(props: props); - server.sendPageEvent( - eventTarget: widget.control.id, eventName: "change", eventData: svalue); + _value = value; + updateControlProps(widget.control.id, {"value": svalue}); + sendControlEvent(widget.control.id, "change", svalue); } @override @@ -96,7 +82,7 @@ class _CheckboxControlState extends State<CupertinoCheckboxControl> { bool autofocus = widget.control.attrBool("autofocus", false)!; bool disabled = widget.control.isDisabled || widget.parentDisabled; - debugPrint("CupertinoCheckbox StoreConnector build: ${widget.control.id}"); + debugPrint("CupertinoCheckbox build: ${widget.control.id}"); bool? value = widget.control.attrBool("value", _tristate ? null : false); if (_value != value) { diff --git a/package/lib/src/controls/cupertino_dialog_action.dart b/package/lib/src/controls/cupertino_dialog_action.dart index 4c14ac05a..bc08b2813 100644 --- a/package/lib/src/controls/cupertino_dialog_action.dart +++ b/package/lib/src/controls/cupertino_dialog_action.dart @@ -1,31 +1,29 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import '../flet_app_services.dart'; import '../models/control.dart'; import '../utils/text.dart'; import 'create_control.dart'; +import 'flet_control_stateless_mixin.dart'; -class CupertinoDialogActionControl extends StatelessWidget { +class CupertinoDialogActionControl extends StatelessWidget + with FletControlStatelessMixin { final Control? parent; final Control control; final List<Control> children; final bool parentDisabled; const CupertinoDialogActionControl( - {Key? key, + {super.key, this.parent, required this.control, required this.children, - required this.parentDisabled}) - : super(key: key); + required this.parentDisabled}); @override Widget build(BuildContext context) { debugPrint("CupertinoDialogAction build: ${control.id}"); - final server = FletAppServices.of(context).server; - String text = control.attrString("text", "")!; var contentCtrls = children.where((c) => c.name == "content"); bool isDefaultAction = control.attrBool("isDefaultAction", false)!; @@ -35,8 +33,7 @@ class CupertinoDialogActionControl extends StatelessWidget { Function()? onPressed = !disabled ? () { debugPrint("CupertinoDialogAction ${control.id} clicked!"); - server.sendPageEvent( - eventTarget: control.id, eventName: "click", eventData: ""); + sendControlEvent(context, control.id, "click", ""); } : null; diff --git a/package/lib/src/controls/cupertino_navigation_bar.dart b/package/lib/src/controls/cupertino_navigation_bar.dart index 2dfa2bb9b..e57fc7f49 100644 --- a/package/lib/src/controls/cupertino_navigation_bar.dart +++ b/package/lib/src/controls/cupertino_navigation_bar.dart @@ -1,32 +1,26 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_redux/flutter_redux.dart'; -import '../actions.dart'; -import '../flet_app_services.dart'; -import '../models/app_state.dart'; import '../models/control.dart'; -import '../models/controls_view_model.dart'; -import '../protocol/update_control_props_payload.dart'; import '../utils/borders.dart'; import '../utils/colors.dart'; import '../utils/icons.dart'; import 'create_control.dart'; +import 'flet_control_stateful_mixin.dart'; +import 'flet_store_mixin.dart'; class CupertinoNavigationBarControl extends StatefulWidget { final Control? parent; final Control control; final List<Control> children; final bool parentDisabled; - final dynamic dispatch; const CupertinoNavigationBarControl( {super.key, this.parent, required this.control, required this.children, - required this.parentDisabled, - required this.dispatch}); + required this.parentDisabled}); @override State<CupertinoNavigationBarControl> createState() => @@ -34,23 +28,16 @@ class CupertinoNavigationBarControl extends StatefulWidget { } class _CupertinoNavigationBarControlState - extends State<CupertinoNavigationBarControl> { + extends State<CupertinoNavigationBarControl> + with FletControlStatefulMixin, FletStoreMixin { int _selectedIndex = 0; void _onTap(int index) { _selectedIndex = index; debugPrint("Selected index: $_selectedIndex"); - List<Map<String, String>> props = [ - {"i": widget.control.id, "selectedindex": _selectedIndex.toString()} - ]; - widget.dispatch( - UpdateControlPropsAction(UpdateControlPropsPayload(props: props))); - final server = FletAppServices.of(context).server; - server.updateControlProps(props: props); - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "change", - eventData: _selectedIndex.toString()); + updateControlProps( + widget.control.id, {"selectedindex": _selectedIndex.toString()}); + sendControlEvent(widget.control.id, "change", _selectedIndex.toString()); } @override @@ -64,53 +51,49 @@ class _CupertinoNavigationBarControlState _selectedIndex = selectedIndex; } - var navBar = StoreConnector<AppState, ControlsViewModel>( - distinct: true, - converter: (store) => ControlsViewModel.fromStore( - store, - widget.children - .where((c) => c.isVisible && c.name == null) - .map((c) => c.id)), - builder: (content, viewModel) { - return CupertinoTabBar( - backgroundColor: HexColor.fromString( - Theme.of(context), widget.control.attrString("bgColor", "")!), - activeColor: HexColor.fromString( - Theme.of(context), widget.control.attrString("activeColor", "")!), - inactiveColor: HexColor.fromString(Theme.of(context), - widget.control.attrString("inactiveColor", "")!) ?? CupertinoColors.inactiveGray, - iconSize: widget.control.attrDouble("iconSize", 30.0)!, - currentIndex: _selectedIndex, - border: parseBorder(Theme.of(context), widget.control, "border"), - onTap: _onTap, - items: viewModel.controlViews.map((destView) { - var label = destView.control.attrString("label", "")!; + var navBar = withControls( + widget.children + .where((c) => c.isVisible && c.name == null) + .map((c) => c.id), (content, viewModel) { + return CupertinoTabBar( + backgroundColor: HexColor.fromString( + Theme.of(context), widget.control.attrString("bgColor", "")!), + activeColor: HexColor.fromString( + Theme.of(context), widget.control.attrString("activeColor", "")!), + inactiveColor: HexColor.fromString(Theme.of(context), + widget.control.attrString("inactiveColor", "")!) ?? + CupertinoColors.inactiveGray, + iconSize: widget.control.attrDouble("iconSize", 30.0)!, + currentIndex: _selectedIndex, + border: parseBorder(Theme.of(context), widget.control, "border"), + onTap: _onTap, + items: viewModel.controlViews.map((destView) { + var label = destView.control.attrString("label", "")!; - var icon = - parseIcon(destView.control.attrString("icon", "")!); - var iconContentCtrls = - destView.children.where((c) => c.name == "icon_content"); + var icon = parseIcon(destView.control.attrString("icon", "")!); + var iconContentCtrls = + destView.children.where((c) => c.name == "icon_content"); - var selectedIcon = parseIcon( - destView.control.attrString("selectedIcon", "")!); - var selectedIconContentCtrls = destView.children - .where((c) => c.name == "selected_icon_content"); + var selectedIcon = + parseIcon(destView.control.attrString("selectedIcon", "")!); + var selectedIconContentCtrls = destView.children + .where((c) => c.name == "selected_icon_content"); - return BottomNavigationBarItem( - tooltip: destView.control.attrString("tooltip", "")!, - icon: iconContentCtrls.isNotEmpty - ? createControl(destView.control, - iconContentCtrls.first.id, disabled) - : Icon(icon), - activeIcon: selectedIconContentCtrls.isNotEmpty - ? createControl(destView.control, - selectedIconContentCtrls.first.id, disabled) - : selectedIcon != null - ? Icon(selectedIcon) - : null, - label: label); - }).toList()); - }); + return BottomNavigationBarItem( + tooltip: destView.control.attrString("tooltip", "")!, + icon: iconContentCtrls.isNotEmpty + ? createControl( + destView.control, iconContentCtrls.first.id, disabled) + : Icon(icon), + activeIcon: selectedIconContentCtrls.isNotEmpty + ? createControl(destView.control, + selectedIconContentCtrls.first.id, disabled) + : selectedIcon != null + ? Icon(selectedIcon) + : null, + label: label); + }).toList()); + }); return constrainedControl(context, navBar, widget.parent, widget.control); } diff --git a/package/lib/src/controls/cupertino_radio.dart b/package/lib/src/controls/cupertino_radio.dart index f36eeff36..5e75104c0 100644 --- a/package/lib/src/controls/cupertino_radio.dart +++ b/package/lib/src/controls/cupertino_radio.dart @@ -1,16 +1,12 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_redux/flutter_redux.dart'; -import '../actions.dart'; -import '../flet_app_services.dart'; -import '../models/app_state.dart'; import '../models/control.dart'; -import '../models/control_ancestor_view_model.dart'; -import '../protocol/update_control_props_payload.dart'; import '../utils/colors.dart'; import 'create_control.dart'; import 'error.dart'; +import 'flet_control_stateful_mixin.dart'; +import 'flet_store_mixin.dart'; import 'list_tile.dart'; enum LabelPosition { right, left } @@ -19,20 +15,19 @@ class CupertinoRadioControl extends StatefulWidget { final Control? parent; final Control control; final bool parentDisabled; - final dynamic dispatch; const CupertinoRadioControl( {super.key, this.parent, required this.control, - required this.parentDisabled, - required this.dispatch}); + required this.parentDisabled}); @override State<CupertinoRadioControl> createState() => _CupertinoRadioControlState(); } -class _CupertinoRadioControlState extends State<CupertinoRadioControl> { +class _CupertinoRadioControlState extends State<CupertinoRadioControl> + with FletControlStatefulMixin, FletStoreMixin { late final FocusNode _focusNode; @override @@ -43,10 +38,8 @@ class _CupertinoRadioControlState extends State<CupertinoRadioControl> { } void _onFocusChange() { - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, - eventName: _focusNode.hasFocus ? "focus" : "blur", - eventData: ""); + sendControlEvent( + widget.control.id, _focusNode.hasFocus ? "focus" : "blur", ""); } @override @@ -59,16 +52,8 @@ class _CupertinoRadioControlState extends State<CupertinoRadioControl> { void _onChange(String ancestorId, String? value) { var svalue = value ?? ""; debugPrint(svalue); - List<Map<String, String>> props = [ - {"i": ancestorId, "value": svalue} - ]; - widget.dispatch( - UpdateControlPropsAction(UpdateControlPropsPayload(props: props))); - - final server = FletAppServices.of(context).server; - server.updateControlProps(props: props); - server.sendPageEvent( - eventTarget: ancestorId, eventName: "change", eventData: svalue); + updateControlProps(ancestorId, {"value": svalue}); + sendControlEvent(ancestorId, "change", svalue); } @override @@ -85,69 +70,60 @@ class _CupertinoRadioControlState extends State<CupertinoRadioControl> { bool autofocus = widget.control.attrBool("autofocus", false)!; bool disabled = widget.control.isDisabled || widget.parentDisabled; - return StoreConnector<AppState, ControlAncestorViewModel>( - distinct: true, - ignoreChange: (state) { - return state.controls[widget.control.id] == null; - }, - converter: (store) => ControlAncestorViewModel.fromStore( - store, widget.control.id, "radiogroup"), - builder: (context, viewModel) { - debugPrint( - "CupertinoRadio StoreConnector build: ${widget.control.id}"); - - if (viewModel.ancestor == null) { - return const ErrorControl( - "CupertinoRadio control must be enclosed with RadioGroup."); - } - - String groupValue = viewModel.ancestor!.attrString("value", "")!; - String ancestorId = viewModel.ancestor!.id; - - var cupertinoRadio = CupertinoRadio<String>( - autofocus: autofocus, - focusNode: _focusNode, - groupValue: groupValue, - value: value, - useCheckmarkStyle: - widget.control.attrBool("useCheckmarkStyle", false)!, - fillColor: HexColor.fromString(Theme.of(context), - widget.control.attrString("fillColor", "")!), - activeColor: HexColor.fromString(Theme.of(context), - widget.control.attrString("activeColor", "")!), - inactiveColor: HexColor.fromString(Theme.of(context), - widget.control.attrString("inactiveColor", "")!), - onChanged: !disabled - ? (String? value) { - _onChange(ancestorId, value); - } - : null); - - ListTileClicks.of(context)?.notifier.addListener(() { - _onChange(ancestorId, value); - }); - - Widget result = cupertinoRadio; - if (label != "") { - var labelWidget = disabled - ? Text(label, - style: TextStyle(color: Theme.of(context).disabledColor)) - : MouseRegion( - cursor: SystemMouseCursors.click, child: Text(label)); - result = MergeSemantics( - child: GestureDetector( - onTap: !disabled - ? () { - _onChange(ancestorId, value); - } - : null, - child: labelPosition == LabelPosition.right - ? Row(children: [cupertinoRadio, labelWidget]) - : Row(children: [labelWidget, cupertinoRadio]))); - } - - return constrainedControl( - context, result, widget.parent, widget.control); - }); + return withControlAncestor(widget.control.id, "radiogroup", + (context, viewModel) { + debugPrint("CupertinoRadio build: ${widget.control.id}"); + + if (viewModel.ancestor == null) { + return const ErrorControl( + "CupertinoRadio control must be enclosed with RadioGroup."); + } + + String groupValue = viewModel.ancestor!.attrString("value", "")!; + String ancestorId = viewModel.ancestor!.id; + + var cupertinoRadio = CupertinoRadio<String>( + autofocus: autofocus, + focusNode: _focusNode, + groupValue: groupValue, + value: value, + useCheckmarkStyle: + widget.control.attrBool("useCheckmarkStyle", false)!, + fillColor: HexColor.fromString( + Theme.of(context), widget.control.attrString("fillColor", "")!), + activeColor: HexColor.fromString( + Theme.of(context), widget.control.attrString("activeColor", "")!), + inactiveColor: HexColor.fromString(Theme.of(context), + widget.control.attrString("inactiveColor", "")!), + onChanged: !disabled + ? (String? value) { + _onChange(ancestorId, value); + } + : null); + + ListTileClicks.of(context)?.notifier.addListener(() { + _onChange(ancestorId, value); + }); + + Widget result = cupertinoRadio; + if (label != "") { + var labelWidget = disabled + ? Text(label, + style: TextStyle(color: Theme.of(context).disabledColor)) + : MouseRegion(cursor: SystemMouseCursors.click, child: Text(label)); + result = MergeSemantics( + child: GestureDetector( + onTap: !disabled + ? () { + _onChange(ancestorId, value); + } + : null, + child: labelPosition == LabelPosition.right + ? Row(children: [cupertinoRadio, labelWidget]) + : Row(children: [labelWidget, cupertinoRadio]))); + } + + return constrainedControl(context, result, widget.parent, widget.control); + }); } } diff --git a/package/lib/src/controls/cupertino_slider.dart b/package/lib/src/controls/cupertino_slider.dart index a88bb445e..464e50ee1 100644 --- a/package/lib/src/controls/cupertino_slider.dart +++ b/package/lib/src/controls/cupertino_slider.dart @@ -1,32 +1,30 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import '../actions.dart'; -import '../flet_app_services.dart'; + import '../models/control.dart'; -import '../protocol/update_control_props_payload.dart'; import '../utils/colors.dart'; -import '../utils/desktop.dart'; import '../utils/debouncer.dart'; +import '../utils/desktop.dart'; import 'create_control.dart'; +import 'flet_control_stateful_mixin.dart'; class CupertinoSliderControl extends StatefulWidget { final Control? parent; final Control control; final bool parentDisabled; - final dynamic dispatch; const CupertinoSliderControl( {super.key, this.parent, required this.control, - required this.parentDisabled, - required this.dispatch}); + required this.parentDisabled}); @override State<CupertinoSliderControl> createState() => _CupertinoSliderControlState(); } -class _CupertinoSliderControlState extends State<CupertinoSliderControl> { +class _CupertinoSliderControlState extends State<CupertinoSliderControl> + with FletControlStatefulMixin { double _value = 0; final _debouncer = Debouncer(milliseconds: isDesktop() ? 10 : 100); @@ -39,21 +37,12 @@ class _CupertinoSliderControlState extends State<CupertinoSliderControl> { void onChange(double value) { var svalue = value.toString(); debugPrint(svalue); - setState(() { - _value = value; - }); - - List<Map<String, String>> props = [ - {"i": widget.control.id, "value": svalue} - ]; - widget.dispatch( - UpdateControlPropsAction(UpdateControlPropsPayload(props: props))); - + _value = value; + var props = {"value": svalue}; + updateControlProps(widget.control.id, props, clientOnly: true); _debouncer.run(() { - final server = FletAppServices.of(context).server; - server.updateControlProps(props: props); - server.sendPageEvent( - eventTarget: widget.control.id, eventName: "change", eventData: ''); + updateControlProps(widget.control.id, props); + sendControlEvent(widget.control.id, "change", ''); }); } @@ -67,10 +56,7 @@ class _CupertinoSliderControlState extends State<CupertinoSliderControl> { double max = widget.control.attrDouble("max", 1)!; int? divisions = widget.control.attrInt("divisions"); - final server = FletAppServices.of(context).server; - - debugPrint( - "CupertinoSliderControl StoreConnector build: ${widget.control.id}"); + debugPrint("CupertinoSliderControl build: ${widget.control.id}"); double value = widget.control.attrDouble("value", 0)!; if (_value != value) { @@ -101,18 +87,14 @@ class _CupertinoSliderControlState extends State<CupertinoSliderControl> { : null, onChangeStart: !disabled ? (double value) { - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "change_start", - eventData: value.toString()); + sendControlEvent( + widget.control.id, "change_start", value.toString()); } : null, onChangeEnd: !disabled ? (double value) { - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "change_end", - eventData: value.toString()); + sendControlEvent( + widget.control.id, "change_end", value.toString()); } : null); diff --git a/package/lib/src/controls/cupertino_switch.dart b/package/lib/src/controls/cupertino_switch.dart index cccf9c6c0..704494bd5 100644 --- a/package/lib/src/controls/cupertino_switch.dart +++ b/package/lib/src/controls/cupertino_switch.dart @@ -1,15 +1,10 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_redux/flutter_redux.dart'; -import '../actions.dart'; -import '../flet_app_services.dart'; -import '../models/app_state.dart'; import '../models/control.dart'; -import '../protocol/update_control_props_payload.dart'; -import '../utils/buttons.dart'; import '../utils/colors.dart'; import 'create_control.dart'; +import 'flet_control_stateful_mixin.dart'; import 'list_tile.dart'; enum LabelPosition { right, left } @@ -18,20 +13,19 @@ class CupertinoSwitchControl extends StatefulWidget { final Control? parent; final Control control; final bool parentDisabled; - final dynamic dispatch; const CupertinoSwitchControl( {super.key, this.parent, required this.control, - required this.parentDisabled, - required this.dispatch}); + required this.parentDisabled}); @override State<CupertinoSwitchControl> createState() => _CupertinoSwitchControlState(); } -class _CupertinoSwitchControlState extends State<CupertinoSwitchControl> { +class _CupertinoSwitchControlState extends State<CupertinoSwitchControl> + with FletControlStatefulMixin { bool _value = false; late final FocusNode _focusNode; @@ -52,25 +46,14 @@ class _CupertinoSwitchControlState extends State<CupertinoSwitchControl> { void _onChange(bool value) { var svalue = value.toString(); debugPrint(svalue); - setState(() { - _value = value; - }); - List<Map<String, String>> props = [ - {"i": widget.control.id, "value": svalue} - ]; - widget.dispatch( - UpdateControlPropsAction(UpdateControlPropsPayload(props: props))); - final server = FletAppServices.of(context).server; - server.updateControlProps(props: props); - server.sendPageEvent( - eventTarget: widget.control.id, eventName: "change", eventData: svalue); + _value = value; + updateControlProps(widget.control.id, {"value": svalue}); + sendControlEvent(widget.control.id, "change", svalue); } void _onFocusChange() { - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, - eventName: _focusNode.hasFocus ? "focus" : "blur", - eventData: ""); + sendControlEvent( + widget.control.id, _focusNode.hasFocus ? "focus" : "blur", ""); } @override @@ -86,65 +69,57 @@ class _CupertinoSwitchControlState extends State<CupertinoSwitchControl> { bool autofocus = widget.control.attrBool("autofocus", false)!; bool disabled = widget.control.isDisabled || widget.parentDisabled; - return StoreConnector<AppState, Function>( - distinct: true, - converter: (store) => store.dispatch, - builder: (context, dispatch) { - debugPrint( - "CupertinoSwitch StoreConnector build: ${widget.control.id}"); - - bool value = widget.control.attrBool("value", false)!; - if (_value != value) { - _value = value; - } - - var materialThumbColor = parseMaterialStateColor( - Theme.of(context), widget.control, "thumbColor"); - - var materialTrackColor = parseMaterialStateColor( - Theme.of(context), widget.control, "trackColor"); - - var swtch = CupertinoSwitch( - autofocus: autofocus, - focusNode: _focusNode, - activeColor: HexColor.fromString(Theme.of(context), - widget.control.attrString("activeColor", "")!), - thumbColor: materialThumbColor?.resolve({}), - trackColor: materialTrackColor?.resolve({}), - focusColor: HexColor.fromString(Theme.of(context), - widget.control.attrString("focusColor", "")!), - value: _value, - onChanged: !disabled - ? (bool value) { - _onChange(value); + debugPrint("CupertinoSwitch build: ${widget.control.id}"); + + bool value = widget.control.attrBool("value", false)!; + if (_value != value) { + _value = value; + } + + var materialThumbColor = parseMaterialStateColor( + Theme.of(context), widget.control, "thumbColor"); + + var materialTrackColor = parseMaterialStateColor( + Theme.of(context), widget.control, "trackColor"); + + var swtch = CupertinoSwitch( + autofocus: autofocus, + focusNode: _focusNode, + activeColor: HexColor.fromString( + Theme.of(context), widget.control.attrString("activeColor", "")!), + thumbColor: materialThumbColor?.resolve({}), + trackColor: materialTrackColor?.resolve({}), + focusColor: HexColor.fromString( + Theme.of(context), widget.control.attrString("focusColor", "")!), + value: _value, + onChanged: !disabled + ? (bool value) { + _onChange(value); + } + : null); + + ListTileClicks.of(context)?.notifier.addListener(() { + _onChange(!_value); + }); + + Widget result = swtch; + if (label != "") { + var labelWidget = disabled + ? Text(label, + style: TextStyle(color: Theme.of(context).disabledColor)) + : MouseRegion(cursor: SystemMouseCursors.click, child: Text(label)); + result = MergeSemantics( + child: GestureDetector( + onTap: !disabled + ? () { + _onChange(!_value); } - : null); - - ListTileClicks.of(context)?.notifier.addListener(() { - _onChange(!_value); - }); - - Widget result = swtch; - if (label != "") { - var labelWidget = disabled - ? Text(label, - style: TextStyle(color: Theme.of(context).disabledColor)) - : MouseRegion( - cursor: SystemMouseCursors.click, child: Text(label)); - result = MergeSemantics( - child: GestureDetector( - onTap: !disabled - ? () { - _onChange(!_value); - } - : null, - child: labelPosition == LabelPosition.right - ? Row(children: [swtch, labelWidget]) - : Row(children: [labelWidget, swtch]))); - } - - return constrainedControl( - context, result, widget.parent, widget.control); - }); + : null, + child: labelPosition == LabelPosition.right + ? Row(children: [swtch, labelWidget]) + : Row(children: [labelWidget, swtch]))); + } + + return constrainedControl(context, result, widget.parent, widget.control); } } diff --git a/package/lib/src/controls/cupertino_textfield.dart b/package/lib/src/controls/cupertino_textfield.dart index d84e86a78..34954c8af 100644 --- a/package/lib/src/controls/cupertino_textfield.dart +++ b/package/lib/src/controls/cupertino_textfield.dart @@ -1,15 +1,9 @@ import 'package:collection/collection.dart'; -import 'package:flet/src/controls/textfield.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_redux/flutter_redux.dart'; -import '../actions.dart'; -import '../flet_app_services.dart'; -import '../models/app_state.dart'; import '../models/control.dart'; -import '../protocol/update_control_props_payload.dart'; import '../utils/borders.dart'; import '../utils/colors.dart'; import '../utils/gradient.dart'; @@ -17,7 +11,9 @@ import '../utils/shadows.dart'; import '../utils/text.dart'; import '../utils/textfield.dart'; import 'create_control.dart'; +import 'flet_control_stateful_mixin.dart'; import 'form_field.dart'; +import 'textfield.dart'; class CupertinoTextFieldControl extends StatefulWidget { final Control? parent; @@ -37,9 +33,10 @@ class CupertinoTextFieldControl extends StatefulWidget { _CupertinoTextFieldControlState(); } -class _CupertinoTextFieldControlState extends State<CupertinoTextFieldControl> { +class _CupertinoTextFieldControlState extends State<CupertinoTextFieldControl> + with FletControlStatefulMixin { String _value = ""; - bool _revealPassword = false; + final bool _revealPassword = false; bool _focused = false; late TextEditingController _controller; late final FocusNode _focusNode; @@ -54,10 +51,7 @@ class _CupertinoTextFieldControlState extends State<CupertinoTextFieldControl> { onKey: (FocusNode node, RawKeyEvent evt) { if (!evt.isShiftPressed && evt.logicalKey.keyLabel == 'Enter') { if (evt is RawKeyDownEvent) { - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "submit", - eventData: ""); + sendControlEvent(widget.control.id, "submit", ""); } return KeyEventResult.handled; } else { @@ -84,20 +78,16 @@ class _CupertinoTextFieldControlState extends State<CupertinoTextFieldControl> { setState(() { _focused = _shiftEnterfocusNode.hasFocus; }); - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, - eventName: _shiftEnterfocusNode.hasFocus ? "focus" : "blur", - eventData: ""); + sendControlEvent(widget.control.id, + _shiftEnterfocusNode.hasFocus ? "focus" : "blur", ""); } void _onFocusChange() { setState(() { _focused = _focusNode.hasFocus; }); - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, - eventName: _focusNode.hasFocus ? "focus" : "blur", - eventData: ""); + sendControlEvent( + widget.control.id, _focusNode.hasFocus ? "focus" : "blur", ""); } @override @@ -107,229 +97,193 @@ class _CupertinoTextFieldControlState extends State<CupertinoTextFieldControl> { bool autofocus = widget.control.attrBool("autofocus", false)!; bool disabled = widget.control.isDisabled || widget.parentDisabled; - return StoreConnector<AppState, Function>( - distinct: true, - converter: (store) => store.dispatch, - builder: (context, dispatch) { - debugPrint( - "CupertinoTextField StoreConnector build: ${widget.control.id}"); - - String value = widget.control.attrs["value"] ?? ""; - if (_value != value) { - _value = value; - _controller.text = value; - } - - var prefixControls = - widget.children.where((c) => c.name == "prefix" && c.isVisible); - var suffixControls = - widget.children.where((c) => c.name == "suffix" && c.isVisible); - - bool shiftEnter = widget.control.attrBool("shiftEnter", false)!; - bool multiline = - widget.control.attrBool("multiline", false)! || shiftEnter; - int minLines = widget.control.attrInt("minLines", 1)!; - int? maxLines = - widget.control.attrInt("maxLines", multiline ? null : 1); - - bool readOnly = widget.control.attrBool("readOnly", false)!; - bool password = widget.control.attrBool("password", false)!; - bool onChange = widget.control.attrBool("onChange", false)!; - - var cursorColor = HexColor.fromString( - Theme.of(context), widget.control.attrString("cursorColor", "")!); - var selectionColor = HexColor.fromString(Theme.of(context), - widget.control.attrString("selectionColor", "")!); - - int? maxLength = widget.control.attrInt("maxLength"); - - var textSize = widget.control.attrDouble("textSize"); - - var color = HexColor.fromString( - Theme.of(context), widget.control.attrString("color", "")!); - var focusedColor = HexColor.fromString(Theme.of(context), - widget.control.attrString("focusedColor", "")!); - - TextStyle? textStyle = - parseTextStyle(Theme.of(context), widget.control, "textStyle"); - if (textSize != null || color != null || focusedColor != null) { - textStyle = (textStyle ?? const TextStyle()).copyWith( - fontSize: textSize, - color: _focused ? focusedColor ?? color : color); - } - - TextCapitalization? textCapitalization = TextCapitalization.values - .firstWhere( - (a) => - a.name.toLowerCase() == - widget.control - .attrString("capitalization", "")! - .toLowerCase(), - orElse: () => TextCapitalization.none); - - FilteringTextInputFormatter? inputFilter = - parseInputFilter(widget.control, "inputFilter"); - - List<TextInputFormatter>? inputFormatters = []; - // add non-null input formatters - if (inputFilter != null) { - inputFormatters.add(inputFilter); - } - if (textCapitalization != TextCapitalization.none) { - inputFormatters - .add(TextCapitalizationFormatter(textCapitalization)); - } - - TextInputType keyboardType = parseTextInputType( - widget.control.attrString("keyboardType", "")!); - - if (multiline) { - keyboardType = TextInputType.multiline; - } - - TextAlign textAlign = TextAlign.values.firstWhere( - ((b) => - b.name == - widget.control.attrString("textAlign", "")!.toLowerCase()), - orElse: () => TextAlign.start, - ); - - bool autocorrect = widget.control.attrBool("autocorrect", true)!; - bool enableSuggestions = - widget.control.attrBool("enableSuggestions", true)!; - bool smartDashesType = - widget.control.attrBool("smartDashesType", true)!; - bool smartQuotesType = - widget.control.attrBool("smartQuotesType", true)!; - - FocusNode focusNode = shiftEnter ? _shiftEnterfocusNode : _focusNode; - - var focusValue = widget.control.attrString("focus"); - if (focusValue != null && focusValue != _lastFocusValue) { - _lastFocusValue = focusValue; - focusNode.requestFocus(); - } + debugPrint("CupertinoTextField StoreConnector build: ${widget.control.id}"); + + String value = widget.control.attrs["value"] ?? ""; + if (_value != value) { + _value = value; + _controller.text = value; + } + + var prefixControls = + widget.children.where((c) => c.name == "prefix" && c.isVisible); + var suffixControls = + widget.children.where((c) => c.name == "suffix" && c.isVisible); + + bool shiftEnter = widget.control.attrBool("shiftEnter", false)!; + bool multiline = widget.control.attrBool("multiline", false)! || shiftEnter; + int minLines = widget.control.attrInt("minLines", 1)!; + int? maxLines = widget.control.attrInt("maxLines", multiline ? null : 1); + + bool readOnly = widget.control.attrBool("readOnly", false)!; + bool password = widget.control.attrBool("password", false)!; + bool onChange = widget.control.attrBool("onChange", false)!; + + var cursorColor = HexColor.fromString( + Theme.of(context), widget.control.attrString("cursorColor", "")!); + var selectionColor = HexColor.fromString( + Theme.of(context), widget.control.attrString("selectionColor", "")!); + + int? maxLength = widget.control.attrInt("maxLength"); + + var textSize = widget.control.attrDouble("textSize"); + + var color = HexColor.fromString( + Theme.of(context), widget.control.attrString("color", "")!); + var focusedColor = HexColor.fromString( + Theme.of(context), widget.control.attrString("focusedColor", "")!); + + TextStyle? textStyle = + parseTextStyle(Theme.of(context), widget.control, "textStyle"); + if (textSize != null || color != null || focusedColor != null) { + textStyle = (textStyle ?? const TextStyle()).copyWith( + fontSize: textSize, color: _focused ? focusedColor ?? color : color); + } + + TextCapitalization? textCapitalization = TextCapitalization.values + .firstWhere( + (a) => + a.name.toLowerCase() == + widget.control.attrString("capitalization", "")!.toLowerCase(), + orElse: () => TextCapitalization.none); + + FilteringTextInputFormatter? inputFilter = + parseInputFilter(widget.control, "inputFilter"); + + List<TextInputFormatter>? inputFormatters = []; + // add non-null input formatters + if (inputFilter != null) { + inputFormatters.add(inputFilter); + } + if (textCapitalization != TextCapitalization.none) { + inputFormatters.add(TextCapitalizationFormatter(textCapitalization)); + } + + TextInputType keyboardType = + parseTextInputType(widget.control.attrString("keyboardType", "")!); + + if (multiline) { + keyboardType = TextInputType.multiline; + } + + TextAlign textAlign = TextAlign.values.firstWhere( + ((b) => + b.name == widget.control.attrString("textAlign", "")!.toLowerCase()), + orElse: () => TextAlign.start, + ); - BoxDecoration? defaultDecoration = - const CupertinoTextField().decoration; - var gradient = - parseGradient(Theme.of(context), widget.control, "gradient"); - var blendMode = BlendMode.values.firstWhereOrNull((e) => - e.name.toLowerCase() == - widget.control.attrString("blendMode", "")!.toLowerCase()); - - var borderRadius = parseBorderRadius(widget.control, "borderRadius"); - var bgColor = HexColor.fromString( - Theme.of(context), widget.control.attrString("bgColor", "")!); - - Widget textField = CupertinoTextField( - style: textStyle, - placeholder: widget.control.attrString("placeholderText"), - placeholderStyle: parseTextStyle( - Theme.of(context), widget.control, "placeholderStyle"), - autofocus: autofocus, - enabled: !disabled, - onSubmitted: !multiline - ? (_) { - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "submit", - eventData: ""); - } - : null, - decoration: defaultDecoration?.copyWith( - color: bgColor, - gradient: gradient, - backgroundBlendMode: - bgColor != null || gradient != null ? blendMode : null, - border: - parseBorder(Theme.of(context), widget.control, "border"), - borderRadius: borderRadius, - boxShadow: parseBoxShadow( - Theme.of(context), widget.control, "shadow")), - cursorHeight: widget.control.attrDouble("cursorHeight"), - showCursor: widget.control.attrBool("showCursor"), - cursorWidth: widget.control.attrDouble("cursorWidth") ?? 2.0, - cursorRadius: parseRadius(widget.control, "cursorRadius") ?? - const Radius.circular(2.0), - keyboardType: keyboardType, - autocorrect: autocorrect, - enableSuggestions: enableSuggestions, - smartDashesType: smartDashesType - ? SmartDashesType.enabled - : SmartDashesType.disabled, - smartQuotesType: smartQuotesType - ? SmartQuotesType.enabled - : SmartQuotesType.disabled, - suffixMode: parseVisibilityMode( - widget.control.attrString("suffixVisibilityMode", "")!), - prefixMode: parseVisibilityMode( - widget.control.attrString("prefixVisibilityMode", "")!), - textAlign: textAlign, - minLines: minLines, - maxLines: maxLines, - maxLength: maxLength, - prefix: prefixControls.isNotEmpty - ? createControl( - widget.control, prefixControls.first.id, disabled) - : null, - suffix: suffixControls.isNotEmpty - ? createControl( - widget.control, suffixControls.first.id, disabled) - : null, - readOnly: readOnly, - inputFormatters: - inputFormatters.isNotEmpty ? inputFormatters : null, - obscureText: password && !_revealPassword, - controller: _controller, - focusNode: focusNode, - onChanged: (String value) { - //debugPrint(value); - setState(() { - _value = value; - }); - List<Map<String, String>> props = [ - {"i": widget.control.id, "value": value} - ]; - dispatch(UpdateControlPropsAction( - UpdateControlPropsPayload(props: props))); - FletAppServices.of(context) - .server - .updateControlProps(props: props); - if (onChange) { - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "change", - eventData: value); - } - }); - - if (cursorColor != null || selectionColor != null) { - textField = TextSelectionTheme( - data: TextSelectionTheme.of(context).copyWith( - cursorColor: cursorColor, selectionColor: selectionColor), - child: textField); + bool autocorrect = widget.control.attrBool("autocorrect", true)!; + bool enableSuggestions = + widget.control.attrBool("enableSuggestions", true)!; + bool smartDashesType = widget.control.attrBool("smartDashesType", true)!; + bool smartQuotesType = widget.control.attrBool("smartQuotesType", true)!; + + FocusNode focusNode = shiftEnter ? _shiftEnterfocusNode : _focusNode; + + var focusValue = widget.control.attrString("focus"); + if (focusValue != null && focusValue != _lastFocusValue) { + _lastFocusValue = focusValue; + focusNode.requestFocus(); + } + + BoxDecoration? defaultDecoration = const CupertinoTextField().decoration; + var gradient = parseGradient(Theme.of(context), widget.control, "gradient"); + var blendMode = BlendMode.values.firstWhereOrNull((e) => + e.name.toLowerCase() == + widget.control.attrString("blendMode", "")!.toLowerCase()); + + var borderRadius = parseBorderRadius(widget.control, "borderRadius"); + var bgColor = HexColor.fromString( + Theme.of(context), widget.control.attrString("bgColor", "")!); + + Widget textField = CupertinoTextField( + style: textStyle, + placeholder: widget.control.attrString("placeholderText"), + placeholderStyle: parseTextStyle( + Theme.of(context), widget.control, "placeholderStyle"), + autofocus: autofocus, + enabled: !disabled, + onSubmitted: !multiline + ? (_) { + sendControlEvent(widget.control.id, "submit", ""); + } + : null, + decoration: defaultDecoration?.copyWith( + color: bgColor, + gradient: gradient, + backgroundBlendMode: + bgColor != null || gradient != null ? blendMode : null, + border: parseBorder(Theme.of(context), widget.control, "border"), + borderRadius: borderRadius, + boxShadow: + parseBoxShadow(Theme.of(context), widget.control, "shadow")), + cursorHeight: widget.control.attrDouble("cursorHeight"), + showCursor: widget.control.attrBool("showCursor"), + cursorWidth: widget.control.attrDouble("cursorWidth") ?? 2.0, + cursorRadius: parseRadius(widget.control, "cursorRadius") ?? + const Radius.circular(2.0), + keyboardType: keyboardType, + autocorrect: autocorrect, + enableSuggestions: enableSuggestions, + smartDashesType: smartDashesType + ? SmartDashesType.enabled + : SmartDashesType.disabled, + smartQuotesType: smartQuotesType + ? SmartQuotesType.enabled + : SmartQuotesType.disabled, + suffixMode: parseVisibilityMode( + widget.control.attrString("suffixVisibilityMode", "")!), + prefixMode: parseVisibilityMode( + widget.control.attrString("prefixVisibilityMode", "")!), + textAlign: textAlign, + minLines: minLines, + maxLines: maxLines, + maxLength: maxLength, + prefix: prefixControls.isNotEmpty + ? createControl(widget.control, prefixControls.first.id, disabled) + : null, + suffix: suffixControls.isNotEmpty + ? createControl(widget.control, suffixControls.first.id, disabled) + : null, + readOnly: readOnly, + inputFormatters: inputFormatters.isNotEmpty ? inputFormatters : null, + obscureText: password && !_revealPassword, + controller: _controller, + focusNode: focusNode, + onChanged: (String value) { + //debugPrint(value); + _value = value; + updateControlProps(widget.control.id, {"value": value}); + if (onChange) { + sendControlEvent(widget.control.id, "change", value); } + }); - if (widget.control.attrInt("expand", 0)! > 0) { - return constrainedControl( - context, textField, widget.parent, widget.control); - } else { - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - if (constraints.maxWidth == double.infinity && - widget.control.attrDouble("width") == null) { - textField = ConstrainedBox( - constraints: const BoxConstraints.tightFor(width: 300), - child: textField, - ); - } - - return constrainedControl( - context, textField, widget.parent, widget.control); - }, + if (cursorColor != null || selectionColor != null) { + textField = TextSelectionTheme( + data: TextSelectionTheme.of(context).copyWith( + cursorColor: cursorColor, selectionColor: selectionColor), + child: textField); + } + + if (widget.control.attrInt("expand", 0)! > 0) { + return constrainedControl( + context, textField, widget.parent, widget.control); + } else { + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + if (constraints.maxWidth == double.infinity && + widget.control.attrDouble("width") == null) { + textField = ConstrainedBox( + constraints: const BoxConstraints.tightFor(width: 300), + child: textField, ); } - }); + + return constrainedControl( + context, textField, widget.parent, widget.control); + }, + ); + } } } diff --git a/package/lib/src/controls/datatable.dart b/package/lib/src/controls/datatable.dart index c35d69ede..aa5f5da52 100644 --- a/package/lib/src/controls/datatable.dart +++ b/package/lib/src/controls/datatable.dart @@ -1,18 +1,15 @@ import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:flutter_redux/flutter_redux.dart'; -import '../flet_app_services.dart'; -import '../models/app_state.dart'; import '../models/control.dart'; -import '../models/controls_view_model.dart'; import '../utils/borders.dart'; -import '../utils/buttons.dart'; import '../utils/colors.dart'; import '../utils/gradient.dart'; import '../utils/text.dart'; import 'create_control.dart'; +import 'flet_control_stateful_mixin.dart'; +import 'flet_store_mixin.dart'; class DataTableControl extends StatefulWidget { final Control? parent; @@ -31,188 +28,157 @@ class DataTableControl extends StatefulWidget { State<DataTableControl> createState() => _DataTableControlState(); } -class _DataTableControlState extends State<DataTableControl> { +class _DataTableControlState extends State<DataTableControl> + with FletControlStatefulMixin, FletStoreMixin { @override Widget build(BuildContext context) { debugPrint("DataTableControl build: ${widget.control.id}"); bool tableDisabled = widget.control.isDisabled || widget.parentDisabled; - var server = FletAppServices.of(context).server; + var datatable = + withControls(widget.children.where((c) => c.isVisible).map((c) => c.id), + (content, viewModel) { + var bgColor = widget.control.attrString("bgColor"); + var border = parseBorder(Theme.of(context), widget.control, "border"); + var borderRadius = parseBorderRadius(widget.control, "borderRadius"); + var gradient = + parseGradient(Theme.of(context), widget.control, "gradient"); + var horizontalLines = + parseBorderSide(Theme.of(context), widget.control, "horizontalLines"); + var verticalLines = + parseBorderSide(Theme.of(context), widget.control, "verticalLines"); + var defaultDecoration = + Theme.of(context).dataTableTheme.decoration ?? const BoxDecoration(); - var datatable = StoreConnector<AppState, ControlsViewModel>( - distinct: true, - converter: (store) => ControlsViewModel.fromStore( - store, widget.children.where((c) => c.isVisible).map((c) => c.id)), - builder: (content, viewModel) { - var bgColor = widget.control.attrString("bgColor"); - var border = parseBorder(Theme.of(context), widget.control, "border"); - var borderRadius = parseBorderRadius(widget.control, "borderRadius"); - var gradient = - parseGradient(Theme.of(context), widget.control, "gradient"); - var horizontalLines = parseBorderSide( - Theme.of(context), widget.control, "horizontalLines"); - var verticalLines = parseBorderSide( - Theme.of(context), widget.control, "verticalLines"); - var defaultDecoration = Theme.of(context).dataTableTheme.decoration ?? - const BoxDecoration(); + BoxDecoration? decoration; + if (bgColor != null || + border != null || + borderRadius != null || + gradient != null) { + decoration = (defaultDecoration as BoxDecoration).copyWith( + color: HexColor.fromString(Theme.of(context), bgColor ?? ""), + border: border, + borderRadius: borderRadius, + gradient: gradient); + } - BoxDecoration? decoration; - if (bgColor != null || - border != null || - borderRadius != null || - gradient != null) { - decoration = (defaultDecoration as BoxDecoration).copyWith( - color: HexColor.fromString(Theme.of(context), bgColor ?? ""), - border: border, - borderRadius: borderRadius, - gradient: gradient); - } + TableBorder? tableBorder; + if (horizontalLines != null || verticalLines != null) { + tableBorder = TableBorder( + horizontalInside: horizontalLines ?? BorderSide.none, + verticalInside: verticalLines ?? BorderSide.none); + } - TableBorder? tableBorder; - if (horizontalLines != null || verticalLines != null) { - tableBorder = TableBorder( - horizontalInside: horizontalLines ?? BorderSide.none, - verticalInside: verticalLines ?? BorderSide.none); - } - - return DataTable( - decoration: decoration, - border: tableBorder, - checkboxHorizontalMargin: - widget.control.attrDouble("checkboxHorizontalMargin"), - columnSpacing: widget.control.attrDouble("columnSpacing"), - dataRowColor: parseMaterialStateColor( - Theme.of(context), widget.control, "dataRowColor"), - dataRowMinHeight: widget.control.attrDouble("dataRowMinHeight"), - dataRowMaxHeight: widget.control.attrDouble("dataRowMaxHeight"), - dataTextStyle: parseTextStyle( - Theme.of(context), widget.control, "dataTextStyle"), - headingRowColor: parseMaterialStateColor( - Theme.of(context), widget.control, "headingRowColor"), - headingRowHeight: widget.control.attrDouble("headingRowHeight"), - headingTextStyle: parseTextStyle( - Theme.of(context), widget.control, "headingTextStyle"), - dividerThickness: widget.control.attrDouble("dividerThickness"), - horizontalMargin: widget.control.attrDouble("horizontalMargin"), - showBottomBorder: - widget.control.attrBool("showBottomBorder", false)!, - showCheckboxColumn: - widget.control.attrBool("showCheckboxColumn", false)!, - sortAscending: widget.control.attrBool("sortAscending", false)!, - sortColumnIndex: widget.control.attrInt("sortColumnIndex"), - onSelectAll: widget.control.attrBool("onSelectAll", false)! - ? (selected) { - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "select_all", - eventData: - selected != null ? selected.toString() : ""); - } - : null, - columns: viewModel.controlViews - .where((c) => c.control.type == "c") - .map((column) { - var labelCtrls = column.children.where((c) => c.name == "l"); - return DataColumn( - numeric: column.control.attrBool("numeric", false)!, - tooltip: column.control.attrString("tooltip"), - onSort: column.control.attrBool("onSort", false)! - ? (columnIndex, ascending) { - server.sendPageEvent( - eventTarget: column.control.id, - eventName: "sort", - eventData: json.encode( - {"i": columnIndex, "a": ascending})); - } - : null, - label: createControl(column.control, labelCtrls.first.id, - column.control.isDisabled || tableDisabled)); - }).toList(), - rows: viewModel.controlViews - .where((c) => c.control.type == "r") - .map((row) { - return DataRow( - key: ValueKey(row.control.id), - selected: row.control.attrBool("selected", false)!, - color: parseMaterialStateColor( - Theme.of(context), row.control, "color"), - onSelectChanged: - row.control.attrBool("onSelectChanged", false)! - ? (selected) { - server.sendPageEvent( - eventTarget: row.control.id, - eventName: "select_changed", - eventData: selected != null - ? selected.toString() - : ""); - } - : null, - onLongPress: row.control.attrBool("onLongPress", false)! - ? () { - server.sendPageEvent( - eventTarget: row.control.id, - eventName: "long_press", - eventData: ""); - } - : null, - cells: row.children - .map((cell) => DataCell( - createControl(row.control, cell.childIds.first, - row.control.isDisabled || tableDisabled), - placeholder: cell.attrBool("placeholder", false)!, - showEditIcon: - cell.attrBool("showEditIcon", false)!, - onDoubleTap: cell.attrBool("onDoubleTap", false)! - ? () { - server.sendPageEvent( - eventTarget: cell.id, - eventName: "double_tap", - eventData: ""); - } - : null, - onLongPress: cell.attrBool("onLongPress", false)! - ? () { - server.sendPageEvent( - eventTarget: cell.id, - eventName: "long_press", - eventData: ""); - } - : null, - onTap: cell.attrBool("onTap", false)! - ? () { - server.sendPageEvent( - eventTarget: cell.id, - eventName: "tap", - eventData: ""); - } - : null, - onTapCancel: cell.attrBool("onTapCancel", false)! - ? () { - server.sendPageEvent( - eventTarget: cell.id, - eventName: "tap_cancel", - eventData: ""); - } - : null, - onTapDown: cell.attrBool("onTapDown", false)! - ? (details) { - server.sendPageEvent( - eventTarget: cell.id, - eventName: "tap_down", - eventData: json.encode({ - "kind": details.kind?.name, - "lx": details.localPosition.dx, - "ly": details.localPosition.dy, - "gx": details.globalPosition.dx, - "gy": details.globalPosition.dy, - })); - } - : null, - )) - .toList()); - }).toList()); - }); + return DataTable( + decoration: decoration, + border: tableBorder, + checkboxHorizontalMargin: + widget.control.attrDouble("checkboxHorizontalMargin"), + columnSpacing: widget.control.attrDouble("columnSpacing"), + dataRowColor: parseMaterialStateColor( + Theme.of(context), widget.control, "dataRowColor"), + dataRowMinHeight: widget.control.attrDouble("dataRowMinHeight"), + dataRowMaxHeight: widget.control.attrDouble("dataRowMaxHeight"), + dataTextStyle: parseTextStyle( + Theme.of(context), widget.control, "dataTextStyle"), + headingRowColor: parseMaterialStateColor( + Theme.of(context), widget.control, "headingRowColor"), + headingRowHeight: widget.control.attrDouble("headingRowHeight"), + headingTextStyle: parseTextStyle( + Theme.of(context), widget.control, "headingTextStyle"), + dividerThickness: widget.control.attrDouble("dividerThickness"), + horizontalMargin: widget.control.attrDouble("horizontalMargin"), + showBottomBorder: widget.control.attrBool("showBottomBorder", false)!, + showCheckboxColumn: + widget.control.attrBool("showCheckboxColumn", false)!, + sortAscending: widget.control.attrBool("sortAscending", false)!, + sortColumnIndex: widget.control.attrInt("sortColumnIndex"), + onSelectAll: widget.control.attrBool("onSelectAll", false)! + ? (selected) { + sendControlEvent(widget.control.id, "select_all", + selected != null ? selected.toString() : ""); + } + : null, + columns: viewModel.controlViews + .where((c) => c.control.type == "c") + .map((column) { + var labelCtrls = column.children.where((c) => c.name == "l"); + return DataColumn( + numeric: column.control.attrBool("numeric", false)!, + tooltip: column.control.attrString("tooltip"), + onSort: column.control.attrBool("onSort", false)! + ? (columnIndex, ascending) { + sendControlEvent(column.control.id, "sort", + json.encode({"i": columnIndex, "a": ascending})); + } + : null, + label: createControl(column.control, labelCtrls.first.id, + column.control.isDisabled || tableDisabled)); + }).toList(), + rows: viewModel.controlViews + .where((c) => c.control.type == "r") + .map((row) { + return DataRow( + key: ValueKey(row.control.id), + selected: row.control.attrBool("selected", false)!, + color: parseMaterialStateColor( + Theme.of(context), row.control, "color"), + onSelectChanged: row.control.attrBool("onSelectChanged", false)! + ? (selected) { + sendControlEvent(row.control.id, "select_changed", + selected != null ? selected.toString() : ""); + } + : null, + onLongPress: row.control.attrBool("onLongPress", false)! + ? () { + sendControlEvent(row.control.id, "long_press", ""); + } + : null, + cells: row.children + .map((cell) => DataCell( + createControl(row.control, cell.childIds.first, + row.control.isDisabled || tableDisabled), + placeholder: cell.attrBool("placeholder", false)!, + showEditIcon: cell.attrBool("showEditIcon", false)!, + onDoubleTap: cell.attrBool("onDoubleTap", false)! + ? () { + sendControlEvent(cell.id, "double_tap", ""); + } + : null, + onLongPress: cell.attrBool("onLongPress", false)! + ? () { + sendControlEvent(cell.id, "long_press", ""); + } + : null, + onTap: cell.attrBool("onTap", false)! + ? () { + sendControlEvent(cell.id, "tap", ""); + } + : null, + onTapCancel: cell.attrBool("onTapCancel", false)! + ? () { + sendControlEvent(cell.id, "tap_cancel", ""); + } + : null, + onTapDown: cell.attrBool("onTapDown", false)! + ? (details) { + sendControlEvent( + cell.id, + "tap_down", + json.encode({ + "kind": details.kind?.name, + "lx": details.localPosition.dx, + "ly": details.localPosition.dy, + "gx": details.globalPosition.dx, + "gy": details.globalPosition.dy, + })); + } + : null, + )) + .toList()); + }).toList()); + }); return constrainedControl( context, datatable, widget.parent, widget.control); diff --git a/package/lib/src/controls/date_picker.dart b/package/lib/src/controls/date_picker.dart index 18f3a272a..5e99625b4 100644 --- a/package/lib/src/controls/date_picker.dart +++ b/package/lib/src/controls/date_picker.dart @@ -1,10 +1,8 @@ import 'package:flutter/material.dart'; -import '../actions.dart'; -import '../flet_app_services.dart'; import '../models/control.dart'; -import '../protocol/update_control_props_payload.dart'; import '../utils/icons.dart'; +import 'flet_control_stateful_mixin.dart'; import 'form_field.dart'; class DatePickerControl extends StatefulWidget { @@ -12,7 +10,6 @@ class DatePickerControl extends StatefulWidget { final Control control; final List<Control> children; final bool parentDisabled; - final dynamic dispatch; const DatePickerControl({ super.key, @@ -20,14 +17,14 @@ class DatePickerControl extends StatefulWidget { required this.control, required this.children, required this.parentDisabled, - required this.dispatch, }); @override State<DatePickerControl> createState() => _DatePickerControlState(); } -class _DatePickerControlState extends State<DatePickerControl> { +class _DatePickerControlState extends State<DatePickerControl> + with FletControlStatefulMixin { @override Widget build(BuildContext context) { debugPrint("DatePicker build: ${widget.control.id}"); @@ -64,8 +61,8 @@ class _DatePickerControlState extends State<DatePickerControl> { String? fieldLabelText = widget.control.attrString("fieldLabelText"); IconData? switchToCalendarEntryModeIcon = parseIcon( widget.control.attrString("switchToCalendarEntryModeIcon", "")!); - IconData? switchToInputEntryModeIcon = parseIcon( - widget.control.attrString("switchToInputEntryModeIcon", "")!); + IconData? switchToInputEntryModeIcon = + parseIcon(widget.control.attrString("switchToInputEntryModeIcon", "")!); //Locale locale; // if (localeString == null) { @@ -86,17 +83,9 @@ class _DatePickerControlState extends State<DatePickerControl> { eventName = "change"; } widget.control.state["open"] = false; - List<Map<String, String>> props = [ - {"i": widget.control.id, "value": stringValue, "open": "false"} - ]; - widget.dispatch( - UpdateControlPropsAction(UpdateControlPropsPayload(props: props))); - FletAppServices.of(context).server.updateControlProps(props: props); - - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, - eventName: eventName, - eventData: stringValue); + updateControlProps( + widget.control.id, {"value": stringValue, "open": "false"}); + sendControlEvent(widget.control.id, eventName, stringValue); } Widget createSelectDateDialog() { diff --git a/package/lib/src/controls/dismissible.dart b/package/lib/src/controls/dismissible.dart index 39b760cb2..22faddb42 100644 --- a/package/lib/src/controls/dismissible.dart +++ b/package/lib/src/controls/dismissible.dart @@ -3,97 +3,97 @@ import 'dart:convert'; import 'package:flutter/material.dart'; -import '../flet_server.dart'; import '../models/control.dart'; import '../utils/dismissible.dart'; import 'create_control.dart'; import 'error.dart'; +import 'flet_control_stateful_mixin.dart'; -class DismissibleControl extends StatelessWidget { +class DismissibleControl extends StatefulWidget { final Control? parent; final Control control; final List<Control> children; final bool parentDisabled; - final FletServer server; const DismissibleControl( {super.key, this.parent, required this.control, required this.children, - required this.parentDisabled, - required this.server}); + required this.parentDisabled}); @override + State<DismissibleControl> createState() => _DismissibleControlState(); +} + +class _DismissibleControlState extends State<DismissibleControl> + with FletControlStatefulMixin { + @override Widget build(BuildContext context) { - debugPrint("Dismissible build: ${control.id}"); + debugPrint("Dismissible build: ${widget.control.id}"); - bool disabled = control.isDisabled || parentDisabled; - var contentCtrls = children.where((c) => c.name == "content"); + bool disabled = widget.control.isDisabled || widget.parentDisabled; + var contentCtrls = widget.children.where((c) => c.name == "content"); if (contentCtrls.isEmpty) { return const ErrorControl("Dismissible does not have a content."); } - var backgroundCtrls = children.where((c) => c.name == "background"); + var backgroundCtrls = widget.children.where((c) => c.name == "background"); var secondaryBackgroundCtrls = - children.where((c) => c.name == "secondaryBackground"); + widget.children.where((c) => c.name == "secondaryBackground"); var dismissThresholds = - parseDismissThresholds(control, "dismissThresholds"); + parseDismissThresholds(widget.control, "dismissThresholds"); DismissDirection? direction = DismissDirection.values.firstWhere( (a) => a.name.toLowerCase() == - control.attrString("dismissDirection", "")!.toLowerCase(), + widget.control.attrString("dismissDirection", "")!.toLowerCase(), orElse: () => DismissDirection.horizontal); - server.controlInvokeMethods[control.id] = (methodName, args) async { - debugPrint("Dismissible.onMethod(${control.id})"); + subscribeMethods(widget.control.id, (methodName, args) async { + debugPrint("Dismissible.onMethod(${widget.control.id})"); if (methodName == "confirm_dismiss") { - control.state["confirm_dismiss"] + widget.control.state["confirm_dismiss"] ?.complete(bool.tryParse(args["dismiss"] ?? "")); - server.controlInvokeMethods.remove(control.id); + unsubscribeMethods(widget.control.id); } return null; - }; + }); return constrainedControl( context, Dismissible( - key: ValueKey<String>(control.id), + key: ValueKey<String>(widget.control.id), direction: direction, background: backgroundCtrls.isNotEmpty - ? createControl(control, backgroundCtrls.first.id, disabled) + ? createControl( + widget.control, backgroundCtrls.first.id, disabled) : Container(color: Colors.transparent), secondaryBackground: secondaryBackgroundCtrls.isNotEmpty ? createControl( - control, secondaryBackgroundCtrls.first.id, disabled) + widget.control, secondaryBackgroundCtrls.first.id, disabled) : Container(color: Colors.transparent), - onDismissed: control.attrBool("onDismiss", false)! + onDismissed: widget.control.attrBool("onDismiss", false)! ? (DismissDirection direction) { - server.sendPageEvent( - eventTarget: control.id, - eventName: "dismiss", - eventData: direction.name); + sendControlEvent( + widget.control.id, "dismiss", direction.name); } : null, - onResize: control.attrBool("onResize", false)! + onResize: widget.control.attrBool("onResize", false)! ? () { - server.sendPageEvent( - eventTarget: control.id, - eventName: "resize", - eventData: ""); + sendControlEvent(widget.control.id, "resize", ""); } : null, - onUpdate: control.attrBool("onUpdate", false)! + onUpdate: widget.control.attrBool("onUpdate", false)! ? (DismissUpdateDetails details) { - server.sendPageEvent( - eventTarget: control.id, - eventName: "update", - eventData: json.encode(DismissibleUpdateEvent( + sendControlEvent( + widget.control.id, + "update", + json.encode(DismissibleUpdateEvent( direction: details.direction.name, previousReached: details.previousReached, progress: details.progress, @@ -101,27 +101,28 @@ class DismissibleControl extends StatelessWidget { .toJson())); } : null, - confirmDismiss: control.attrBool("onConfirmDismiss", false)! + confirmDismiss: widget.control.attrBool("onConfirmDismiss", false)! ? (DismissDirection direction) { - debugPrint("Dismissible.confirmDismiss(${control.id})"); + debugPrint( + "Dismissible.confirmDismiss(${widget.control.id})"); var completer = Completer<bool?>(); - control.state["confirm_dismiss"] = completer; - server.sendPageEvent( - eventTarget: control.id, - eventName: "confirm_dismiss", - eventData: direction.name); + widget.control.state["confirm_dismiss"] = completer; + sendControlEvent( + widget.control.id, "confirm_dismiss", direction.name); return completer.future; } : null, - movementDuration: - Duration(milliseconds: control.attrInt("duration", 200)!), - resizeDuration: - Duration(milliseconds: control.attrInt("resizeDuration", 300)!), - crossAxisEndOffset: control.attrDouble("crossAxisEndOffset", 0.0)!, + movementDuration: Duration( + milliseconds: widget.control.attrInt("duration", 200)!), + resizeDuration: Duration( + milliseconds: widget.control.attrInt("resizeDuration", 300)!), + crossAxisEndOffset: + widget.control.attrDouble("crossAxisEndOffset", 0.0)!, dismissThresholds: dismissThresholds ?? {}, - child: createControl(control, contentCtrls.first.id, disabled)), - parent, - control); + child: + createControl(widget.control, contentCtrls.first.id, disabled)), + widget.parent, + widget.control); } } diff --git a/package/lib/src/controls/drag_target.dart b/package/lib/src/controls/drag_target.dart index a2fc45994..b43e79f48 100644 --- a/package/lib/src/controls/drag_target.dart +++ b/package/lib/src/controls/drag_target.dart @@ -2,13 +2,30 @@ import 'dart:convert'; import 'package:flutter/material.dart'; -import '../flet_app_services.dart'; import '../models/control.dart'; -import '../protocol/drag_target_accept_event.dart'; import 'create_control.dart'; import 'error.dart'; +import 'flet_control_stateless_mixin.dart'; -class DragTargetControl extends StatelessWidget { +class DragTargetAcceptEvent { + final String srcId; + final double x; + final double y; + + DragTargetAcceptEvent({ + required this.srcId, + required this.x, + required this.y, + }); + + Map<String, dynamic> toJson() => <String, dynamic>{ + 'src_id': srcId, + 'x': x, + 'y': y, + }; +} + +class DragTargetControl extends StatelessWidget with FletControlStatelessMixin { final Control? parent; final Control control; final List<Control> children; @@ -38,8 +55,6 @@ class DragTargetControl extends StatelessWidget { return const ErrorControl("DragTarget should have content."); } - final server = FletAppServices.of(context).server; - return DragTarget<String>( builder: ( BuildContext context, @@ -58,10 +73,8 @@ class DragTargetControl extends StatelessWidget { srcGroup = jd["group"] as String; } var groupsEqual = srcGroup == group; - server.sendPageEvent( - eventTarget: control.id, - eventName: "will_accept", - eventData: groupsEqual.toString()); + sendControlEvent( + context, control.id, "will_accept", groupsEqual.toString()); return groupsEqual; }, onAcceptWithDetails: (details) { @@ -69,10 +82,11 @@ class DragTargetControl extends StatelessWidget { debugPrint("DragTarget.onAcceptWithDetails ${control.id}: $data"); var jd = json.decode(data); var srcId = jd["id"] as String; - server.sendPageEvent( - eventTarget: control.id, - eventName: "accept", - eventData: json.encode(DragTargetAcceptEvent( + sendControlEvent( + context, + control.id, + "accept", + json.encode(DragTargetAcceptEvent( srcId: srcId, x: details.offset.dx, y: details.offset.dy) .toJson())); }, @@ -83,8 +97,7 @@ class DragTargetControl extends StatelessWidget { var jd = json.decode(data); srcId = jd["id"] as String; } - server.sendPageEvent( - eventTarget: control.id, eventName: "leave", eventData: srcId); + sendControlEvent(context, control.id, "leave", srcId); }, ); } diff --git a/package/lib/src/controls/dropdown.dart b/package/lib/src/controls/dropdown.dart index c6ee23933..4b648dd53 100644 --- a/package/lib/src/controls/dropdown.dart +++ b/package/lib/src/controls/dropdown.dart @@ -1,37 +1,32 @@ import 'package:flutter/material.dart'; -import 'package:flutter_redux/flutter_redux.dart'; -import '../actions.dart'; -import '../flet_app_services.dart'; -import '../models/app_state.dart'; import '../models/control.dart'; -import '../models/control_children_view_model.dart'; -import '../protocol/update_control_props_payload.dart'; import '../utils/alignment.dart'; import '../utils/borders.dart'; import '../utils/colors.dart'; import '../utils/text.dart'; import 'create_control.dart'; +import 'flet_control_stateful_mixin.dart'; +import 'flet_store_mixin.dart'; import 'form_field.dart'; class DropdownControl extends StatefulWidget { final Control? parent; final Control control; final bool parentDisabled; - final dynamic dispatch; const DropdownControl( {super.key, this.parent, required this.control, - required this.parentDisabled, - required this.dispatch}); + required this.parentDisabled}); @override State<DropdownControl> createState() => _DropdownControlState(); } -class _DropdownControlState extends State<DropdownControl> { +class _DropdownControlState extends State<DropdownControl> + with FletControlStatefulMixin, FletStoreMixin { String? _value; bool _focused = false; late final FocusNode _focusNode; @@ -48,10 +43,8 @@ class _DropdownControlState extends State<DropdownControl> { setState(() { _focused = _focusNode.hasFocus; }); - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, - eventName: _focusNode.hasFocus ? "focus" : "blur", - eventData: ""); + sendControlEvent( + widget.control.id, _focusNode.hasFocus ? "focus" : "blur", ""); } @override @@ -65,136 +58,115 @@ class _DropdownControlState extends State<DropdownControl> { Widget build(BuildContext context) { debugPrint("Dropdown build: ${widget.control.id}"); - final server = FletAppServices.of(context).server; - - return StoreConnector<AppState, ControlChildrenViewModel>( - distinct: true, - ignoreChange: (state) { - return state.controls[widget.control.id] == null; - }, - converter: (store) => ControlChildrenViewModel.fromStore( - store, widget.control.id, - dispatch: store.dispatch), - builder: (context, itemsView) { - debugPrint("Dropdown StoreConnector build: ${widget.control.id}"); - - bool autofocus = widget.control.attrBool("autofocus", false)!; - bool disabled = widget.control.isDisabled || widget.parentDisabled; - - var textSize = widget.control.attrDouble("textSize"); - - var color = HexColor.fromString( - Theme.of(context), widget.control.attrString("color", "")!); - var focusedColor = HexColor.fromString(Theme.of(context), - widget.control.attrString("focusedColor", "")!); - - TextStyle? textStyle = - parseTextStyle(Theme.of(context), widget.control, "textStyle"); - if (textSize != null || color != null || focusedColor != null) { - textStyle = (textStyle ?? const TextStyle()).copyWith( - fontSize: textSize, - color: (_focused ? focusedColor ?? color : color) ?? - Theme.of(context).colorScheme.onSurface); - } - - var alignment = parseAlignment(widget.control, "alignment"); - - var items = itemsView.children - .where((c) => c.name == null && c.isVisible) - .map<DropdownMenuItem<String>>((Control itemCtrl) { - Widget itemChild = Text( - itemCtrl.attrs["text"] ?? itemCtrl.attrs["key"] ?? itemCtrl.id, - ); - - if (alignment != null) { - itemChild = Container(alignment: alignment, child: itemChild); + return withControls(widget.control.childIds, (context, itemsView) { + debugPrint("DropdownFletControlState build: ${widget.control.id}"); + + bool autofocus = widget.control.attrBool("autofocus", false)!; + bool disabled = widget.control.isDisabled || widget.parentDisabled; + + var textSize = widget.control.attrDouble("textSize"); + + var color = HexColor.fromString( + Theme.of(context), widget.control.attrString("color", "")!); + var focusedColor = HexColor.fromString( + Theme.of(context), widget.control.attrString("focusedColor", "")!); + + TextStyle? textStyle = + parseTextStyle(Theme.of(context), widget.control, "textStyle"); + if (textSize != null || color != null || focusedColor != null) { + textStyle = (textStyle ?? const TextStyle()).copyWith( + fontSize: textSize, + color: (_focused ? focusedColor ?? color : color) ?? + Theme.of(context).colorScheme.onSurface); + } + + var alignment = parseAlignment(widget.control, "alignment"); + + var items = itemsView.controlViews + .map((v) => v.control) + .where((c) => c.name == null && c.isVisible) + .map<DropdownMenuItem<String>>((Control itemCtrl) { + Widget itemChild = Text( + itemCtrl.attrs["text"] ?? itemCtrl.attrs["key"] ?? itemCtrl.id, + ); + + if (alignment != null) { + itemChild = Container(alignment: alignment, child: itemChild); + } + return DropdownMenuItem<String>( + enabled: !(disabled || itemCtrl.isDisabled), + value: itemCtrl.attrs["key"] ?? itemCtrl.attrs["text"] ?? itemCtrl.id, + child: itemChild, + ); + }).toList(); + + String? value = widget.control.attrString("value"); + if (_value != value) { + _value = value; + } + + if (items.where((item) => item.value == value).isEmpty) { + _value = null; + } + + var prefixControls = itemsView.controlViews + .where((c) => c.control.name == "prefix" && c.control.isVisible); + var suffixControls = itemsView.controlViews + .where((c) => c.control.name == "suffix" && c.control.isVisible); + + var focusValue = widget.control.attrString("focus"); + if (focusValue != null && focusValue != _lastFocusValue) { + _lastFocusValue = focusValue; + _focusNode.requestFocus(); + } + + var borderRadius = parseBorderRadius(widget.control, "borderRadius"); + + Widget dropDown = DropdownButtonFormField<String>( + style: textStyle, + autofocus: autofocus, + focusNode: _focusNode, + value: _value, + borderRadius: borderRadius, + alignment: alignment ?? AlignmentDirectional.centerStart, + isExpanded: alignment != null, + decoration: buildInputDecoration( + context, + widget.control, + prefixControls.isNotEmpty ? prefixControls.first.control : null, + suffixControls.isNotEmpty ? suffixControls.first.control : null, + null, + _focused), + onChanged: disabled + ? null + : (String? value) { + debugPrint("Dropdown selected value: $value"); + _value = value!; + updateControlProps(widget.control.id, {"value": value}); + sendControlEvent(widget.control.id, "change", value); + }, + items: items, + ); + + if (widget.control.attrInt("expand", 0)! > 0) { + return constrainedControl( + context, dropDown, widget.parent, widget.control); + } else { + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + if (constraints.maxWidth == double.infinity && + widget.control.attrDouble("width") == null) { + dropDown = ConstrainedBox( + constraints: const BoxConstraints.tightFor(width: 300), + child: dropDown, + ); } - return DropdownMenuItem<String>( - enabled: !(disabled || itemCtrl.isDisabled), - value: itemCtrl.attrs["key"] ?? - itemCtrl.attrs["text"] ?? - itemCtrl.id, - child: itemChild, - ); - }).toList(); - - String? value = widget.control.attrString("value"); - if (_value != value) { - _value = value; - } - - if (items.where((item) => item.value == value).isEmpty) { - _value = null; - } - - var prefixControls = itemsView.children - .where((c) => c.name == "prefix" && c.isVisible); - var suffixControls = itemsView.children - .where((c) => c.name == "suffix" && c.isVisible); - - var focusValue = widget.control.attrString("focus"); - if (focusValue != null && focusValue != _lastFocusValue) { - _lastFocusValue = focusValue; - _focusNode.requestFocus(); - } - - var borderRadius = parseBorderRadius(widget.control, "borderRadius"); - - Widget dropDown = DropdownButtonFormField<String>( - style: textStyle, - autofocus: autofocus, - focusNode: _focusNode, - value: _value, - borderRadius: borderRadius, - alignment: alignment ?? AlignmentDirectional.centerStart, - isExpanded: alignment != null, - decoration: buildInputDecoration( - context, - widget.control, - prefixControls.isNotEmpty ? prefixControls.first : null, - suffixControls.isNotEmpty ? suffixControls.first : null, - null, - _focused), - onChanged: disabled - ? null - : (String? value) { - debugPrint("Dropdown selected value: $value"); - setState(() { - _value = value!; - }); - List<Map<String, String>> props = [ - {"i": widget.control.id, "value": value!} - ]; - widget.dispatch(UpdateControlPropsAction( - UpdateControlPropsPayload(props: props))); - server.updateControlProps(props: props); - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "change", - eventData: value); - }, - items: items, - ); - - if (widget.control.attrInt("expand", 0)! > 0) { + return constrainedControl( context, dropDown, widget.parent, widget.control); - } else { - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - if (constraints.maxWidth == double.infinity && - widget.control.attrDouble("width") == null) { - dropDown = ConstrainedBox( - constraints: const BoxConstraints.tightFor(width: 300), - child: dropDown, - ); - } - - return constrainedControl( - context, dropDown, widget.parent, widget.control); - }, - ); - } - }); + }, + ); + } + }); } } diff --git a/package/lib/src/controls/elevated_button.dart b/package/lib/src/controls/elevated_button.dart index 737750893..ca2d9d420 100644 --- a/package/lib/src/controls/elevated_button.dart +++ b/package/lib/src/controls/elevated_button.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import '../flet_app_services.dart'; import '../models/control.dart'; import '../utils/buttons.dart'; import '../utils/colors.dart'; @@ -8,6 +7,7 @@ import '../utils/icons.dart'; import '../utils/launch_url.dart'; import 'create_control.dart'; import 'error.dart'; +import 'flet_control_stateful_mixin.dart'; class ElevatedButtonControl extends StatefulWidget { final Control? parent; @@ -26,7 +26,8 @@ class ElevatedButtonControl extends StatefulWidget { State<ElevatedButtonControl> createState() => _ElevatedButtonControlState(); } -class _ElevatedButtonControlState extends State<ElevatedButtonControl> { +class _ElevatedButtonControlState extends State<ElevatedButtonControl> + with FletControlStatefulMixin { late final FocusNode _focusNode; String? _lastFocusValue; @@ -45,18 +46,14 @@ class _ElevatedButtonControlState extends State<ElevatedButtonControl> { } void _onFocusChange() { - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, - eventName: _focusNode.hasFocus ? "focus" : "blur", - eventData: ""); + sendControlEvent( + widget.control.id, _focusNode.hasFocus ? "focus" : "blur", ""); } @override Widget build(BuildContext context) { debugPrint("Button build: ${widget.control.id}"); - final server = FletAppServices.of(context).server; - String text = widget.control.attrString("text", "")!; String url = widget.control.attrString("url", "")!; IconData? icon = parseIcon(widget.control.attrString("icon", "")!); @@ -75,30 +72,21 @@ class _ElevatedButtonControlState extends State<ElevatedButtonControl> { openWebBrowser(url, webWindowName: widget.control.attrString("urlTarget")); } - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "click", - eventData: ""); + sendControlEvent(widget.control.id, "click", ""); } : null; Function()? onLongPressHandler = onLongPress && !disabled ? () { debugPrint("Button ${widget.control.id} long pressed!"); - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "long_press", - eventData: ""); + sendControlEvent(widget.control.id, "long_press", ""); } : null; Function(bool)? onHoverHandler = onHover && !disabled ? (state) { debugPrint("Button ${widget.control.id} hovered!"); - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "hover", - eventData: state.toString()); + sendControlEvent(widget.control.id, "hover", state.toString()); } : null; diff --git a/package/lib/src/controls/expansion_panel.dart b/package/lib/src/controls/expansion_panel.dart index a6c9e8e19..478e7dc9c 100644 --- a/package/lib/src/controls/expansion_panel.dart +++ b/package/lib/src/controls/expansion_panel.dart @@ -1,37 +1,32 @@ import 'package:flutter/material.dart'; -import 'package:flutter_redux/flutter_redux.dart'; -import '../actions.dart'; -import '../flet_app_services.dart'; -import '../models/app_state.dart'; import '../models/control.dart'; -import '../models/controls_view_model.dart'; -import '../protocol/update_control_props_payload.dart'; import '../utils/colors.dart'; import '../utils/edge_insets.dart'; import 'create_control.dart'; +import 'flet_control_stateful_mixin.dart'; +import 'flet_store_mixin.dart'; class ExpansionPanelListControl extends StatefulWidget { final Control? parent; final Control control; final List<Control> children; final bool parentDisabled; - final dynamic dispatch; const ExpansionPanelListControl( {super.key, this.parent, required this.control, required this.children, - required this.parentDisabled, - required this.dispatch}); + required this.parentDisabled}); @override State<ExpansionPanelListControl> createState() => _ExpansionPanelListControlState(); } -class _ExpansionPanelListControlState extends State<ExpansionPanelListControl> { +class _ExpansionPanelListControlState extends State<ExpansionPanelListControl> + with FletControlStatefulMixin, FletStoreMixin { @override Widget build(BuildContext context) { debugPrint("ExpansionPanelList build: ${widget.control.id}"); @@ -41,20 +36,9 @@ class _ExpansionPanelListControlState extends State<ExpansionPanelListControl> { .toList(); void onChange(int index, bool isExpanded) { - List<Map<String, String>> props = [ - { - "i": panels[index].id, - "expanded": isExpanded.toString().toLowerCase() - } - ]; - widget.dispatch( - UpdateControlPropsAction(UpdateControlPropsPayload(props: props))); - var server = FletAppServices.of(context).server; - server.updateControlProps(props: props); - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "change", - eventData: "$index"); + updateControlProps( + panels[index].id, {"expanded": isExpanded.toString().toLowerCase()}); + sendControlEvent(widget.control.id, "change", "$index"); } bool disabled = widget.control.isDisabled || widget.parentDisabled; @@ -68,55 +52,50 @@ class _ExpansionPanelListControlState extends State<ExpansionPanelListControl> { parseEdgeInsets(widget.control, "expandedHeaderPadding"); debugPrint( - "ExpansionPanelListControl StoreConnector build: ${widget.control.id}"); + "ExpansionPanelListControlFletControlState build: ${widget.control.id}"); - var panelList = StoreConnector<AppState, ControlsViewModel>( - distinct: true, - converter: (store) => - ControlsViewModel.fromStore(store, panels.map((p) => p.id)), - builder: (content, panelViews) { - return ExpansionPanelList( - elevation: widget.control.attrDouble("elevation", 2)!, - materialGapSize: widget.control.attrDouble("spacing", 16)!, - dividerColor: dividerColor, - expandIconColor: expandedIconColor, - expandedHeaderPadding: expandedHeaderPadding ?? - const EdgeInsets.symmetric(vertical: 16), - expansionCallback: !disabled - ? (int index, bool isExpanded) { - onChange(index, isExpanded); - } - : null, - children: panelViews.controlViews.map((panelView) { - var headerCtrls = panelView.children - .where((c) => c.name == "header" && c.isVisible); - var bodyCtrls = panelView.children - .where((c) => c.name == "content" && c.isVisible); + var panelList = + withControls(panels.map((p) => p.id), (content, panelViews) { + return ExpansionPanelList( + elevation: widget.control.attrDouble("elevation", 2)!, + materialGapSize: widget.control.attrDouble("spacing", 16)!, + dividerColor: dividerColor, + expandIconColor: expandedIconColor, + expandedHeaderPadding: + expandedHeaderPadding ?? const EdgeInsets.symmetric(vertical: 16), + expansionCallback: !disabled + ? (int index, bool isExpanded) { + onChange(index, isExpanded); + } + : null, + children: panelViews.controlViews.map((panelView) { + var headerCtrls = panelView.children + .where((c) => c.name == "header" && c.isVisible); + var bodyCtrls = panelView.children + .where((c) => c.name == "content" && c.isVisible); - var isExpanded = - panelView.control.attrBool("expanded", false)!; - var canTapHeader = - panelView.control.attrBool("canTapHeader", false)!; - var bgColor = HexColor.fromString(Theme.of(context), - panelView.control.attrString("bgColor", "")!); + var isExpanded = panelView.control.attrBool("expanded", false)!; + var canTapHeader = + panelView.control.attrBool("canTapHeader", false)!; + var bgColor = HexColor.fromString(Theme.of(context), + panelView.control.attrString("bgColor", "")!); - return ExpansionPanel( - backgroundColor: bgColor, - isExpanded: isExpanded, - canTapOnHeader: canTapHeader, - headerBuilder: (BuildContext context, bool isExpanded) { - return headerCtrls.isNotEmpty - ? createControl( - widget.control, headerCtrls.first.id, disabled) - : const ListTile(title: Text("Header Placeholder")); - }, - body: bodyCtrls.isNotEmpty - ? createControl( - widget.control, bodyCtrls.first.id, disabled) - : const ListTile(title: Text("Body Placeholder")), - ); - }).toList()); - }); + return ExpansionPanel( + backgroundColor: bgColor, + isExpanded: isExpanded, + canTapOnHeader: canTapHeader, + headerBuilder: (BuildContext context, bool isExpanded) { + return headerCtrls.isNotEmpty + ? createControl( + widget.control, headerCtrls.first.id, disabled) + : const ListTile(title: Text("Header Placeholder")); + }, + body: bodyCtrls.isNotEmpty + ? createControl(widget.control, bodyCtrls.first.id, disabled) + : const ListTile(title: Text("Body Placeholder")), + ); + }).toList()); + }); return constrainedControl( context, panelList, widget.parent, widget.control); diff --git a/package/lib/src/controls/expansion_tile.dart b/package/lib/src/controls/expansion_tile.dart index cb2bdd193..406d92b5b 100644 --- a/package/lib/src/controls/expansion_tile.dart +++ b/package/lib/src/controls/expansion_tile.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import '../flet_app_services.dart'; import '../models/control.dart'; import '../utils/alignment.dart'; import '../utils/borders.dart'; @@ -8,8 +7,10 @@ import '../utils/colors.dart'; import '../utils/edge_insets.dart'; import 'create_control.dart'; import 'error.dart'; +import 'flet_control_stateless_mixin.dart'; -class ExpansionTileControl extends StatelessWidget { +class ExpansionTileControl extends StatelessWidget + with FletControlStatelessMixin { final Control? parent; final Control control; final List<Control> children; @@ -26,8 +27,6 @@ class ExpansionTileControl extends StatelessWidget { Widget build(BuildContext context) { debugPrint("ExpansionTile build: ${control.id}"); - final server = FletAppServices.of(context).server; - var ctrls = children.where((c) => c.name == "controls" && c.isVisible); var leadingCtrls = children.where((c) => c.name == "leading" && c.isVisible); @@ -77,17 +76,14 @@ class ExpansionTileControl extends StatelessWidget { return const ErrorControl( 'CrossAxisAlignment.baseline is not supported since the expanded ' 'controls are aligned in a column, not a row. ' - 'Try aligning the controls differently.'); + 'Try aligning the controls differently.'); } Function(bool)? onChange = (onchange) && !disabled ? (expanded) { debugPrint( "ExpansionTile ${control.id} was ${expanded ? "expanded" : "collapsed"}"); - server.sendPageEvent( - eventTarget: control.id, - eventName: "change", - eventData: "$expanded"); + sendControlEvent(context, control.id, "change", "$expanded"); } : null; diff --git a/package/lib/src/controls/file_picker.dart b/package/lib/src/controls/file_picker.dart index 4ead8ba43..2a0ada2d8 100644 --- a/package/lib/src/controls/file_picker.dart +++ b/package/lib/src/controls/file_picker.dart @@ -4,20 +4,62 @@ import 'package:collection/collection.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_redux/flutter_redux.dart'; import 'package:http/http.dart' as http; -import '../actions.dart'; import '../flet_app_services.dart'; import '../flet_server.dart'; -import '../models/app_state.dart'; import '../models/control.dart'; -import '../protocol/file_picker_result_event.dart'; -import '../protocol/file_picker_upload_file.dart'; -import '../protocol/file_picker_upload_progress_event.dart'; -import '../protocol/update_control_props_payload.dart'; import '../utils/desktop.dart'; import '../utils/strings.dart'; +import 'flet_control_stateful_mixin.dart'; +import 'flet_store_mixin.dart'; + +class FilePickerResultEvent { + final String? path; + final List<FilePickerFile>? files; + + FilePickerResultEvent({required this.path, required this.files}); + + Map<String, dynamic> toJson() => <String, dynamic>{ + 'path': path, + 'files': files?.map((f) => f.toJson()).toList() + }; +} + +class FilePickerFile { + final String name; + final String? path; + final int size; + + FilePickerFile({required this.name, required this.path, required this.size}); + + Map<String, dynamic> toJson() => + <String, dynamic>{'name': name, 'path': path, 'size': size}; +} + +class FilePickerUploadFile { + final String name; + final String uploadUrl; + final String method; + + FilePickerUploadFile( + {required this.name, required this.uploadUrl, required this.method}); +} + +class FilePickerUploadProgressEvent { + final String name; + final double? progress; + final String? error; + + FilePickerUploadProgressEvent( + {required this.name, required this.progress, required this.error}); + + Map<String, dynamic> toJson() => <String, dynamic>{ + 'file_name': name, + 'progress': progress, + 'error': error + }; +} class FilePickerControl extends StatefulWidget { final Control? parent; @@ -34,7 +76,8 @@ class FilePickerControl extends StatefulWidget { State<FilePickerControl> createState() => _FilePickerControlState(); } -class _FilePickerControlState extends State<FilePickerControl> { +class _FilePickerControlState extends State<FilePickerControl> + with FletControlStatefulMixin, FletStoreMixin { String? _state; String? _upload; String? _path; @@ -44,128 +87,117 @@ class _FilePickerControlState extends State<FilePickerControl> { Widget build(BuildContext context) { debugPrint("FilePicker build: ${widget.control.id}"); - return StoreConnector<AppState, Uri?>( - distinct: true, - converter: (store) => store.state.pageUri, - builder: (context, pageUri) { - var state = widget.control.attrString("state"); - var upload = widget.control.attrString("upload"); - var dialogTitle = widget.control.attrString("dialogTitle"); - var fileName = widget.control.attrString("fileName"); - var initialDirectory = widget.control.attrString("initialDirectory"); - var allowMultiple = widget.control.attrBool("allowMultiple", false)!; - var allowedExtensions = - parseStringList(widget.control, "allowedExtensions"); - FileType fileType = FileType.values.firstWhere( - (m) => - m.name.toLowerCase() == - widget.control.attrString("fileType", "")!.toLowerCase(), - orElse: () => FileType.any); - if (allowedExtensions != null && allowedExtensions.isNotEmpty) { - fileType = FileType.custom; - } - - debugPrint("FilePicker _state: $_state, state: $state"); - - resetDialogState() { - _state = null; - var fletServices = FletAppServices.of(context); - List<Map<String, String>> props = [ - {"i": widget.control.id, "state": ""} - ]; - fletServices.store.dispatch(UpdateControlPropsAction( - UpdateControlPropsPayload(props: props))); - fletServices.server.updateControlProps(props: props); - } - - sendEvent() { - if (defaultTargetPlatform != TargetPlatform.windows || - !isDesktop()) { - resetDialogState(); - } - var fletServices = FletAppServices.of(context); - fletServices.server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "result", - eventData: json.encode(FilePickerResultEvent( - path: _path, - files: _files - ?.map((f) => FilePickerFile( - name: f.name, - path: kIsWeb ? null : f.path, - size: f.size)) - .toList()))); - } - - if (_state != state) { - _path = null; - _files = null; - _state = state; - - if (isDesktop() && - defaultTargetPlatform == TargetPlatform.windows) { - resetDialogState(); - } - - // pickFiles - if (state?.toLowerCase() == "pickfiles") { - FilePicker.platform - .pickFiles( - dialogTitle: dialogTitle, - initialDirectory: initialDirectory, - lockParentWindow: true, - type: fileType, - allowedExtensions: allowedExtensions, - allowMultiple: allowMultiple, - withData: false, - withReadStream: true) - .then((result) { - debugPrint("pickFiles() completed"); - _files = result?.files; - sendEvent(); - }); - } - // saveFile - else if (state?.toLowerCase() == "savefile" && !kIsWeb) { - FilePicker.platform - .saveFile( - dialogTitle: dialogTitle, - fileName: fileName, - initialDirectory: initialDirectory, - lockParentWindow: true, - type: fileType, - allowedExtensions: allowedExtensions, - ) - .then((result) { - debugPrint("saveFile() completed"); - _path = result; - sendEvent(); - }); - } - // saveFile - else if (state?.toLowerCase() == "getdirectorypath" && !kIsWeb) { - FilePicker.platform - .getDirectoryPath( - dialogTitle: dialogTitle, - initialDirectory: initialDirectory, - lockParentWindow: true, - ) - .then((result) { - debugPrint("getDirectoryPath() completed"); - _path = result; - sendEvent(); - }); - } - } - - // upload files - if (_upload != upload && upload != null && _files != null) { - _upload = upload; - uploadFiles(upload, FletAppServices.of(context).server, pageUri!); - } - - return widget.nextChild ?? const SizedBox.shrink(); - }); + return withPageArgs((context, pageArgs) { + var state = widget.control.attrString("state"); + var upload = widget.control.attrString("upload"); + var dialogTitle = widget.control.attrString("dialogTitle"); + var fileName = widget.control.attrString("fileName"); + var initialDirectory = widget.control.attrString("initialDirectory"); + var allowMultiple = widget.control.attrBool("allowMultiple", false)!; + var allowedExtensions = + parseStringList(widget.control, "allowedExtensions"); + FileType fileType = FileType.values.firstWhere( + (m) => + m.name.toLowerCase() == + widget.control.attrString("fileType", "")!.toLowerCase(), + orElse: () => FileType.any); + if (allowedExtensions != null && allowedExtensions.isNotEmpty) { + fileType = FileType.custom; + } + + debugPrint("FilePicker _state: $_state, state: $state"); + + resetDialogState() { + _state = null; + updateControlProps(widget.control.id, {"state": ""}); + } + + sendEvent() { + if (defaultTargetPlatform != TargetPlatform.windows || !isDesktop()) { + resetDialogState(); + } + sendControlEvent( + widget.control.id, + "result", + json.encode(FilePickerResultEvent( + path: _path, + files: _files + ?.map((f) => FilePickerFile( + name: f.name, + path: kIsWeb ? null : f.path, + size: f.size)) + .toList()))); + } + + if (_state != state) { + _path = null; + _files = null; + _state = state; + + if (isDesktop() && defaultTargetPlatform == TargetPlatform.windows) { + resetDialogState(); + } + + // pickFiles + if (state?.toLowerCase() == "pickfiles") { + FilePicker.platform + .pickFiles( + dialogTitle: dialogTitle, + initialDirectory: initialDirectory, + lockParentWindow: true, + type: fileType, + allowedExtensions: allowedExtensions, + allowMultiple: allowMultiple, + withData: false, + withReadStream: true) + .then((result) { + debugPrint("pickFiles() completed"); + _files = result?.files; + sendEvent(); + }); + } + // saveFile + else if (state?.toLowerCase() == "savefile" && !kIsWeb) { + FilePicker.platform + .saveFile( + dialogTitle: dialogTitle, + fileName: fileName, + initialDirectory: initialDirectory, + lockParentWindow: true, + type: fileType, + allowedExtensions: allowedExtensions, + ) + .then((result) { + debugPrint("saveFile() completed"); + _path = result; + sendEvent(); + }); + } + // saveFile + else if (state?.toLowerCase() == "getdirectorypath" && !kIsWeb) { + FilePicker.platform + .getDirectoryPath( + dialogTitle: dialogTitle, + initialDirectory: initialDirectory, + lockParentWindow: true, + ) + .then((result) { + debugPrint("getDirectoryPath() completed"); + _path = result; + sendEvent(); + }); + } + } + + // upload files + if (_upload != upload && upload != null && _files != null) { + _upload = upload; + uploadFiles( + upload, FletAppServices.of(context).server, pageArgs.pageUri!); + } + + return widget.nextChild ?? const SizedBox.shrink(); + }); } Future uploadFiles(String filesJson, FletServer server, Uri pageUri) async { @@ -233,10 +265,10 @@ class _FilePickerControlState extends State<FilePickerControl> { void sendProgress( FletServer server, String name, double? progress, String? error) { - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "upload", - eventData: json.encode(FilePickerUploadProgressEvent( + sendControlEvent( + widget.control.id, + "upload", + json.encode(FilePickerUploadProgressEvent( name: name, progress: progress, error: error))); } diff --git a/package/lib/src/controls/flet_control_stateful_mixin.dart b/package/lib/src/controls/flet_control_stateful_mixin.dart new file mode 100644 index 000000000..090f2d151 --- /dev/null +++ b/package/lib/src/controls/flet_control_stateful_mixin.dart @@ -0,0 +1,37 @@ +import 'package:flutter/widgets.dart'; + +import '../actions.dart'; +import '../flet_app_services.dart'; +import '../protocol/update_control_props_payload.dart'; + +mixin FletControlStatefulMixin<T extends StatefulWidget> on State<T> { + void updateControlProps(String id, Map<String, String> props, + {bool clientOnly = false}) { + var appServices = FletAppServices.of(context); + var dispatch = appServices.store.dispatch; + Map<String, String> allProps = {"i": id}; + for (var entry in props.entries) { + allProps[entry.key] = entry.value; + } + dispatch( + UpdateControlPropsAction(UpdateControlPropsPayload(props: [allProps]))); + if (!clientOnly) { + appServices.server.updateControlProps(props: [allProps]); + } + } + + void sendControlEvent(String controlId, String eventName, String eventData) { + FletAppServices.of(context).server.sendPageEvent( + eventTarget: controlId, eventName: eventName, eventData: eventData); + } + + void subscribeMethods(String controlId, + Future<String?> Function(String, Map<String, String>) methodHandler) { + FletAppServices.of(context).server.controlInvokeMethods[controlId] = + methodHandler; + } + + void unsubscribeMethods(String controlId) { + FletAppServices.of(context).server.controlInvokeMethods.remove(controlId); + } +} diff --git a/package/lib/src/controls/flet_control_stateless_mixin.dart b/package/lib/src/controls/flet_control_stateless_mixin.dart new file mode 100644 index 000000000..a086a1eb0 --- /dev/null +++ b/package/lib/src/controls/flet_control_stateless_mixin.dart @@ -0,0 +1,11 @@ +import 'package:flutter/widgets.dart'; + +import '../flet_app_services.dart'; + +mixin FletControlStatelessMixin on StatelessWidget { + void sendControlEvent(BuildContext context, String controlId, + String eventName, String eventData) { + FletAppServices.of(context).server.sendPageEvent( + eventTarget: controlId, eventName: eventName, eventData: eventData); + } +} diff --git a/package/lib/src/controls/flet_store_mixin.dart b/package/lib/src/controls/flet_store_mixin.dart new file mode 100644 index 000000000..24a69de2d --- /dev/null +++ b/package/lib/src/controls/flet_store_mixin.dart @@ -0,0 +1,90 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_redux/flutter_redux.dart'; + +import '../models/app_state.dart'; +import '../models/control.dart'; +import '../models/control_ancestor_view_model.dart'; +import '../models/control_tree_view_model.dart'; +import '../models/control_view_model.dart'; +import '../models/controls_view_model.dart'; +import '../models/page_args_model.dart'; +import '../models/page_size_view_model.dart'; + +mixin FletStoreMixin { + Widget withPageArgs(Widget Function(BuildContext, PageArgsModel) build) { + return StoreConnector<AppState, PageArgsModel>( + distinct: true, + converter: (store) => PageArgsModel.fromStore(store), + builder: build); + } + + Widget withPageSize(Widget Function(BuildContext, PageSizeViewModel) build) { + return StoreConnector<AppState, PageSizeViewModel>( + distinct: true, + converter: (store) => PageSizeViewModel.fromStore(store), + builder: build); + } + + Widget withPagePlatform(Widget Function(BuildContext, TargetPlatform) build) { + return StoreConnector<AppState, TargetPlatform>( + distinct: true, + converter: (store) => TargetPlatform.values.firstWhere( + (a) => + a.name.toLowerCase() == + store.state.controls["page"]! + .attrString("platform", "")! + .toLowerCase(), + orElse: () => defaultTargetPlatform), + builder: build); + } + + Widget withControl( + String id, Widget Function(BuildContext, ControlViewModel?) build) { + return StoreConnector<AppState, ControlViewModel?>( + distinct: true, + converter: (store) { + return ControlViewModel.fromStore(store, id); + }, + ignoreChange: (state) { + return state.controls[id] == null; + }, + builder: build); + } + + Widget withControlTree(Control control, + Widget Function(BuildContext, ControlTreeViewModel) build) { + return StoreConnector<AppState, ControlTreeViewModel>( + distinct: true, + converter: (store) => ControlTreeViewModel.fromStore(store, control), + builder: build); + } + + Widget withControlAncestor(String id, String ancestorType, + Widget Function(BuildContext, ControlAncestorViewModel) build) { + return StoreConnector<AppState, ControlAncestorViewModel>( + distinct: true, + converter: (store) => + ControlAncestorViewModel.fromStore(store, id, ancestorType), + ignoreChange: (state) { + return state.controls[id] == null; + }, + builder: build); + } + + Widget withControls(Iterable<String> controlIds, + Widget Function(BuildContext, ControlsViewModel) build) { + return StoreConnector<AppState, ControlsViewModel>( + distinct: true, + converter: (store) => ControlsViewModel.fromStore(store, controlIds), + ignoreChange: (state) { + for (var id in controlIds) { + if (state.controls[id] == null) { + return true; + } + } + return false; + }, + builder: build); + } +} diff --git a/package/lib/src/controls/floating_action_button.dart b/package/lib/src/controls/floating_action_button.dart index 5b0c5cc75..a137bfc13 100644 --- a/package/lib/src/controls/floating_action_button.dart +++ b/package/lib/src/controls/floating_action_button.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import '../flet_app_services.dart'; import '../models/control.dart'; import '../utils/borders.dart'; import '../utils/colors.dart'; @@ -9,8 +8,10 @@ import '../utils/launch_url.dart'; import '../utils/transforms.dart'; import 'create_control.dart'; import 'error.dart'; +import 'flet_control_stateless_mixin.dart'; -class FloatingActionButtonControl extends StatelessWidget { +class FloatingActionButtonControl extends StatelessWidget + with FletControlStatelessMixin { final Control? parent; final Control control; final List<Control> children; @@ -47,8 +48,7 @@ class FloatingActionButtonControl extends StatelessWidget { if (url != "") { openWebBrowser(url, webWindowName: urlTarget); } - FletAppServices.of(context).server.sendPageEvent( - eventTarget: control.id, eventName: "click", eventData: ""); + sendControlEvent(context, control.id, "click", ""); }; if (text == null && icon == null && contentCtrls.isEmpty) { diff --git a/package/lib/src/controls/gesture_detector.dart b/package/lib/src/controls/gesture_detector.dart index 927c2d16c..c79c82bcb 100644 --- a/package/lib/src/controls/gesture_detector.dart +++ b/package/lib/src/controls/gesture_detector.dart @@ -4,11 +4,11 @@ import 'dart:convert'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import '../flet_app_services.dart'; import '../models/control.dart'; import '../utils/mouse.dart'; import 'create_control.dart'; import 'error.dart'; +import 'flet_control_stateful_mixin.dart'; class GestureDetectorControl extends StatefulWidget { final Control? parent; @@ -27,7 +27,8 @@ class GestureDetectorControl extends StatefulWidget { State<GestureDetectorControl> createState() => _GestureDetectorControlState(); } -class _GestureDetectorControlState extends State<GestureDetectorControl> { +class _GestureDetectorControlState extends State<GestureDetectorControl> + with FletControlStatefulMixin { int _panTimestamp = DateTime.now().millisecondsSinceEpoch; double _panX = 0; double _panY = 0; @@ -61,8 +62,6 @@ class _GestureDetectorControlState extends State<GestureDetectorControl> { widget.children.where((c) => c.name == "content" && c.isVisible); bool disabled = widget.control.isDisabled || widget.parentDisabled; - var server = FletAppServices.of(context).server; - void sendEvent(String eventName, dynamic eventData) { var d = ""; if (eventData is String) { @@ -72,8 +71,7 @@ class _GestureDetectorControlState extends State<GestureDetectorControl> { } debugPrint("GestureDetector ${widget.control.id} $eventName"); - server.sendPageEvent( - eventTarget: widget.control.id, eventName: eventName, eventData: d); + sendControlEvent(widget.control.id, eventName, d); } var onHover = widget.control.attrBool("onHover", false)!; diff --git a/package/lib/src/controls/grid_view.dart b/package/lib/src/controls/grid_view.dart index 95403508e..64eb6e3f1 100644 --- a/package/lib/src/controls/grid_view.dart +++ b/package/lib/src/controls/grid_view.dart @@ -13,15 +13,13 @@ class GridViewControl extends StatefulWidget { final Control control; final bool parentDisabled; final List<Control> children; - final dynamic dispatch; const GridViewControl( {super.key, this.parent, required this.control, required this.children, - required this.parentDisabled, - required this.dispatch}); + required this.parentDisabled}); @override State<GridViewControl> createState() => _GridViewControlState(); @@ -99,7 +97,6 @@ class _GridViewControlState extends State<GridViewControl> { child = ScrollableControl( control: widget.control, scrollDirection: horizontal ? Axis.horizontal : Axis.vertical, - dispatch: widget.dispatch, scrollController: _controller, child: child, ); diff --git a/package/lib/src/controls/haptic_feedback.dart b/package/lib/src/controls/haptic_feedback.dart index 53d153450..40c1f8023 100644 --- a/package/lib/src/controls/haptic_feedback.dart +++ b/package/lib/src/controls/haptic_feedback.dart @@ -1,9 +1,8 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; -import '../flet_app_services.dart'; -import '../flet_server.dart'; import '../models/control.dart'; +import 'flet_control_stateful_mixin.dart'; class HapticFeedbackControl extends StatefulWidget { final Control? parent; @@ -20,12 +19,11 @@ class HapticFeedbackControl extends StatefulWidget { State<HapticFeedbackControl> createState() => _HapticFeedbackControlState(); } -class _HapticFeedbackControlState extends State<HapticFeedbackControl> { - FletServer? _server; - +class _HapticFeedbackControlState extends State<HapticFeedbackControl> + with FletControlStatefulMixin { @override void deactivate() { - _server?.controlInvokeMethods.remove(widget.control.id); + unsubscribeMethods(widget.control.id); super.deactivate(); } @@ -33,9 +31,7 @@ class _HapticFeedbackControlState extends State<HapticFeedbackControl> { Widget build(BuildContext context) { debugPrint("HapticFeedback build: ${widget.control.id}"); - _server = FletAppServices.of(context).server; - _server?.controlInvokeMethods[widget.control.id] = - (methodName, args) async { + subscribeMethods(widget.control.id, (methodName, args) async { switch (methodName) { case "heavy_impact": HapticFeedback.heavyImpact(); @@ -51,7 +47,7 @@ class _HapticFeedbackControlState extends State<HapticFeedbackControl> { break; } return null; - }; + }); return widget.nextChild ?? const SizedBox.shrink(); } diff --git a/package/lib/src/controls/icon_button.dart b/package/lib/src/controls/icon_button.dart index 52981eb58..1fcb1efad 100644 --- a/package/lib/src/controls/icon_button.dart +++ b/package/lib/src/controls/icon_button.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import '../flet_app_services.dart'; import '../models/control.dart'; import '../utils/buttons.dart'; import '../utils/colors.dart'; @@ -8,6 +7,7 @@ import '../utils/icons.dart'; import '../utils/launch_url.dart'; import 'create_control.dart'; import 'error.dart'; +import 'flet_control_stateful_mixin.dart'; class IconButtonControl extends StatefulWidget { final Control? parent; @@ -26,7 +26,8 @@ class IconButtonControl extends StatefulWidget { State<IconButtonControl> createState() => _IconButtonControlState(); } -class _IconButtonControlState extends State<IconButtonControl> { +class _IconButtonControlState extends State<IconButtonControl> + with FletControlStatefulMixin { late final FocusNode _focusNode; String? _lastFocusValue; @@ -45,10 +46,8 @@ class _IconButtonControlState extends State<IconButtonControl> { } void _onFocusChange() { - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, - eventName: _focusNode.hasFocus ? "focus" : "blur", - eventData: ""); + sendControlEvent( + widget.control.id, _focusNode.hasFocus ? "focus" : "blur", ""); } @override @@ -80,10 +79,7 @@ class _IconButtonControlState extends State<IconButtonControl> { if (url != "") { openWebBrowser(url, webWindowName: urlTarget); } - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "click", - eventData: ""); + sendControlEvent(widget.control.id, "click", ""); }; Widget? button; diff --git a/package/lib/src/controls/image.dart b/package/lib/src/controls/image.dart index 74888e26d..13a2bd4b9 100644 --- a/package/lib/src/controls/image.dart +++ b/package/lib/src/controls/image.dart @@ -4,20 +4,20 @@ import 'dart:io' as io; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_svg/svg.dart'; -import '../models/app_state.dart'; import '../models/control.dart'; -import '../models/page_args_model.dart'; import '../utils/borders.dart'; import '../utils/collections.dart'; import '../utils/colors.dart'; import '../utils/images.dart'; import 'create_control.dart'; import 'error.dart'; +import 'flet_control_stateless_mixin.dart'; +import 'flet_store_mixin.dart'; -class ImageControl extends StatelessWidget { +class ImageControl extends StatelessWidget + with FletControlStatelessMixin, FletStoreMixin { final Control? parent; final List<Control> children; final Control control; @@ -57,43 +57,15 @@ class ImageControl extends StatelessWidget { var errorContentCtrls = children.where((c) => c.name == "error_content" && c.isVisible); - return StoreConnector<AppState, PageArgsModel>( - distinct: true, - converter: (store) => PageArgsModel.fromStore(store), - builder: (context, pageArgs) { - Widget? image; + return withPageArgs((context, pageArgs) { + Widget? image; - if (srcBase64 != "") { - try { - Uint8List bytes = base64Decode(srcBase64); - if (arrayIndexOf( - bytes, Uint8List.fromList(utf8.encode(svgTag))) != - -1) { - image = SvgPicture.memory(bytes, - width: width, - height: height, - fit: fit ?? BoxFit.contain, - colorFilter: color != null - ? ColorFilter.mode( - color, colorBlendMode ?? BlendMode.srcIn) - : null, - semanticsLabel: semanticsLabel); - } else { - image = Image.memory(bytes, - width: width, - height: height, - repeat: repeat, - fit: fit, - color: color, - colorBlendMode: colorBlendMode, - gaplessPlayback: gaplessPlayback ?? true, - semanticLabel: semanticsLabel); - } - } catch (ex) { - return ErrorControl("Error decoding base64: ${ex.toString()}"); - } - } else if (src.contains(svgTag)) { - image = SvgPicture.memory(Uint8List.fromList(utf8.encode(src)), + if (srcBase64 != "") { + try { + Uint8List bytes = base64Decode(srcBase64); + if (arrayIndexOf(bytes, Uint8List.fromList(utf8.encode(svgTag))) != + -1) { + image = SvgPicture.memory(bytes, width: width, height: height, fit: fit ?? BoxFit.contain, @@ -102,74 +74,95 @@ class ImageControl extends StatelessWidget { : null, semanticsLabel: semanticsLabel); } else { - var assetSrc = - getAssetSrc(src, pageArgs.pageUri!, pageArgs.assetsDir); + image = Image.memory(bytes, + width: width, + height: height, + repeat: repeat, + fit: fit, + color: color, + colorBlendMode: colorBlendMode, + gaplessPlayback: gaplessPlayback ?? true, + semanticLabel: semanticsLabel); + } + } catch (ex) { + return ErrorControl("Error decoding base64: ${ex.toString()}"); + } + } else if (src.contains(svgTag)) { + image = SvgPicture.memory(Uint8List.fromList(utf8.encode(src)), + width: width, + height: height, + fit: fit ?? BoxFit.contain, + colorFilter: color != null + ? ColorFilter.mode(color, colorBlendMode ?? BlendMode.srcIn) + : null, + semanticsLabel: semanticsLabel); + } else { + var assetSrc = getAssetSrc(src, pageArgs.pageUri!, pageArgs.assetsDir); - if (assetSrc.isFile) { - // from File - if (assetSrc.path.endsWith(".svg")) { - image = getSvgPictureFromFile( - src: assetSrc.path, - width: width, - height: height, - fit: fit ?? BoxFit.contain, - color: color, - blendMode: colorBlendMode ?? BlendMode.srcIn, - semanticsLabel: semanticsLabel); - } else { - image = Image.file( - io.File(assetSrc.path), - width: width, - height: height, - repeat: repeat, - fit: fit, - color: color, - gaplessPlayback: gaplessPlayback ?? false, - colorBlendMode: colorBlendMode, - semanticLabel: semanticsLabel, - errorBuilder: errorContentCtrls.isNotEmpty - ? (context, error, stackTrace) { - return createControl( - control, errorContentCtrls.first.id, disabled); - } - : null, - ); - } - } else { - // URL - if (assetSrc.path.endsWith(".svg")) { - image = SvgPicture.network(assetSrc.path, - width: width, - height: height, - fit: fit ?? BoxFit.contain, - colorFilter: color != null - ? ColorFilter.mode( - color, colorBlendMode ?? BlendMode.srcIn) - : null, - semanticsLabel: semanticsLabel); - } else { - image = Image.network(assetSrc.path, - width: width, - height: height, - repeat: repeat, - fit: fit, - color: color, - gaplessPlayback: gaplessPlayback ?? false, - colorBlendMode: colorBlendMode, - semanticLabel: semanticsLabel, - errorBuilder: errorContentCtrls.isNotEmpty - ? (context, error, stackTrace) { - return createControl( - control, errorContentCtrls.first.id, disabled); - } - : null); - } - } + if (assetSrc.isFile) { + // from File + if (assetSrc.path.endsWith(".svg")) { + image = getSvgPictureFromFile( + src: assetSrc.path, + width: width, + height: height, + fit: fit ?? BoxFit.contain, + color: color, + blendMode: colorBlendMode ?? BlendMode.srcIn, + semanticsLabel: semanticsLabel); + } else { + image = Image.file( + io.File(assetSrc.path), + width: width, + height: height, + repeat: repeat, + fit: fit, + color: color, + gaplessPlayback: gaplessPlayback ?? false, + colorBlendMode: colorBlendMode, + semanticLabel: semanticsLabel, + errorBuilder: errorContentCtrls.isNotEmpty + ? (context, error, stackTrace) { + return createControl( + control, errorContentCtrls.first.id, disabled); + } + : null, + ); + } + } else { + // URL + if (assetSrc.path.endsWith(".svg")) { + image = SvgPicture.network(assetSrc.path, + width: width, + height: height, + fit: fit ?? BoxFit.contain, + colorFilter: color != null + ? ColorFilter.mode(color, colorBlendMode ?? BlendMode.srcIn) + : null, + semanticsLabel: semanticsLabel); + } else { + image = Image.network(assetSrc.path, + width: width, + height: height, + repeat: repeat, + fit: fit, + color: color, + gaplessPlayback: gaplessPlayback ?? false, + colorBlendMode: colorBlendMode, + semanticLabel: semanticsLabel, + errorBuilder: errorContentCtrls.isNotEmpty + ? (context, error, stackTrace) { + return createControl( + control, errorContentCtrls.first.id, disabled); + } + : null); } + } + } - return constrainedControl( - context, _clipCorners(image, control), parent, control); - }); + return constrainedControl( + context, _clipCorners(image, control), parent, control); + }); } Widget _clipCorners(Widget image, Control control) { diff --git a/package/lib/src/controls/linechart.dart b/package/lib/src/controls/linechart.dart index a20946bcf..48b0707cc 100644 --- a/package/lib/src/controls/linechart.dart +++ b/package/lib/src/controls/linechart.dart @@ -1,18 +1,14 @@ import 'dart:convert'; import 'package:collection/collection.dart'; +import 'package:equatable/equatable.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; +import 'package:redux/redux.dart'; -import '../flet_app_services.dart'; import '../models/app_state.dart'; -import '../models/chart_axis_view_model.dart'; import '../models/control.dart'; -import '../models/linechart_data_point_view_model.dart'; -import '../models/linechart_data_view_model.dart'; -import '../models/linechart_event_data.dart'; -import '../models/linechart_view_model.dart'; import '../utils/animations.dart'; import '../utils/borders.dart'; import '../utils/charts.dart'; @@ -21,7 +17,139 @@ import '../utils/gradient.dart'; import '../utils/numbers.dart'; import '../utils/shadows.dart'; import '../utils/text.dart'; +import 'charts.dart'; import 'create_control.dart'; +import 'flet_control_stateful_mixin.dart'; + +class LineChartDataPointViewModel extends Equatable { + final Control control; + final double x; + final double y; + final String? tooltip; + + const LineChartDataPointViewModel( + {required this.control, + required this.x, + required this.y, + required this.tooltip}); + + static LineChartDataPointViewModel fromStore( + Store<AppState> store, Control control) { + return LineChartDataPointViewModel( + control: control, + x: control.attrDouble("x")!, + y: control.attrDouble("y")!, + tooltip: control.attrString("tooltip")); + } + + @override + List<Object?> get props => [control]; +} + +class LineChartDataViewModel extends Equatable { + final Control control; + final List<LineChartDataPointViewModel> dataPoints; + + const LineChartDataViewModel( + {required this.control, required this.dataPoints}); + + static LineChartDataViewModel fromStore( + Store<AppState> store, Control control) { + return LineChartDataViewModel( + control: control, + dataPoints: store.state.controls[control.id]!.childIds + .map((childId) => store.state.controls[childId]) + .whereNotNull() + .where((c) => c.isVisible) + .map((c) => LineChartDataPointViewModel.fromStore(store, c)) + .toList()); + } + + @override + List<Object?> get props => [control, dataPoints]; +} + +class LineChartEventData extends Equatable { + final String eventType; + final List<LineChartEventDataSpot> barSpots; + + const LineChartEventData({required this.eventType, required this.barSpots}); + + Map<String, dynamic> toJson() => <String, dynamic>{ + 'type': eventType, + 'spots': barSpots, + }; + + @override + List<Object?> get props => [eventType, barSpots]; +} + +class LineChartEventDataSpot extends Equatable { + final int barIndex; + final int spotIndex; + + const LineChartEventDataSpot( + {required this.barIndex, required this.spotIndex}); + + Map<String, dynamic> toJson() => <String, dynamic>{ + 'bar_index': barIndex, + 'spot_index': spotIndex, + }; + + @override + List<Object?> get props => [barIndex, spotIndex]; +} + +class LineChartViewModel extends Equatable { + final Control control; + final ChartAxisViewModel? leftAxis; + final ChartAxisViewModel? topAxis; + final ChartAxisViewModel? rightAxis; + final ChartAxisViewModel? bottomAxis; + final List<LineChartDataViewModel> dataSeries; + + const LineChartViewModel( + {required this.control, + required this.leftAxis, + required this.topAxis, + required this.rightAxis, + required this.bottomAxis, + required this.dataSeries}); + + static LineChartViewModel fromStore( + Store<AppState> store, Control control, List<Control> children) { + var leftAxisCtrls = + children.where((c) => c.type == "axis" && c.name == "l" && c.isVisible); + var topAxisCtrls = + children.where((c) => c.type == "axis" && c.name == "t" && c.isVisible); + var rightAxisCtrls = + children.where((c) => c.type == "axis" && c.name == "r" && c.isVisible); + var bottomAxisCtrls = + children.where((c) => c.type == "axis" && c.name == "b" && c.isVisible); + return LineChartViewModel( + control: control, + leftAxis: leftAxisCtrls.isNotEmpty + ? ChartAxisViewModel.fromStore(store, leftAxisCtrls.first) + : null, + topAxis: topAxisCtrls.isNotEmpty + ? ChartAxisViewModel.fromStore(store, topAxisCtrls.first) + : null, + rightAxis: rightAxisCtrls.isNotEmpty + ? ChartAxisViewModel.fromStore(store, rightAxisCtrls.first) + : null, + bottomAxis: bottomAxisCtrls.isNotEmpty + ? ChartAxisViewModel.fromStore(store, bottomAxisCtrls.first) + : null, + dataSeries: children + .where((c) => c.type == "data" && c.isVisible) + .map((c) => LineChartDataViewModel.fromStore(store, c)) + .toList()); + } + + @override + List<Object?> get props => + [control, leftAxis, rightAxis, topAxis, bottomAxis, dataSeries]; +} class LineChartControl extends StatefulWidget { final Control? parent; @@ -40,7 +168,8 @@ class LineChartControl extends StatefulWidget { State<LineChartControl> createState() => _LineChartControlState(); } -class _LineChartControlState extends State<LineChartControl> { +class _LineChartControlState extends State<LineChartControl> + with FletControlStatefulMixin { LineChartEventData? _eventData; @override @@ -242,10 +371,8 @@ class _LineChartControlState extends State<LineChartControl> { _eventData = eventData; debugPrint( "LineChart ${widget.control.id} ${eventData.eventType}"); - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "chart_event", - eventData: json.encode(eventData)); + sendControlEvent(widget.control.id, "chart_event", + json.encode(eventData)); } } : null, diff --git a/package/lib/src/controls/list_tile.dart b/package/lib/src/controls/list_tile.dart index 800bdd2bd..0b013b47c 100644 --- a/package/lib/src/controls/list_tile.dart +++ b/package/lib/src/controls/list_tile.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; -import '../flet_app_services.dart'; import '../models/control.dart'; import '../utils/edge_insets.dart'; import '../utils/launch_url.dart'; import 'create_control.dart'; +import 'flet_control_stateless_mixin.dart'; class ListTileClicks extends InheritedWidget { const ListTileClicks({ @@ -23,7 +23,7 @@ class ListTileClicks extends InheritedWidget { bool updateShouldNotify(ListTileClicks oldWidget) => true; } -class ListTileControl extends StatelessWidget { +class ListTileControl extends StatelessWidget with FletControlStatelessMixin { final Control? parent; final Control control; final List<Control> children; @@ -41,8 +41,6 @@ class ListTileControl extends StatelessWidget { Widget build(BuildContext context) { debugPrint("ListTile build: ${control.id}"); - final server = FletAppServices.of(context).server; - var leadingCtrls = children.where((c) => c.name == "leading" && c.isVisible); var titleCtrls = children.where((c) => c.name == "title" && c.isVisible); @@ -72,8 +70,7 @@ class ListTileControl extends StatelessWidget { openWebBrowser(url, webWindowName: urlTarget); } if (onclick) { - server.sendPageEvent( - eventTarget: control.id, eventName: "click", eventData: ""); + sendControlEvent(context, control.id, "click", ""); } } : null; @@ -81,10 +78,7 @@ class ListTileControl extends StatelessWidget { Function()? onLongPress = onLongPressDefined && !disabled ? () { debugPrint("Button ${control.id} clicked!"); - server.sendPageEvent( - eventTarget: control.id, - eventName: "long_press", - eventData: ""); + sendControlEvent(context, control.id, "long_press", ""); } : null; diff --git a/package/lib/src/controls/list_view.dart b/package/lib/src/controls/list_view.dart index 784e2ce45..37a27cd4a 100644 --- a/package/lib/src/controls/list_view.dart +++ b/package/lib/src/controls/list_view.dart @@ -13,15 +13,13 @@ class ListViewControl extends StatefulWidget { final Control control; final bool parentDisabled; final List<Control> children; - final dynamic dispatch; const ListViewControl( {super.key, this.parent, required this.control, required this.children, - required this.parentDisabled, - required this.dispatch}); + required this.parentDisabled}); @override State<ListViewControl> createState() => _ListViewControlState(); @@ -115,7 +113,6 @@ class _ListViewControlState extends State<ListViewControl> { child = ScrollableControl( control: widget.control, scrollDirection: horizontal ? Axis.horizontal : Axis.vertical, - dispatch: widget.dispatch, scrollController: _controller, child: child, ); diff --git a/package/lib/src/controls/markdown.dart b/package/lib/src/controls/markdown.dart index 61cd55c3e..bd64fc7e9 100644 --- a/package/lib/src/controls/markdown.dart +++ b/package/lib/src/controls/markdown.dart @@ -1,24 +1,24 @@ import 'package:flutter/material.dart'; import 'package:flutter_highlight/theme_map.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; -import 'package:flutter_redux/flutter_redux.dart'; import 'package:markdown/markdown.dart' as md; -import '../flet_app_services.dart'; -import '../models/app_state.dart'; import '../models/control.dart'; -import '../models/page_args_model.dart'; import '../utils/launch_url.dart'; import '../utils/text.dart'; import '../utils/uri.dart'; import 'create_control.dart'; +import 'flet_control_stateless_mixin.dart'; +import 'flet_store_mixin.dart'; import 'highlight_view.dart'; -class MarkdownControl extends StatelessWidget { +class MarkdownControl extends StatelessWidget + with FletControlStatelessMixin, FletStoreMixin { final Control? parent; final Control control; - const MarkdownControl({super.key, required this.parent, required this.control}); + const MarkdownControl( + {super.key, required this.parent, required this.control}); @override Widget build(BuildContext context) { @@ -52,35 +52,29 @@ class MarkdownControl extends StatelessWidget { var autoFollowLinks = control.attrBool("autoFollowLinks", false)!; var autoFollowLinksTarget = control.attrString("autoFollowLinksTarget"); - return StoreConnector<AppState, PageArgsModel>( - distinct: true, - converter: (store) => PageArgsModel.fromStore(store), - builder: (context, pageArgs) { - Widget markdown = MarkdownBody( - data: value, - selectable: control.attrBool("selectable", false)!, - imageDirectory: pageArgs.assetsDir != "" - ? pageArgs.assetsDir - : getBaseUri(pageArgs.pageUri!).toString(), - extensionSet: extensionSet, - builders: { - 'code': - CodeElementBuilder(codeTheme.toLowerCase(), mdStyleSheet), - }, - styleSheet: mdStyleSheet, - onTapLink: (String text, String? href, String title) { - debugPrint("Markdown link tapped ${control.id} clicked: $href"); - if (autoFollowLinks && href != null) { - openWebBrowser(href, webWindowName: autoFollowLinksTarget); - } - FletAppServices.of(context).server.sendPageEvent( - eventTarget: control.id, - eventName: "tap_link", - eventData: href?.toString() ?? ""); - }); - - return constrainedControl(context, markdown, parent, control); - }); + return withPageArgs((context, pageArgs) { + Widget markdown = MarkdownBody( + data: value, + selectable: control.attrBool("selectable", false)!, + imageDirectory: pageArgs.assetsDir != "" + ? pageArgs.assetsDir + : getBaseUri(pageArgs.pageUri!).toString(), + extensionSet: extensionSet, + builders: { + 'code': CodeElementBuilder(codeTheme.toLowerCase(), mdStyleSheet), + }, + styleSheet: mdStyleSheet, + onTapLink: (String text, String? href, String title) { + debugPrint("Markdown link tapped ${control.id} clicked: $href"); + if (autoFollowLinks && href != null) { + openWebBrowser(href, webWindowName: autoFollowLinksTarget); + } + sendControlEvent( + context, control.id, "tap_link", href?.toString() ?? ""); + }); + + return constrainedControl(context, markdown, parent, control); + }); } } diff --git a/package/lib/src/controls/menu_item_button.dart b/package/lib/src/controls/menu_item_button.dart index 9796c8994..e5cfb3d25 100644 --- a/package/lib/src/controls/menu_item_button.dart +++ b/package/lib/src/controls/menu_item_button.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; -import '../flet_app_services.dart'; import '../models/control.dart'; import '../utils/buttons.dart'; import 'create_control.dart'; +import 'flet_control_stateful_mixin.dart'; class MenuItemButtonControl extends StatefulWidget { final Control? parent; @@ -22,7 +22,8 @@ class MenuItemButtonControl extends StatefulWidget { State<MenuItemButtonControl> createState() => _MenuItemButtonControlState(); } -class _MenuItemButtonControlState extends State<MenuItemButtonControl> { +class _MenuItemButtonControlState extends State<MenuItemButtonControl> + with FletControlStatefulMixin { late final FocusNode _focusNode; String? _lastFocusValue; @@ -41,10 +42,8 @@ class _MenuItemButtonControlState extends State<MenuItemButtonControl> { } void _onFocusChange() { - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, - eventName: _focusNode.hasFocus ? "focus" : "blur", - eventData: ""); + sendControlEvent( + widget.control.id, _focusNode.hasFocus ? "focus" : "blur", ""); } @override @@ -82,8 +81,6 @@ class _MenuItemButtonControlState extends State<MenuItemButtonControl> { bool onClick = widget.control.attrBool("onClick", false)!; bool onHover = widget.control.attrBool("onHover", false)!; - var server = FletAppServices.of(context).server; - var menuItem = MenuItemButton( focusNode: _focusNode, clipBehavior: clipBehavior, @@ -92,18 +89,12 @@ class _MenuItemButtonControlState extends State<MenuItemButtonControl> { requestFocusOnHover: widget.control.attrBool("focusOnHover", true)!, onHover: onHover && !disabled ? (bool value) { - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "hover", - eventData: "$value"); + sendControlEvent(widget.control.id, "hover", "$value"); } : null, onPressed: onClick && !disabled ? () { - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "click", - eventData: ""); + sendControlEvent(widget.control.id, "click", ""); } : null, leadingIcon: leading.isNotEmpty diff --git a/package/lib/src/controls/navigation_bar.dart b/package/lib/src/controls/navigation_bar.dart index 702dd9a66..36836d14f 100644 --- a/package/lib/src/controls/navigation_bar.dart +++ b/package/lib/src/controls/navigation_bar.dart @@ -1,138 +1,120 @@ import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_redux/flutter_redux.dart'; -import '../actions.dart'; -import '../flet_app_services.dart'; -import '../models/app_state.dart'; import '../models/control.dart'; -import '../models/controls_view_model.dart'; -import '../protocol/update_control_props_payload.dart'; import '../utils/borders.dart'; import '../utils/colors.dart'; import '../utils/icons.dart'; import 'create_control.dart'; import 'cupertino_navigation_bar.dart'; +import 'flet_control_stateful_mixin.dart'; +import 'flet_store_mixin.dart'; class NavigationBarControl extends StatefulWidget { final Control? parent; final Control control; final List<Control> children; final bool parentDisabled; - final dynamic dispatch; const NavigationBarControl( {super.key, this.parent, required this.control, required this.children, - required this.parentDisabled, - required this.dispatch}); + required this.parentDisabled}); @override State<NavigationBarControl> createState() => _NavigationBarControlState(); } -class _NavigationBarControlState extends State<NavigationBarControl> { +class _NavigationBarControlState extends State<NavigationBarControl> + with FletControlStatefulMixin, FletStoreMixin { int _selectedIndex = 0; void _destinationChanged(int index) { _selectedIndex = index; debugPrint("Selected index: $_selectedIndex"); - List<Map<String, String>> props = [ - {"i": widget.control.id, "selectedindex": _selectedIndex.toString()} - ]; - widget.dispatch( - UpdateControlPropsAction(UpdateControlPropsPayload(props: props))); - final server = FletAppServices.of(context).server; - server.updateControlProps(props: props); - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "change", - eventData: _selectedIndex.toString()); + updateControlProps( + widget.control.id, {"selectedindex": _selectedIndex.toString()}); + sendControlEvent(widget.control.id, "change", _selectedIndex.toString()); } @override Widget build(BuildContext context) { debugPrint("NavigationBarControl build: ${widget.control.id}"); - bool adaptive = widget.control.attrBool("adaptive", false)!; - if (adaptive && - (defaultTargetPlatform == TargetPlatform.iOS || - defaultTargetPlatform == TargetPlatform.macOS)) { - return CupertinoNavigationBarControl( - control: widget.control, - children: widget.children, - parentDisabled: widget.parentDisabled, - dispatch: widget.dispatch); - } - - bool disabled = widget.control.isDisabled || widget.parentDisabled; - var selectedIndex = widget.control.attrInt("selectedIndex", 0)!; - - if (_selectedIndex != selectedIndex) { - _selectedIndex = selectedIndex; - } - - NavigationDestinationLabelBehavior? labelBehavior = - NavigationDestinationLabelBehavior.values.firstWhereOrNull((a) => - a.name.toLowerCase() == - widget.control.attrString("labelBehavior", "")!.toLowerCase()); - - var navBar = StoreConnector<AppState, ControlsViewModel>( - distinct: true, - converter: (store) => ControlsViewModel.fromStore( - store, - widget.children - .where((c) => c.isVisible && c.name == null) - .map((c) => c.id)), - builder: (content, viewModel) { - return NavigationBar( - labelBehavior: labelBehavior, - height: widget.control.attrDouble("height"), - elevation: widget.control.attrDouble("elevation"), - shadowColor: HexColor.fromString(Theme.of(context), - widget.control.attrString("shadowColor", "")!), - surfaceTintColor: HexColor.fromString(Theme.of(context), - widget.control.attrString("surfaceTintColor", "")!), - indicatorColor: HexColor.fromString(Theme.of(context), - widget.control.attrString("indicatorColor", "")!), - indicatorShape: - parseOutlinedBorder(widget.control, "indicatorShape"), - backgroundColor: HexColor.fromString( - Theme.of(context), widget.control.attrString("bgColor", "")!), - selectedIndex: _selectedIndex, - onDestinationSelected: _destinationChanged, - destinations: viewModel.controlViews.map((destView) { - var label = destView.control.attrString("label", "")!; - - var icon = - parseIcon(destView.control.attrString("icon", "")!); - var iconContentCtrls = - destView.children.where((c) => c.name == "icon_content"); - - var selectedIcon = parseIcon( - destView.control.attrString("selectedIcon", "")!); - var selectedIconContentCtrls = destView.children - .where((c) => c.name == "selected_icon_content"); - - return NavigationDestination( - tooltip: destView.control.attrString("tooltip", "")!, - icon: iconContentCtrls.isNotEmpty - ? createControl(destView.control, - iconContentCtrls.first.id, disabled) - : Icon(icon), - selectedIcon: selectedIconContentCtrls.isNotEmpty - ? createControl(destView.control, - selectedIconContentCtrls.first.id, disabled) - : selectedIcon != null - ? Icon(selectedIcon) - : null, - label: label); - }).toList()); - }); - - return constrainedControl(context, navBar, widget.parent, widget.control); + return withPagePlatform((context, platform) { + bool adaptive = widget.control.attrBool("adaptive", false)!; + if (adaptive && + (platform == TargetPlatform.iOS || + platform == TargetPlatform.macOS)) { + return CupertinoNavigationBarControl( + control: widget.control, + children: widget.children, + parentDisabled: widget.parentDisabled); + } + + bool disabled = widget.control.isDisabled || widget.parentDisabled; + var selectedIndex = widget.control.attrInt("selectedIndex", 0)!; + + if (_selectedIndex != selectedIndex) { + _selectedIndex = selectedIndex; + } + + NavigationDestinationLabelBehavior? labelBehavior = + NavigationDestinationLabelBehavior.values.firstWhereOrNull((a) => + a.name.toLowerCase() == + widget.control.attrString("labelBehavior", "")!.toLowerCase()); + + var navBar = withControls( + widget.children + .where((c) => c.isVisible && c.name == null) + .map((c) => c.id), (content, viewModel) { + return NavigationBar( + labelBehavior: labelBehavior, + height: widget.control.attrDouble("height"), + elevation: widget.control.attrDouble("elevation"), + shadowColor: HexColor.fromString(Theme.of(context), + widget.control.attrString("shadowColor", "")!), + surfaceTintColor: HexColor.fromString(Theme.of(context), + widget.control.attrString("surfaceTintColor", "")!), + indicatorColor: HexColor.fromString(Theme.of(context), + widget.control.attrString("indicatorColor", "")!), + indicatorShape: + parseOutlinedBorder(widget.control, "indicatorShape"), + backgroundColor: HexColor.fromString( + Theme.of(context), widget.control.attrString("bgColor", "")!), + selectedIndex: _selectedIndex, + onDestinationSelected: _destinationChanged, + destinations: viewModel.controlViews.map((destView) { + var label = destView.control.attrString("label", "")!; + + var icon = parseIcon(destView.control.attrString("icon", "")!); + var iconContentCtrls = + destView.children.where((c) => c.name == "icon_content"); + + var selectedIcon = + parseIcon(destView.control.attrString("selectedIcon", "")!); + var selectedIconContentCtrls = destView.children + .where((c) => c.name == "selected_icon_content"); + + return NavigationDestination( + tooltip: destView.control.attrString("tooltip", "")!, + icon: iconContentCtrls.isNotEmpty + ? createControl( + destView.control, iconContentCtrls.first.id, disabled) + : Icon(icon), + selectedIcon: selectedIconContentCtrls.isNotEmpty + ? createControl(destView.control, + selectedIconContentCtrls.first.id, disabled) + : selectedIcon != null + ? Icon(selectedIcon) + : null, + label: label); + }).toList()); + }); + + return constrainedControl(context, navBar, widget.parent, widget.control); + }); } } diff --git a/package/lib/src/controls/navigation_drawer.dart b/package/lib/src/controls/navigation_drawer.dart index 5960894ff..569cce3ce 100644 --- a/package/lib/src/controls/navigation_drawer.dart +++ b/package/lib/src/controls/navigation_drawer.dart @@ -1,55 +1,42 @@ import 'package:flutter/material.dart'; -import 'package:flutter_redux/flutter_redux.dart'; -import '../actions.dart'; -import '../flet_app_services.dart'; -import '../models/app_state.dart'; import '../models/control.dart'; -import '../models/controls_view_model.dart'; -import '../protocol/update_control_props_payload.dart'; import '../utils/borders.dart'; import '../utils/colors.dart'; import '../utils/edge_insets.dart'; import '../utils/icons.dart'; import 'create_control.dart'; +import 'flet_control_stateful_mixin.dart'; +import 'flet_store_mixin.dart'; class NavigationDrawerControl extends StatefulWidget { final Control? parent; final Control control; final List<Control> children; final bool parentDisabled; - final dynamic dispatch; const NavigationDrawerControl( {super.key, this.parent, required this.control, required this.children, - required this.parentDisabled, - required this.dispatch}); + required this.parentDisabled}); @override State<NavigationDrawerControl> createState() => _NavigationDrawerControlState(); } -class _NavigationDrawerControlState extends State<NavigationDrawerControl> { +class _NavigationDrawerControlState extends State<NavigationDrawerControl> + with FletControlStatefulMixin, FletStoreMixin { int _selectedIndex = 0; void _destinationChanged(int index) { _selectedIndex = index; debugPrint("Selected index: $_selectedIndex"); - List<Map<String, String>> props = [ - {"i": widget.control.id, "selectedindex": _selectedIndex.toString()} - ]; - widget.dispatch( - UpdateControlPropsAction(UpdateControlPropsPayload(props: props))); - final server = FletAppServices.of(context).server; - server.updateControlProps(props: props); - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "change", - eventData: _selectedIndex.toString()); + updateControlProps( + widget.control.id, {"selectedindex": _selectedIndex.toString()}); + sendControlEvent(widget.control.id, "change", _selectedIndex.toString()); } @override @@ -63,64 +50,57 @@ class _NavigationDrawerControlState extends State<NavigationDrawerControl> { _selectedIndex = selectedIndex; } - var navDrawer = StoreConnector<AppState, ControlsViewModel>( - distinct: true, - converter: (store) => ControlsViewModel.fromStore( - store, - widget.children - .where((c) => c.isVisible && c.name == null) - .map((c) => c.id)), - builder: (content, viewModel) { - List<Widget> children = viewModel.controlViews.map((destView) { - if (destView.control.type == "navigationdrawerdestination") { - var icon = - parseIcon(destView.control.attrString("icon", "")!); - var iconContentCtrls = - destView.children.where((c) => c.name == "icon_content"); - var selectedIcon = parseIcon( - destView.control.attrString("selectedIcon", "")!); - var selectedIconContentCtrls = destView.children - .where((c) => c.name == "selected_icon_content"); - return NavigationDrawerDestination( - // backgroundColor: HexColor.fromString(Theme.of(context), - // destView.control.attrString("bgColor", "")!), - // flutter issue https://github.com/flutter/flutter/issues/138105 - icon: iconContentCtrls.isNotEmpty - ? createControl( - destView.control, iconContentCtrls.first.id, disabled) - : Icon(icon), - label: Text(destView.control.attrString("label", "")!), - selectedIcon: selectedIconContentCtrls.isNotEmpty - ? createControl(destView.control, - selectedIconContentCtrls.first.id, disabled) - : selectedIcon != null - ? Icon(selectedIcon) - : null, - ); - } else { - return createControl( - widget.control, destView.control.id, disabled); - } - }).toList(); - return NavigationDrawer( - elevation: widget.control.attrDouble("elevation"), - indicatorColor: HexColor.fromString(Theme.of(context), - widget.control.attrString("indicatorColor", "")!), - indicatorShape: - parseOutlinedBorder(widget.control, "indicatorShape"), - backgroundColor: HexColor.fromString( - Theme.of(context), widget.control.attrString("bgColor", "")!), - selectedIndex: _selectedIndex, - shadowColor: HexColor.fromString(Theme.of(context), - widget.control.attrString("shadowColor", "")!), - surfaceTintColor: HexColor.fromString(Theme.of(context), - widget.control.attrString("surfaceTintColor", "")!), - tilePadding: parseEdgeInsets(widget.control, "tilePadding") ?? - const EdgeInsets.symmetric(horizontal: 12.0), - onDestinationSelected: _destinationChanged, - children: children, + var navDrawer = withControls( + widget.children + .where((c) => c.isVisible && c.name == null) + .map((c) => c.id), (content, viewModel) { + List<Widget> children = viewModel.controlViews.map((destView) { + if (destView.control.type == "navigationdrawerdestination") { + var icon = parseIcon(destView.control.attrString("icon", "")!); + var iconContentCtrls = + destView.children.where((c) => c.name == "icon_content"); + var selectedIcon = + parseIcon(destView.control.attrString("selectedIcon", "")!); + var selectedIconContentCtrls = + destView.children.where((c) => c.name == "selected_icon_content"); + return NavigationDrawerDestination( + // backgroundColor: HexColor.fromString(Theme.of(context), + // destView.control.attrString("bgColor", "")!), + // flutter issue https://github.com/flutter/flutter/issues/138105 + icon: iconContentCtrls.isNotEmpty + ? createControl( + destView.control, iconContentCtrls.first.id, disabled) + : Icon(icon), + label: Text(destView.control.attrString("label", "")!), + selectedIcon: selectedIconContentCtrls.isNotEmpty + ? createControl(destView.control, + selectedIconContentCtrls.first.id, disabled) + : selectedIcon != null + ? Icon(selectedIcon) + : null, ); - }); + } else { + return createControl(widget.control, destView.control.id, disabled); + } + }).toList(); + return NavigationDrawer( + elevation: widget.control.attrDouble("elevation"), + indicatorColor: HexColor.fromString(Theme.of(context), + widget.control.attrString("indicatorColor", "")!), + indicatorShape: parseOutlinedBorder(widget.control, "indicatorShape"), + backgroundColor: HexColor.fromString( + Theme.of(context), widget.control.attrString("bgColor", "")!), + selectedIndex: _selectedIndex, + shadowColor: HexColor.fromString( + Theme.of(context), widget.control.attrString("shadowColor", "")!), + surfaceTintColor: HexColor.fromString(Theme.of(context), + widget.control.attrString("surfaceTintColor", "")!), + tilePadding: parseEdgeInsets(widget.control, "tilePadding") ?? + const EdgeInsets.symmetric(horizontal: 12.0), + onDestinationSelected: _destinationChanged, + children: children, + ); + }); return navDrawer; } diff --git a/package/lib/src/controls/navigation_rail.dart b/package/lib/src/controls/navigation_rail.dart index b3d11d32f..5aec8e984 100644 --- a/package/lib/src/controls/navigation_rail.dart +++ b/package/lib/src/controls/navigation_rail.dart @@ -1,55 +1,42 @@ import 'package:flutter/material.dart'; -import 'package:flutter_redux/flutter_redux.dart'; -import '../actions.dart'; -import '../flet_app_services.dart'; -import '../models/app_state.dart'; import '../models/control.dart'; -import '../models/controls_view_model.dart'; -import '../protocol/update_control_props_payload.dart'; import '../utils/borders.dart'; import '../utils/colors.dart'; import '../utils/edge_insets.dart'; import '../utils/icons.dart'; import 'create_control.dart'; import 'error.dart'; +import 'flet_control_stateful_mixin.dart'; +import 'flet_store_mixin.dart'; class NavigationRailControl extends StatefulWidget { final Control? parent; final Control control; final List<Control> children; final bool parentDisabled; - final dynamic dispatch; const NavigationRailControl( {super.key, this.parent, required this.control, required this.children, - required this.parentDisabled, - required this.dispatch}); + required this.parentDisabled}); @override State<NavigationRailControl> createState() => _NavigationRailControlState(); } -class _NavigationRailControlState extends State<NavigationRailControl> { +class _NavigationRailControlState extends State<NavigationRailControl> + with FletControlStatefulMixin, FletStoreMixin { int? _selectedIndex; void _destinationChanged(int index) { _selectedIndex = index; debugPrint("Selected index: $_selectedIndex"); - List<Map<String, String>> props = [ - {"i": widget.control.id, "selectedindex": _selectedIndex.toString()} - ]; - widget.dispatch( - UpdateControlPropsAction(UpdateControlPropsPayload(props: props))); - final server = FletAppServices.of(context).server; - server.updateControlProps(props: props); - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "change", - eventData: _selectedIndex.toString()); + updateControlProps( + widget.control.id, {"selectedindex": _selectedIndex.toString()}); + sendControlEvent(widget.control.id, "change", _selectedIndex.toString()); } @override @@ -77,87 +64,81 @@ class _NavigationRailControlState extends State<NavigationRailControl> { var extended = widget.control.attrBool("extended", false)!; - var rail = StoreConnector<AppState, ControlsViewModel>( - distinct: true, - converter: (store) => ControlsViewModel.fromStore( - store, - widget.children - .where((c) => c.isVisible && c.name == null) - .map((c) => c.id)), - builder: (content, viewModel) { - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - debugPrint( - "NavigationRail constraints.maxWidth: ${constraints.maxWidth}"); - debugPrint( - "NavigationRail constraints.maxHeight: ${constraints.maxHeight}"); - - if (constraints.maxHeight == double.infinity && - widget.control.attrs["height"] == null) { - return const ErrorControl("Error displaying NavigationRail", - description: - "Control's height is unbounded. Either set \"expand\" property, set a fixed \"height\" or nest NavigationRail inside another control with a fixed height."); - } - - return NavigationRail( - labelType: - extended ? NavigationRailLabelType.none : labelType, - extended: extended, - elevation: widget.control.attrDouble("elevation", 0), - indicatorShape: parseOutlinedBorder(widget.control, "indicatorShape"), - minWidth: widget.control.attrDouble("minWidth"), - minExtendedWidth: - widget.control.attrDouble("minExtendedWidth"), - groupAlignment: widget.control.attrDouble("groupAlignment"), - backgroundColor: HexColor.fromString(Theme.of(context), - widget.control.attrString("bgColor", "")!), - indicatorColor: HexColor.fromString(Theme.of(context), - widget.control.attrString("indicatorColor", "")!), - leading: leadingCtrls.isNotEmpty - ? createControl( - widget.control, leadingCtrls.first.id, disabled) - : null, - trailing: trailingCtrls.isNotEmpty - ? createControl( - widget.control, trailingCtrls.first.id, disabled) - : null, - selectedIndex: _selectedIndex, - onDestinationSelected: _destinationChanged, - destinations: viewModel.controlViews.map((destView) { - var label = destView.control.attrString("label", "")!; - var labelContentCtrls = destView.children - .where((c) => c.name == "label_content"); - - var icon = parseIcon( - destView.control.attrString("icon", "")!); - var iconContentCtrls = destView.children - .where((c) => c.name == "icon_content"); - - var selectedIcon = parseIcon( - destView.control.attrString("selectedIcon", "")!); - var selectedIconContentCtrls = destView.children - .where((c) => c.name == "selected_icon_content"); - - return NavigationRailDestination( - padding: parseEdgeInsets(destView.control, "padding"), - icon: iconContentCtrls.isNotEmpty - ? createControl(destView.control, - iconContentCtrls.first.id, disabled) - : Icon(icon), - selectedIcon: selectedIconContentCtrls.isNotEmpty - ? createControl(destView.control, - selectedIconContentCtrls.first.id, disabled) - : selectedIcon != null - ? Icon(selectedIcon) - : null, - label: labelContentCtrls.isNotEmpty - ? createControl(destView.control, - labelContentCtrls.first.id, disabled) - : Text(label)); - }).toList()); - }, - ); - }); + var rail = withControls( + widget.children + .where((c) => c.isVisible && c.name == null) + .map((c) => c.id), (content, viewModel) { + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + debugPrint( + "NavigationRail constraints.maxWidth: ${constraints.maxWidth}"); + debugPrint( + "NavigationRail constraints.maxHeight: ${constraints.maxHeight}"); + + if (constraints.maxHeight == double.infinity && + widget.control.attrs["height"] == null) { + return const ErrorControl("Error displaying NavigationRail", + description: + "Control's height is unbounded. Either set \"expand\" property, set a fixed \"height\" or nest NavigationRail inside another control with a fixed height."); + } + + return NavigationRail( + labelType: extended ? NavigationRailLabelType.none : labelType, + extended: extended, + elevation: widget.control.attrDouble("elevation", 0), + indicatorShape: + parseOutlinedBorder(widget.control, "indicatorShape"), + minWidth: widget.control.attrDouble("minWidth"), + minExtendedWidth: widget.control.attrDouble("minExtendedWidth"), + groupAlignment: widget.control.attrDouble("groupAlignment"), + backgroundColor: HexColor.fromString( + Theme.of(context), widget.control.attrString("bgColor", "")!), + indicatorColor: HexColor.fromString(Theme.of(context), + widget.control.attrString("indicatorColor", "")!), + leading: leadingCtrls.isNotEmpty + ? createControl( + widget.control, leadingCtrls.first.id, disabled) + : null, + trailing: trailingCtrls.isNotEmpty + ? createControl( + widget.control, trailingCtrls.first.id, disabled) + : null, + selectedIndex: _selectedIndex, + onDestinationSelected: _destinationChanged, + destinations: viewModel.controlViews.map((destView) { + var label = destView.control.attrString("label", "")!; + var labelContentCtrls = + destView.children.where((c) => c.name == "label_content"); + + var icon = parseIcon(destView.control.attrString("icon", "")!); + var iconContentCtrls = + destView.children.where((c) => c.name == "icon_content"); + + var selectedIcon = + parseIcon(destView.control.attrString("selectedIcon", "")!); + var selectedIconContentCtrls = destView.children + .where((c) => c.name == "selected_icon_content"); + + return NavigationRailDestination( + padding: parseEdgeInsets(destView.control, "padding"), + icon: iconContentCtrls.isNotEmpty + ? createControl(destView.control, + iconContentCtrls.first.id, disabled) + : Icon(icon), + selectedIcon: selectedIconContentCtrls.isNotEmpty + ? createControl(destView.control, + selectedIconContentCtrls.first.id, disabled) + : selectedIcon != null + ? Icon(selectedIcon) + : null, + label: labelContentCtrls.isNotEmpty + ? createControl(destView.control, + labelContentCtrls.first.id, disabled) + : Text(label)); + }).toList()); + }, + ); + }); return constrainedControl(context, rail, widget.parent, widget.control); } diff --git a/package/lib/src/controls/outlined_button.dart b/package/lib/src/controls/outlined_button.dart index 65dfd30af..f590c6dec 100644 --- a/package/lib/src/controls/outlined_button.dart +++ b/package/lib/src/controls/outlined_button.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; -import '../flet_app_services.dart'; import '../models/control.dart'; import '../utils/buttons.dart'; import '../utils/colors.dart'; import '../utils/icons.dart'; import '../utils/launch_url.dart'; import 'create_control.dart'; +import 'flet_control_stateful_mixin.dart'; class OutlinedButtonControl extends StatefulWidget { final Control? parent; @@ -25,7 +25,8 @@ class OutlinedButtonControl extends StatefulWidget { State<OutlinedButtonControl> createState() => _OutlinedButtonControlState(); } -class _OutlinedButtonControlState extends State<OutlinedButtonControl> { +class _OutlinedButtonControlState extends State<OutlinedButtonControl> + with FletControlStatefulMixin { late final FocusNode _focusNode; String? _lastFocusValue; @@ -44,18 +45,14 @@ class _OutlinedButtonControlState extends State<OutlinedButtonControl> { } void _onFocusChange() { - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, - eventName: _focusNode.hasFocus ? "focus" : "blur", - eventData: ""); + sendControlEvent( + widget.control.id, _focusNode.hasFocus ? "focus" : "blur", ""); } @override Widget build(BuildContext context) { debugPrint("Button build: ${widget.control.id}"); - final server = FletAppServices.of(context).server; - String text = widget.control.attrString("text", "")!; IconData? icon = parseIcon(widget.control.attrString("icon", "")!); Color? iconColor = HexColor.fromString( @@ -74,30 +71,21 @@ class _OutlinedButtonControlState extends State<OutlinedButtonControl> { if (url != "") { openWebBrowser(url, webWindowName: urlTarget); } - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "click", - eventData: ""); + sendControlEvent(widget.control.id, "click", ""); } : null; Function()? onLongPressHandler = onLongPress && !disabled ? () { debugPrint("Button ${widget.control.id} long pressed!"); - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "long_press", - eventData: ""); + sendControlEvent(widget.control.id, "long_press", ""); } : null; Function(bool)? onHoverHandler = onHover && !disabled ? (state) { debugPrint("Button ${widget.control.id} hovered!"); - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "hover", - eventData: state.toString()); + sendControlEvent(widget.control.id, "hover", state.toString()); } : null; diff --git a/package/lib/src/controls/page.dart b/package/lib/src/controls/page.dart index dfac6cc65..1220c7b8e 100644 --- a/package/lib/src/controls/page.dart +++ b/package/lib/src/controls/page.dart @@ -1,25 +1,21 @@ import 'dart:convert'; import 'package:collection/collection.dart'; -import 'package:flet/src/controls/floating_action_button.dart'; -import 'package:flet/src/flet_app_context.dart'; +import 'package:equatable/equatable.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_redux/flutter_redux.dart'; +import 'package:redux/redux.dart'; import '../actions.dart'; +import '../flet_app_context.dart'; import '../flet_app_services.dart'; import '../models/app_state.dart'; import '../models/control.dart'; import '../models/control_view_model.dart'; -import '../models/controls_view_model.dart'; -import '../models/page_args_model.dart'; import '../models/page_media_view_model.dart'; -import '../models/routes_view_model.dart'; -import '../protocol/keyboard_event.dart'; -import '../protocol/update_control_props_payload.dart'; import '../routing/route_parser.dart'; import '../routing/route_state.dart'; import '../routing/router_delegate.dart'; @@ -37,10 +33,75 @@ import '../widgets/window_media.dart'; import 'app_bar.dart'; import 'create_control.dart'; import 'cupertino_app_bar.dart'; +import 'flet_control_stateful_mixin.dart'; +import 'flet_store_mixin.dart'; +import 'floating_action_button.dart'; import 'navigation_drawer.dart'; import 'scroll_notification_control.dart'; import 'scrollable_control.dart'; +class RoutesViewModel extends Equatable { + final Control page; + final bool isLoading; + final String error; + final List<Control> offstageControls; + final List<Control> views; + + const RoutesViewModel( + {required this.page, + required this.isLoading, + required this.error, + required this.offstageControls, + required this.views}); + + static RoutesViewModel fromStore(Store<AppState> store) { + Control? offstageControl = store.state.controls["page"]!.childIds + .map((childId) => store.state.controls[childId]!) + .firstWhereOrNull((c) => c.type == "offstage"); + + return RoutesViewModel( + page: store.state.controls["page"]!, + isLoading: store.state.isLoading, + error: store.state.error, + offstageControls: offstageControl != null + ? store.state.controls[offstageControl.id]!.childIds + .map((childId) => store.state.controls[childId]!) + .where((c) => c.isVisible) + .toList() + : [], + views: store.state.controls["page"]!.childIds + .map((childId) => store.state.controls[childId]!) + .where((c) => c.type != "offstage" && c.isVisible) + .toList()); + } + + @override + List<Object?> get props => [page, isLoading, error, offstageControls, views]; +} + +class KeyboardEvent { + final String key; + final bool isShiftPressed; + final bool isControlPressed; + final bool isAltPressed; + final bool isMetaPressed; + + KeyboardEvent( + {required this.key, + required this.isShiftPressed, + required this.isControlPressed, + required this.isAltPressed, + required this.isMetaPressed}); + + Map<String, dynamic> toJson() => <String, dynamic>{ + 'key': key, + 'shift': isShiftPressed, + 'ctrl': isControlPressed, + 'alt': isAltPressed, + 'meta': isMetaPressed + }; +} + class PageControl extends StatefulWidget { final Control? parent; final Control control; @@ -58,7 +119,8 @@ class PageControl extends StatefulWidget { State<PageControl> createState() => _PageControlState(); } -class _PageControlState extends State<PageControl> { +class _PageControlState extends State<PageControl> + with FletControlStatefulMixin, FletStoreMixin { String? _windowTitle; Color? _windowBgcolor; double? _windowWidth; @@ -141,10 +203,10 @@ class _PageControlState extends State<PageControl> { LogicalKeyboardKey.shiftLeft, LogicalKeyboardKey.shiftRight ].contains(k)) { - FletAppServices.of(context).server.sendPageEvent( - eventTarget: "page", - eventName: "keyboard_event", - eventData: json.encode(KeyboardEvent( + sendControlEvent( + "page", + "keyboard_event", + json.encode(KeyboardEvent( key: k.keyLabel, isAltPressed: e.isAltPressed, isControlPressed: e.isControlPressed, @@ -424,45 +486,42 @@ class _PageControlState extends State<PageControl> { updateWindow(); - return StoreConnector<AppState, PageArgsModel>( - distinct: true, - converter: (store) => PageArgsModel.fromStore(store), - builder: (context, pageArgs) { - debugPrint("Page fonts build: ${widget.control.id}"); - - // load custom fonts - parseFonts(widget.control, "fonts").forEach((fontFamily, fontUrl) { - var assetSrc = - getAssetSrc(fontUrl, pageArgs.pageUri!, pageArgs.assetsDir); - - if (assetSrc.isFile) { - UserFonts.loadFontFromFile(fontFamily, assetSrc.path); - } else { - UserFonts.loadFontFromUrl(fontFamily, assetSrc.path); - } - }); + return withPageArgs((context, pageArgs) { + debugPrint("Page fonts build: ${widget.control.id}"); - return StoreConnector<AppState, PageMediaViewModel>( - distinct: true, - converter: (store) => PageMediaViewModel.fromStore(store), - builder: (context, media) { - debugPrint("MaterialApp.router build: ${widget.control.id}"); - - return FletAppContext( - themeMode: themeMode, - child: MaterialApp.router( - debugShowCheckedModeBanner: false, - showSemanticsDebugger: widget.control - .attrBool("showSemanticsDebugger", false)!, - routerDelegate: _routerDelegate, - routeInformationParser: _routeParser, - title: windowTitle, - theme: theme, - darkTheme: darkTheme, - themeMode: themeMode, - )); - }); - }); + // load custom fonts + parseFonts(widget.control, "fonts").forEach((fontFamily, fontUrl) { + var assetSrc = + getAssetSrc(fontUrl, pageArgs.pageUri!, pageArgs.assetsDir); + + if (assetSrc.isFile) { + UserFonts.loadFontFromFile(fontFamily, assetSrc.path); + } else { + UserFonts.loadFontFromUrl(fontFamily, assetSrc.path); + } + }); + + return StoreConnector<AppState, PageMediaViewModel>( + distinct: true, + converter: (store) => PageMediaViewModel.fromStore(store), + builder: (context, media) { + debugPrint("MaterialApp.router build: ${widget.control.id}"); + + return FletAppContext( + themeMode: themeMode, + child: MaterialApp.router( + debugShowCheckedModeBanner: false, + showSemanticsDebugger: + widget.control.attrBool("showSemanticsDebugger", false)!, + routerDelegate: _routerDelegate, + routeInformationParser: _routeParser, + title: windowTitle, + theme: theme, + darkTheme: darkTheme, + themeMode: themeMode, + )); + }); + }); } Widget _buildNavigator( @@ -512,7 +571,7 @@ class _PageControlState extends State<PageControl> { } if (viewId == routesView.views.first.id && isDesktop()) { - overlayWidgets.add(const WindowMedia()); + overlayWidgets.add(WindowMedia(dispatch: widget.dispatch)); } return overlayWidgets; @@ -536,8 +595,7 @@ class _PageControlState extends State<PageControl> { parent: routesView.page, viewId: view.id, overlayWidgets: overlayWidgets(view.id), - loadingPage: loadingPage, - dispatch: widget.dispatch); + loadingPage: loadingPage); //debugPrint("ROUTES: $_prevViewRoutes $viewRoutes"); @@ -562,11 +620,8 @@ class _PageControlState extends State<PageControl> { key: navigatorKey, pages: pages, onPopPage: (route, dynamic result) { - FletAppServices.of(context).server.sendPageEvent( - eventTarget: "page", - eventName: "view_pop", - eventData: - ((route.settings as Page).key as ValueKey).value); + sendControlEvent("page", "view_pop", + ((route.settings as Page).key as ValueKey).value); return false; }); @@ -588,21 +643,20 @@ class ViewControl extends StatefulWidget { final String viewId; final List<Widget> overlayWidgets; final Widget? loadingPage; - final dynamic dispatch; const ViewControl( {super.key, required this.parent, required this.viewId, required this.overlayWidgets, - required this.loadingPage, - required this.dispatch}); + required this.loadingPage}); @override State<ViewControl> createState() => _ViewControlState(); } -class _ViewControlState extends State<ViewControl> { +class _ViewControlState extends State<ViewControl> + with FletControlStatefulMixin, FletStoreMixin { final scaffoldKey = GlobalKey<ScaffoldState>(); @override @@ -619,7 +673,7 @@ class _ViewControlState extends State<ViewControl> { // debugPrint("View StoreConnector.onWillChange(): $prev, $next"); // }, builder: (context, controlView) { - debugPrint("View StoreConnector"); + debugPrint("View build"); if (controlView == null) { return const SizedBox.shrink(); @@ -698,195 +752,167 @@ class _ViewControlState extends State<ViewControl> { ? TextDirection.rtl : TextDirection.ltr; - return StoreConnector<AppState, ControlsViewModel>( - distinct: true, - converter: (store) => - ControlsViewModel.fromStore(store, childIds), - ignoreChange: (state) { - //debugPrint("ignoreChange: $id"); - for (var id in childIds) { - if (state.controls[id] == null) { - return true; - } - } - return false; - }, - builder: (context, childrenViews) { - debugPrint("Route view StoreConnector build: ${widget.viewId}"); - - var appBarView = childrenViews.controlViews.firstWhereOrNull( - (v) => v.control.id == (appBar?.id ?? "")); - var cupertinoAppBarView = childrenViews.controlViews - .firstWhereOrNull( - (v) => v.control.id == (cupertinoAppBar?.id ?? "")); - var drawerView = childrenViews.controlViews.firstWhereOrNull( - (v) => v.control.id == (drawer?.id ?? "")); - var endDrawerView = childrenViews.controlViews.firstWhereOrNull( - (v) => v.control.id == (endDrawer?.id ?? "")); - - var column = Column( - mainAxisAlignment: mainAlignment, - crossAxisAlignment: crossAlignment, - children: controls); - - Widget child = ScrollableControl( - control: control, - scrollDirection: Axis.vertical, - dispatch: widget.dispatch, - child: column, - ); - - if (control.attrBool("onScroll", false)!) { - child = - ScrollNotificationControl(control: control, child: child); - } + return withControls(childIds, (context, childrenViews) { + debugPrint("Route view build: ${widget.viewId}"); + + var appBarView = childrenViews.controlViews + .firstWhereOrNull((v) => v.control.id == (appBar?.id ?? "")); + var cupertinoAppBarView = childrenViews.controlViews + .firstWhereOrNull( + (v) => v.control.id == (cupertinoAppBar?.id ?? "")); + var drawerView = childrenViews.controlViews + .firstWhereOrNull((v) => v.control.id == (drawer?.id ?? "")); + var endDrawerView = childrenViews.controlViews + .firstWhereOrNull((v) => v.control.id == (endDrawer?.id ?? "")); + + var column = Column( + mainAxisAlignment: mainAlignment, + crossAxisAlignment: crossAlignment, + children: controls); + + Widget child = ScrollableControl( + control: control, + scrollDirection: Axis.vertical, + child: column, + ); + + if (control.attrBool("onScroll", false)!) { + child = ScrollNotificationControl(control: control, child: child); + } - final bool? drawerOpened = widget.parent.state["drawerOpened"]; - final bool? endDrawerOpened = - widget.parent.state["endDrawerOpened"]; - - void dismissDrawer(String id) { - List<Map<String, String>> props = [ - {"i": id, "open": "false"} - ]; - widget.dispatch(UpdateControlPropsAction( - UpdateControlPropsPayload(props: props))); - FletAppServices.of(context) - .server - .updateControlProps(props: props); - FletAppServices.of(context).server.sendPageEvent( - eventTarget: id, eventName: "dismiss", eventData: ""); - } + final bool? drawerOpened = widget.parent.state["drawerOpened"]; + final bool? endDrawerOpened = + widget.parent.state["endDrawerOpened"]; + + void dismissDrawer(String id) { + updateControlProps(id, {"open": "false"}); + sendControlEvent(id, "dismiss", ""); + } - WidgetsBinding.instance.addPostFrameCallback((_) { - if (drawerView != null) { - if (scaffoldKey.currentState?.isDrawerOpen == false && - drawerOpened == true) { - widget.parent.state["drawerOpened"] = false; - dismissDrawer(drawerView.control.id); - } - if (drawerView.control.attrBool("open", false)! && - drawerOpened != true) { - if (scaffoldKey.currentState?.isEndDrawerOpen == true) { - scaffoldKey.currentState?.closeEndDrawer(); - } - Future.delayed(const Duration(milliseconds: 1)) - .then((value) { - scaffoldKey.currentState?.openDrawer(); - widget.parent.state["drawerOpened"] = true; - }); - } else if (!drawerView.control.attrBool("open", false)! && - drawerOpened == true) { - scaffoldKey.currentState?.closeDrawer(); - widget.parent.state["drawerOpened"] = false; - } + WidgetsBinding.instance.addPostFrameCallback((_) { + if (drawerView != null) { + if (scaffoldKey.currentState?.isDrawerOpen == false && + drawerOpened == true) { + widget.parent.state["drawerOpened"] = false; + dismissDrawer(drawerView.control.id); + } + if (drawerView.control.attrBool("open", false)! && + drawerOpened != true) { + if (scaffoldKey.currentState?.isEndDrawerOpen == true) { + scaffoldKey.currentState?.closeEndDrawer(); } - if (endDrawerView != null) { - if (scaffoldKey.currentState?.isEndDrawerOpen == false && - endDrawerOpened == true) { - widget.parent.state["endDrawerOpened"] = false; - dismissDrawer(endDrawerView.control.id); - } - if (endDrawerView.control.attrBool("open", false)! && - endDrawerOpened != true) { - if (scaffoldKey.currentState?.isDrawerOpen == true) { - scaffoldKey.currentState?.closeDrawer(); - } - Future.delayed(const Duration(milliseconds: 1)) - .then((value) { - scaffoldKey.currentState?.openEndDrawer(); - widget.parent.state["endDrawerOpened"] = true; - }); - } else if (!endDrawerView.control - .attrBool("open", false)! && - endDrawerOpened == true) { - scaffoldKey.currentState?.closeEndDrawer(); - widget.parent.state["endDrawerOpened"] = false; - } + Future.delayed(const Duration(milliseconds: 1)).then((value) { + scaffoldKey.currentState?.openDrawer(); + widget.parent.state["drawerOpened"] = true; + }); + } else if (!drawerView.control.attrBool("open", false)! && + drawerOpened == true) { + scaffoldKey.currentState?.closeDrawer(); + widget.parent.state["drawerOpened"] = false; + } + } + if (endDrawerView != null) { + if (scaffoldKey.currentState?.isEndDrawerOpen == false && + endDrawerOpened == true) { + widget.parent.state["endDrawerOpened"] = false; + dismissDrawer(endDrawerView.control.id); + } + if (endDrawerView.control.attrBool("open", false)! && + endDrawerOpened != true) { + if (scaffoldKey.currentState?.isDrawerOpen == true) { + scaffoldKey.currentState?.closeDrawer(); } - }); - - var bnb = navBar ?? bottomAppBar; - - var bar = appBarView != null - ? AppBarControl( + Future.delayed(const Duration(milliseconds: 1)).then((value) { + scaffoldKey.currentState?.openEndDrawer(); + widget.parent.state["endDrawerOpened"] = true; + }); + } else if (!endDrawerView.control.attrBool("open", false)! && + endDrawerOpened == true) { + scaffoldKey.currentState?.closeEndDrawer(); + widget.parent.state["endDrawerOpened"] = false; + } + } + }); + + var bnb = navBar ?? bottomAppBar; + + var bar = appBarView != null + ? AppBarControl( + parent: control, + control: appBarView.control, + children: appBarView.children, + parentDisabled: control.isDisabled, + height: appBarView.control + .attrDouble("toolbarHeight", kToolbarHeight)!) + : cupertinoAppBarView != null + ? CupertinoAppBarControl( parent: control, - control: appBarView.control, - children: appBarView.children, + control: cupertinoAppBarView.control, + children: cupertinoAppBarView.children, parentDisabled: control.isDisabled, - height: appBarView.control - .attrDouble("toolbarHeight", kToolbarHeight)!) - : cupertinoAppBarView != null - ? CupertinoAppBarControl( - parent: control, - control: cupertinoAppBarView.control, - children: cupertinoAppBarView.children, - parentDisabled: control.isDisabled, - bgcolor: HexColor.fromString( - Theme.of(context), - cupertinoAppBarView.control - .attrString("bgcolor", "")!), - ) as ObstructingPreferredSizeWidget - : null; - - var scaffold = Scaffold( - key: scaffoldKey, - backgroundColor: HexColor.fromString( - Theme.of(context), control.attrString("bgcolor", "")!), - appBar: bar, - drawer: drawerView != null - ? NavigationDrawerControl( - control: drawerView.control, - children: drawerView.children, - parentDisabled: control.isDisabled, - dispatch: widget.dispatch, - ) - : null, - onDrawerChanged: (opened) { - if (drawerView != null && !opened) { - widget.parent.state["drawerOpened"] = false; - dismissDrawer(drawerView.control.id); - } - }, - endDrawer: endDrawerView != null - ? NavigationDrawerControl( - control: endDrawerView.control, - children: endDrawerView.children, - parentDisabled: control.isDisabled, - dispatch: widget.dispatch, - ) - : null, - onEndDrawerChanged: (opened) { - if (endDrawerView != null && !opened) { - widget.parent.state["endDrawerOpened"] = false; - dismissDrawer(endDrawerView.control.id); - } - }, - body: Stack(children: [ - SizedBox.expand( - child: Container( - padding: parseEdgeInsets(control, "padding") ?? - const EdgeInsets.all(10), - child: child)), - ...widget.overlayWidgets - ]), - bottomNavigationBar: bnb != null - ? createControl(control, bnb.id, control.isDisabled) - : null, - floatingActionButton: fab != null - ? createControl(control, fab.id, control.isDisabled) - : null, - floatingActionButtonLocation: fabLocation, - ); - - return Directionality( - textDirection: textDirection, - child: widget.loadingPage != null - ? Stack( - children: [scaffold, widget.loadingPage!], - ) - : scaffold); - }); + bgcolor: HexColor.fromString( + Theme.of(context), + cupertinoAppBarView.control + .attrString("bgcolor", "")!), + ) as ObstructingPreferredSizeWidget + : null; + + var scaffold = Scaffold( + key: scaffoldKey, + backgroundColor: HexColor.fromString( + Theme.of(context), control.attrString("bgcolor", "")!), + appBar: bar, + drawer: drawerView != null + ? NavigationDrawerControl( + control: drawerView.control, + children: drawerView.children, + parentDisabled: control.isDisabled, + ) + : null, + onDrawerChanged: (opened) { + if (drawerView != null && !opened) { + widget.parent.state["drawerOpened"] = false; + dismissDrawer(drawerView.control.id); + } + }, + endDrawer: endDrawerView != null + ? NavigationDrawerControl( + control: endDrawerView.control, + children: endDrawerView.children, + parentDisabled: control.isDisabled, + ) + : null, + onEndDrawerChanged: (opened) { + if (endDrawerView != null && !opened) { + widget.parent.state["endDrawerOpened"] = false; + dismissDrawer(endDrawerView.control.id); + } + }, + body: Stack(children: [ + SizedBox.expand( + child: Container( + padding: parseEdgeInsets(control, "padding") ?? + const EdgeInsets.all(10), + child: child)), + ...widget.overlayWidgets + ]), + bottomNavigationBar: bnb != null + ? createControl(control, bnb.id, control.isDisabled) + : null, + floatingActionButton: fab != null + ? createControl(control, fab.id, control.isDisabled) + : null, + floatingActionButtonLocation: fabLocation, + ); + + return Directionality( + textDirection: textDirection, + child: widget.loadingPage != null + ? Stack( + children: [scaffold, widget.loadingPage!], + ) + : scaffold); + }); }); } } diff --git a/package/lib/src/controls/pagelet.dart b/package/lib/src/controls/pagelet.dart index 325eba5bd..8dffe4ba5 100644 --- a/package/lib/src/controls/pagelet.dart +++ b/package/lib/src/controls/pagelet.dart @@ -3,17 +3,15 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; -import '../actions.dart'; -import '../flet_app_services.dart'; import '../models/app_state.dart'; import '../models/control.dart'; import '../models/controls_view_model.dart'; -import '../protocol/update_control_props_payload.dart'; import '../utils/colors.dart'; import 'app_bar.dart'; import 'create_control.dart'; import 'cupertino_app_bar.dart'; import 'error.dart'; +import 'flet_control_stateful_mixin.dart'; import 'floating_action_button.dart'; import 'navigation_drawer.dart'; @@ -22,21 +20,20 @@ class PageletControl extends StatefulWidget { final Control control; final List<Control> children; final bool parentDisabled; - final dynamic dispatch; const PageletControl( {super.key, this.parent, required this.control, required this.children, - required this.parentDisabled, - required this.dispatch}); + required this.parentDisabled}); @override State<PageletControl> createState() => _PageletControlState(); } -class _PageletControlState extends State<PageletControl> { +class _PageletControlState extends State<PageletControl> + with FletControlStatefulMixin { final scaffoldKey = GlobalKey<ScaffoldState>(); @override @@ -115,14 +112,8 @@ class _PageletControlState extends State<PageletControl> { FloatingActionButtonLocation.endFloat); void dismissDrawer(String id) { - List<Map<String, String>> props = [ - {"i": id, "open": "false"} - ]; - widget.dispatch(UpdateControlPropsAction( - UpdateControlPropsPayload(props: props))); - FletAppServices.of(context).server.updateControlProps(props: props); - FletAppServices.of(context).server.sendPageEvent( - eventTarget: id, eventName: "dismiss", eventData: ""); + updateControlProps(id, {"open": "false"}); + sendControlEvent(id, "dismiss", ""); } WidgetsBinding.instance.addPostFrameCallback((_) { @@ -202,7 +193,6 @@ class _PageletControlState extends State<PageletControl> { control: drawerView.control, children: drawerView.children, parentDisabled: widget.control.isDisabled, - dispatch: widget.dispatch, ) : null, onDrawerChanged: (opened) { @@ -216,7 +206,6 @@ class _PageletControlState extends State<PageletControl> { control: endDrawerView.control, children: endDrawerView.children, parentDisabled: widget.control.isDisabled, - dispatch: widget.dispatch, ) : null, onEndDrawerChanged: (opened) { diff --git a/package/lib/src/controls/piechart.dart b/package/lib/src/controls/piechart.dart index 9e8933aec..21b221937 100644 --- a/package/lib/src/controls/piechart.dart +++ b/package/lib/src/controls/piechart.dart @@ -1,20 +1,78 @@ import 'dart:convert'; +import 'package:collection/collection.dart'; +import 'package:equatable/equatable.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; +import 'package:redux/redux.dart'; -import '../flet_app_services.dart'; import '../models/app_state.dart'; import '../models/control.dart'; -import '../models/piechart_event_data.dart'; -import '../models/piechart_section_view_model.dart'; -import '../models/piechart_view_model.dart'; import '../utils/animations.dart'; import '../utils/borders.dart'; import '../utils/colors.dart'; import '../utils/text.dart'; import 'create_control.dart'; +import 'flet_control_stateful_mixin.dart'; + +class PieChartEventData extends Equatable { + final String eventType; + final int? sectionIndex; + // final double? angle; + // final double? radius; + + const PieChartEventData( + {required this.eventType, required this.sectionIndex}); + + Map<String, dynamic> toJson() => + <String, dynamic>{'type': eventType, 'section_index': sectionIndex}; + + @override + List<Object?> get props => [eventType, sectionIndex]; +} + +class PieChartSectionViewModel extends Equatable { + final Control control; + final Control? badge; + + const PieChartSectionViewModel({required this.control, required this.badge}); + + static PieChartSectionViewModel fromStore( + Store<AppState> store, Control control) { + var children = store.state.controls[control.id]!.childIds + .map((childId) => store.state.controls[childId]) + .whereNotNull() + .where((c) => c.isVisible); + + return PieChartSectionViewModel( + control: control, + badge: children.firstWhereOrNull((c) => c.name == "badge")); + } + + @override + List<Object?> get props => [control, badge]; +} + +class PieChartViewModel extends Equatable { + final Control control; + final List<PieChartSectionViewModel> sections; + + const PieChartViewModel({required this.control, required this.sections}); + + static PieChartViewModel fromStore( + Store<AppState> store, Control control, List<Control> children) { + return PieChartViewModel( + control: control, + sections: children + .where((c) => c.type == "section" && c.isVisible) + .map((c) => PieChartSectionViewModel.fromStore(store, c)) + .toList()); + } + + @override + List<Object?> get props => [control, sections]; +} class PieChartControl extends StatefulWidget { final Control? parent; @@ -33,7 +91,8 @@ class PieChartControl extends StatefulWidget { State<PieChartControl> createState() => _PieChartControlState(); } -class _PieChartControlState extends State<PieChartControl> { +class _PieChartControlState extends State<PieChartControl> + with FletControlStatefulMixin { PieChartEventData? _eventData; @override @@ -79,10 +138,8 @@ class _PieChartControlState extends State<PieChartControl> { _eventData = eventData; debugPrint( "PieChart ${widget.control.id} ${eventData.eventType}"); - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "chart_event", - eventData: json.encode(eventData)); + sendControlEvent(widget.control.id, "chart_event", + json.encode(eventData)); } } : null, diff --git a/package/lib/src/controls/popup_menu_button.dart b/package/lib/src/controls/popup_menu_button.dart index 7b36fa47a..93a0d89fc 100644 --- a/package/lib/src/controls/popup_menu_button.dart +++ b/package/lib/src/controls/popup_menu_button.dart @@ -1,14 +1,13 @@ import 'package:flutter/material.dart'; -import 'package:flutter_redux/flutter_redux.dart'; -import '../flet_app_services.dart'; -import '../models/app_state.dart'; import '../models/control.dart'; -import '../models/controls_view_model.dart'; import '../utils/icons.dart'; import 'create_control.dart'; +import 'flet_control_stateless_mixin.dart'; +import 'flet_store_mixin.dart'; -class PopupMenuButtonControl extends StatelessWidget { +class PopupMenuButtonControl extends StatelessWidget + with FletControlStatelessMixin, FletStoreMixin { final Control? parent; final Control control; final bool parentDisabled; @@ -25,8 +24,6 @@ class PopupMenuButtonControl extends StatelessWidget { Widget build(BuildContext context) { debugPrint("PopupMenuButton build: ${control.id}"); - final server = FletAppServices.of(context).server; - var icon = parseIcon(control.attrString("icon", "")!); var tooltip = control.attrString("tooltip"); var contentCtrls = @@ -37,69 +34,60 @@ class PopupMenuButtonControl extends StatelessWidget { ? createControl(control, contentCtrls.first.id, disabled) : null; - var popupButton = StoreConnector<AppState, ControlsViewModel>( - distinct: true, - converter: (store) => ControlsViewModel.fromStore( - store, children.where((c) => c.name != "content").map((c) => c.id)), - builder: (content, viewModel) { - return PopupMenuButton<String>( - enabled: !disabled, - icon: icon != null ? Icon(icon) : null, - tooltip: tooltip, - shape: Theme.of(context).useMaterial3 - ? RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10)) - : null, - onCanceled: () { - server.sendPageEvent( - eventTarget: control.id, - eventName: "cancelled", - eventData: ""); - }, - onSelected: (itemId) { - server.sendPageEvent( - eventTarget: itemId, eventName: "click", eventData: ""); - }, - itemBuilder: (BuildContext context) => - viewModel.controlViews.map((cv) { - var itemIcon = - parseIcon(cv.control.attrString("icon", "")!); - var text = cv.control.attrString("text", "")!; - var checked = cv.control.attrBool("checked"); - var contentCtrls = - cv.children.where((c) => c.name == "content"); + var popupButton = withControls( + children.where((c) => c.name != "content").map((c) => c.id), + (content, viewModel) { + return PopupMenuButton<String>( + enabled: !disabled, + icon: icon != null ? Icon(icon) : null, + tooltip: tooltip, + shape: Theme.of(context).useMaterial3 + ? RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)) + : null, + onCanceled: () { + sendControlEvent(context, control.id, "cancelled", ""); + }, + onSelected: (itemId) { + sendControlEvent(context, itemId, "click", ""); + }, + itemBuilder: (BuildContext context) => + viewModel.controlViews.map((cv) { + var itemIcon = parseIcon(cv.control.attrString("icon", "")!); + var text = cv.control.attrString("text", "")!; + var checked = cv.control.attrBool("checked"); + var contentCtrls = + cv.children.where((c) => c.name == "content"); - Widget? child; - if (contentCtrls.isNotEmpty) { - // custom content - child = createControl( - cv.control, contentCtrls.first.id, parentDisabled); - } else if (itemIcon != null && text != "") { - // icon and text - child = Row(children: [ - Icon(itemIcon), - const SizedBox(width: 8), - Text(text) - ]); - } else if (text != "") { - child = Text(text); - } + Widget? child; + if (contentCtrls.isNotEmpty) { + // custom content + child = createControl( + cv.control, contentCtrls.first.id, parentDisabled); + } else if (itemIcon != null && text != "") { + // icon and text + child = Row(children: [ + Icon(itemIcon), + const SizedBox(width: 8), + Text(text) + ]); + } else if (text != "") { + child = Text(text); + } - var item = checked != null - ? CheckedPopupMenuItem<String>( - value: cv.control.id, - checked: checked, - child: child, - ) - : PopupMenuItem<String>( - value: cv.control.id, child: child); + var item = checked != null + ? CheckedPopupMenuItem<String>( + value: cv.control.id, + checked: checked, + child: child, + ) + : PopupMenuItem<String>(value: cv.control.id, child: child); - return child != null - ? item - : const PopupMenuDivider() as PopupMenuEntry<String>; - }).toList(), - child: child); - }); + return child != null + ? item + : const PopupMenuDivider() as PopupMenuEntry<String>; + }).toList(), + child: child); + }); return constrainedControl(context, popupButton, parent, control); } diff --git a/package/lib/src/controls/radio.dart b/package/lib/src/controls/radio.dart index 118dbe422..326d63790 100644 --- a/package/lib/src/controls/radio.dart +++ b/package/lib/src/controls/radio.dart @@ -1,18 +1,12 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_redux/flutter_redux.dart'; -import '../actions.dart'; -import '../flet_app_services.dart'; -import '../models/app_state.dart'; import '../models/control.dart'; -import '../models/control_ancestor_view_model.dart'; -import '../protocol/update_control_props_payload.dart'; -import '../utils/buttons.dart'; import '../utils/colors.dart'; import 'create_control.dart'; import 'cupertino_radio.dart'; import 'error.dart'; +import 'flet_control_stateful_mixin.dart'; +import 'flet_store_mixin.dart'; import 'list_tile.dart'; enum LabelPosition { right, left } @@ -21,20 +15,19 @@ class RadioControl extends StatefulWidget { final Control? parent; final Control control; final bool parentDisabled; - final dynamic dispatch; const RadioControl( {super.key, this.parent, required this.control, - required this.parentDisabled, - required this.dispatch}); + required this.parentDisabled}); @override State<RadioControl> createState() => _RadioControlState(); } -class _RadioControlState extends State<RadioControl> { +class _RadioControlState extends State<RadioControl> + with FletControlStatefulMixin, FletStoreMixin { late final FocusNode _focusNode; @override @@ -45,10 +38,8 @@ class _RadioControlState extends State<RadioControl> { } void _onFocusChange() { - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, - eventName: _focusNode.hasFocus ? "focus" : "blur", - eventData: ""); + sendControlEvent( + widget.control.id, _focusNode.hasFocus ? "focus" : "blur", ""); } @override @@ -61,100 +52,86 @@ class _RadioControlState extends State<RadioControl> { void _onChange(String ancestorId, String? value) { var svalue = value ?? ""; debugPrint(svalue); - List<Map<String, String>> props = [ - {"i": ancestorId, "value": svalue} - ]; - widget.dispatch( - UpdateControlPropsAction(UpdateControlPropsPayload(props: props))); - - final server = FletAppServices.of(context).server; - server.updateControlProps(props: props); - server.sendPageEvent( - eventTarget: ancestorId, eventName: "change", eventData: svalue); + updateControlProps(ancestorId, {"value": svalue}); + sendControlEvent(ancestorId, "change", svalue); } @override Widget build(BuildContext context) { debugPrint("Radio build: ${widget.control.id}"); - bool adaptive = widget.control.attrBool("adaptive", false)!; - if (adaptive && - (defaultTargetPlatform == TargetPlatform.iOS || - defaultTargetPlatform == TargetPlatform.macOS)) { - return CupertinoRadioControl( - control: widget.control, - parentDisabled: widget.parentDisabled, - dispatch: widget.dispatch); - } - - String label = widget.control.attrString("label", "")!; - String value = widget.control.attrString("value", "")!; - LabelPosition labelPosition = LabelPosition.values.firstWhere( - (p) => - p.name.toLowerCase() == - widget.control.attrString("labelPosition", "")!.toLowerCase(), - orElse: () => LabelPosition.right); - bool autofocus = widget.control.attrBool("autofocus", false)!; - bool disabled = widget.control.isDisabled || widget.parentDisabled; - - return StoreConnector<AppState, ControlAncestorViewModel>( - distinct: true, - ignoreChange: (state) { - return state.controls[widget.control.id] == null; - }, - converter: (store) => ControlAncestorViewModel.fromStore( - store, widget.control.id, "radiogroup"), - builder: (context, viewModel) { - debugPrint("Radio StoreConnector build: ${widget.control.id}"); - - if (viewModel.ancestor == null) { - return const ErrorControl( - "Radio control must be enclosed with RadioGroup."); - } - - String groupValue = viewModel.ancestor!.attrString("value", "")!; - String ancestorId = viewModel.ancestor!.id; - - var radio = Radio<String>( - autofocus: autofocus, - focusNode: _focusNode, - groupValue: groupValue, - value: value, - activeColor: HexColor.fromString(Theme.of(context), - widget.control.attrString("activeColor", "")!), - fillColor: parseMaterialStateColor( - Theme.of(context), widget.control, "fillColor"), - onChanged: !disabled - ? (String? value) { - _onChange(ancestorId, value); - } - : null); - - ListTileClicks.of(context)?.notifier.addListener(() { - _onChange(ancestorId, value); - }); - - Widget result = radio; - if (label != "") { - var labelWidget = disabled - ? Text(label, - style: TextStyle(color: Theme.of(context).disabledColor)) - : MouseRegion( - cursor: SystemMouseCursors.click, child: Text(label)); - result = MergeSemantics( - child: GestureDetector( - onTap: !disabled - ? () { - _onChange(ancestorId, value); - } - : null, - child: labelPosition == LabelPosition.right - ? Row(children: [radio, labelWidget]) - : Row(children: [labelWidget, radio]))); - } - - return constrainedControl( - context, result, widget.parent, widget.control); + return withPagePlatform((context, platform) { + bool adaptive = widget.control.attrBool("adaptive", false)!; + if (adaptive && + (platform == TargetPlatform.iOS || + platform == TargetPlatform.macOS)) { + return CupertinoRadioControl( + control: widget.control, parentDisabled: widget.parentDisabled); + } + + String label = widget.control.attrString("label", "")!; + String value = widget.control.attrString("value", "")!; + LabelPosition labelPosition = LabelPosition.values.firstWhere( + (p) => + p.name.toLowerCase() == + widget.control.attrString("labelPosition", "")!.toLowerCase(), + orElse: () => LabelPosition.right); + bool autofocus = widget.control.attrBool("autofocus", false)!; + bool disabled = widget.control.isDisabled || widget.parentDisabled; + + return withControlAncestor(widget.control.id, "radiogroup", + (context, viewModel) { + debugPrint("Radio StoreConnector build: ${widget.control.id}"); + + if (viewModel.ancestor == null) { + return const ErrorControl( + "Radio control must be enclosed with RadioGroup."); + } + + String groupValue = viewModel.ancestor!.attrString("value", "")!; + String ancestorId = viewModel.ancestor!.id; + + var radio = Radio<String>( + autofocus: autofocus, + focusNode: _focusNode, + groupValue: groupValue, + value: value, + activeColor: HexColor.fromString(Theme.of(context), + widget.control.attrString("activeColor", "")!), + fillColor: parseMaterialStateColor( + Theme.of(context), widget.control, "fillColor"), + onChanged: !disabled + ? (String? value) { + _onChange(ancestorId, value); + } + : null); + + ListTileClicks.of(context)?.notifier.addListener(() { + _onChange(ancestorId, value); }); + + Widget result = radio; + if (label != "") { + var labelWidget = disabled + ? Text(label, + style: TextStyle(color: Theme.of(context).disabledColor)) + : MouseRegion( + cursor: SystemMouseCursors.click, child: Text(label)); + result = MergeSemantics( + child: GestureDetector( + onTap: !disabled + ? () { + _onChange(ancestorId, value); + } + : null, + child: labelPosition == LabelPosition.right + ? Row(children: [radio, labelWidget]) + : Row(children: [labelWidget, radio]))); + } + + return constrainedControl( + context, result, widget.parent, widget.control); + }); + }); } } diff --git a/package/lib/src/controls/range_slider.dart b/package/lib/src/controls/range_slider.dart index 1818093c3..381f59f57 100644 --- a/package/lib/src/controls/range_slider.dart +++ b/package/lib/src/controls/range_slider.dart @@ -1,33 +1,30 @@ import 'package:flutter/material.dart'; -import '../actions.dart'; -import '../flet_app_services.dart'; + import '../models/control.dart'; -import '../protocol/update_control_props_payload.dart'; import '../utils/colors.dart'; +import '../utils/debouncer.dart'; import '../utils/desktop.dart'; import 'create_control.dart'; -import '../utils/buttons.dart'; -import '../utils/debouncer.dart'; +import 'flet_control_stateful_mixin.dart'; class RangeSliderControl extends StatefulWidget { final Control? parent; final Control control; final bool parentDisabled; - final dynamic dispatch; const RangeSliderControl({ super.key, this.parent, required this.control, required this.parentDisabled, - required this.dispatch, }); @override State<RangeSliderControl> createState() => _SliderControlState(); } -class _SliderControlState extends State<RangeSliderControl> { +class _SliderControlState extends State<RangeSliderControl> + with FletControlStatefulMixin { final _debouncer = Debouncer(milliseconds: isDesktop() ? 10 : 100); @override @@ -42,24 +39,14 @@ class _SliderControlState extends State<RangeSliderControl> { } void onChange(double startValue, double endValue) { - var strStartValue = startValue.toString(); - var strEndValue = endValue.toString(); - - List<Map<String, String>> props = [ - { - "i": widget.control.id, - "startvalue": strStartValue, - "endvalue": strEndValue - } - ]; - widget.dispatch( - UpdateControlPropsAction(UpdateControlPropsPayload(props: props))); - + var props = { + "startvalue": startValue.toString(), + "endvalue": endValue.toString() + }; + updateControlProps(widget.control.id, props, clientOnly: true); _debouncer.run(() { - final server = FletAppServices.of(context).server; - server.updateControlProps(props: props); - server.sendPageEvent( - eventTarget: widget.control.id, eventName: "change", eventData: ''); + updateControlProps(widget.control.id, props); + sendControlEvent(widget.control.id, "change", ""); }); } @@ -78,9 +65,7 @@ class _SliderControlState extends State<RangeSliderControl> { int? divisions = widget.control.attrInt("divisions"); int round = widget.control.attrInt("round", 0)!; - final server = FletAppServices.of(context).server; - - debugPrint("SliderControl StoreConnector build: ${widget.control.id}"); + debugPrint("SliderControl build: ${widget.control.id}"); var rangeSlider = RangeSlider( values: RangeValues(startValue, endValue), @@ -105,18 +90,12 @@ class _SliderControlState extends State<RangeSliderControl> { : null, onChangeStart: !disabled ? (RangeValues newValues) { - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "change_start", - eventData: ''); + sendControlEvent(widget.control.id, "change_start", ''); } : null, onChangeEnd: !disabled ? (RangeValues newValues) { - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "change_end", - eventData: ''); + sendControlEvent(widget.control.id, "change_end", ''); } : null); diff --git a/package/lib/src/controls/responsive_row.dart b/package/lib/src/controls/responsive_row.dart index 02d9423d3..1a7d28244 100644 --- a/package/lib/src/controls/responsive_row.dart +++ b/package/lib/src/controls/responsive_row.dart @@ -1,15 +1,15 @@ import 'package:flutter/widgets.dart'; -import 'package:flutter_redux/flutter_redux.dart'; -import '../models/app_state.dart'; import '../models/control.dart'; -import '../models/page_size_view_model.dart'; import '../utils/alignment.dart'; import '../utils/responsive.dart'; import 'create_control.dart'; import 'error.dart'; +import 'flet_control_stateless_mixin.dart'; +import 'flet_store_mixin.dart'; -class ResponsiveRowControl extends StatelessWidget { +class ResponsiveRowControl extends StatelessWidget + with FletControlStatelessMixin, FletStoreMixin { final Control? parent; final Control control; final bool parentDisabled; @@ -31,86 +31,82 @@ class ResponsiveRowControl extends StatelessWidget { final runSpacing = parseResponsiveNumber(control, "runSpacing", 10); bool disabled = control.isDisabled || parentDisabled; - return StoreConnector<AppState, PageSizeViewModel>( - distinct: true, - converter: (store) => PageSizeViewModel.fromStore(store), - builder: (context, view) { - var w = LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - debugPrint( - "ResponsiveRow constraints.maxWidth: ${constraints.maxWidth}"); - debugPrint( - "ResponsiveRow constraints.maxHeight: ${constraints.maxHeight}"); + return withPageSize((context, view) { + var w = LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + debugPrint( + "ResponsiveRow constraints.maxWidth: ${constraints.maxWidth}"); + debugPrint( + "ResponsiveRow constraints.maxHeight: ${constraints.maxHeight}"); - var bpSpacing = - getBreakpointNumber(spacing, view.size.width, view.breakpoints); + var bpSpacing = + getBreakpointNumber(spacing, view.size.width, view.breakpoints); - var bpColumns = - getBreakpointNumber(columns, view.size.width, view.breakpoints); + var bpColumns = + getBreakpointNumber(columns, view.size.width, view.breakpoints); - double totalCols = 0; - List<Widget> controls = []; - for (var ctrl in children.where((c) => c.isVisible)) { - final col = parseResponsiveNumber(ctrl, "col", 12); - var bpCol = - getBreakpointNumber(col, view.size.width, view.breakpoints); - totalCols += bpCol; + double totalCols = 0; + List<Widget> controls = []; + for (var ctrl in children.where((c) => c.isVisible)) { + final col = parseResponsiveNumber(ctrl, "col", 12); + var bpCol = + getBreakpointNumber(col, view.size.width, view.breakpoints); + totalCols += bpCol; - // calculate child width - var colWidth = - (constraints.maxWidth - bpSpacing * (bpColumns - 1)) / - bpColumns; - var childWidth = colWidth * bpCol + bpSpacing * (bpCol - 1); + // calculate child width + var colWidth = + (constraints.maxWidth - bpSpacing * (bpColumns - 1)) / bpColumns; + var childWidth = colWidth * bpCol + bpSpacing * (bpCol - 1); - controls.add(ConstrainedBox( - constraints: BoxConstraints( - minWidth: childWidth, - maxWidth: childWidth, - ), - child: createControl(control, ctrl.id, disabled), - )); - } + controls.add(ConstrainedBox( + constraints: BoxConstraints( + minWidth: childWidth, + maxWidth: childWidth, + ), + child: createControl(control, ctrl.id, disabled), + )); + } - var wrap = (totalCols > bpColumns); + var wrap = (totalCols > bpColumns); - if (!wrap && bpSpacing > 0) { - var i = 1; - while (i < controls.length) { - controls.insert(i, SizedBox(width: bpSpacing)); - i += 2; - } - } + if (!wrap && bpSpacing > 0) { + var i = 1; + while (i < controls.length) { + controls.insert(i, SizedBox(width: bpSpacing)); + i += 2; + } + } - try { - return wrap - ? Wrap( - direction: Axis.horizontal, - spacing: bpSpacing - 0.1, - runSpacing: getBreakpointNumber( - runSpacing, view.size.width, view.breakpoints), - alignment: parseWrapAlignment( - control, "alignment", WrapAlignment.start), - crossAxisAlignment: parseWrapCrossAlignment(control, - "verticalAlignment", WrapCrossAlignment.start), - children: controls, - ) - : Row( - mainAxisAlignment: parseMainAxisAlignment( - control, "alignment", MainAxisAlignment.start), - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: parseCrossAxisAlignment(control, - "verticalAlignment", CrossAxisAlignment.start), - children: controls, - ); - } catch (e) { - return ErrorControl( - "Error displaying ResponsiveRow", - description: e.toString(), - ); - } - }); + try { + return wrap + ? Wrap( + direction: Axis.horizontal, + spacing: bpSpacing - 0.1, + runSpacing: getBreakpointNumber( + runSpacing, view.size.width, view.breakpoints), + alignment: parseWrapAlignment( + control, "alignment", WrapAlignment.start), + crossAxisAlignment: parseWrapCrossAlignment( + control, "verticalAlignment", WrapCrossAlignment.start), + children: controls, + ) + : Row( + mainAxisAlignment: parseMainAxisAlignment( + control, "alignment", MainAxisAlignment.start), + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: parseCrossAxisAlignment( + control, "verticalAlignment", CrossAxisAlignment.start), + children: controls, + ); + } catch (e) { + return ErrorControl( + "Error displaying ResponsiveRow", + description: e.toString(), + ); + } + }); - return constrainedControl(context, w, parent, control); - }); + return constrainedControl(context, w, parent, control); + }); } } diff --git a/package/lib/src/controls/row.dart b/package/lib/src/controls/row.dart index 5810ac3ea..2f631fca3 100644 --- a/package/lib/src/controls/row.dart +++ b/package/lib/src/controls/row.dart @@ -11,15 +11,13 @@ class RowControl extends StatelessWidget { final Control control; final bool parentDisabled; final List<Control> children; - final dynamic dispatch; const RowControl( {super.key, this.parent, required this.control, required this.children, - required this.parentDisabled, - required this.dispatch}); + required this.parentDisabled}); @override Widget build(BuildContext context) { @@ -73,7 +71,6 @@ class RowControl extends StatelessWidget { child = ScrollableControl( control: control, scrollDirection: wrap ? Axis.vertical : Axis.horizontal, - dispatch: dispatch, child: child, ); diff --git a/package/lib/src/controls/scroll_notification_control.dart b/package/lib/src/controls/scroll_notification_control.dart index 010320b69..9dfd737de 100644 --- a/package/lib/src/controls/scroll_notification_control.dart +++ b/package/lib/src/controls/scroll_notification_control.dart @@ -2,8 +2,8 @@ import 'dart:convert'; import 'package:flutter/widgets.dart'; -import '../flet_app_services.dart'; import '../models/control.dart'; +import 'flet_control_stateful_mixin.dart'; class ScrollNotificationControl extends StatefulWidget { final Widget child; @@ -17,7 +17,8 @@ class ScrollNotificationControl extends StatefulWidget { _ScrollNotificationControlState(); } -class _ScrollNotificationControlState extends State<ScrollNotificationControl> { +class _ScrollNotificationControlState extends State<ScrollNotificationControl> + with FletControlStatefulMixin { int _onScrollInterval = 0; final Map<String, int> _lastEventTimestamps = {}; @@ -41,8 +42,7 @@ class _ScrollNotificationControlState extends State<ScrollNotificationControl> { } debugPrint("ScrollNotification ${widget.control.id} event"); - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, eventName: "onScroll", eventData: d); + sendControlEvent(widget.control.id, "onScroll", d); } if (notification.depth == 0) { diff --git a/package/lib/src/controls/scrollable_control.dart b/package/lib/src/controls/scrollable_control.dart index f9e01ca31..f00342069 100644 --- a/package/lib/src/controls/scrollable_control.dart +++ b/package/lib/src/controls/scrollable_control.dart @@ -3,14 +3,13 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import '../actions.dart'; import '../flet_app_services.dart'; import '../models/control.dart'; -import '../protocol/update_control_props_payload.dart'; import '../utils/animations.dart'; import '../utils/desktop.dart'; import '../utils/numbers.dart'; import '../widgets/adjustable_scroll_controller.dart'; +import 'flet_control_stateful_mixin.dart'; enum ScrollMode { none, auto, adaptive, always, hidden } @@ -19,21 +18,21 @@ class ScrollableControl extends StatefulWidget { final Widget child; final Axis scrollDirection; final ScrollController? scrollController; - final dynamic dispatch; - const ScrollableControl( - {super.key, - required this.control, - required this.child, - required this.scrollDirection, - this.scrollController, - required this.dispatch}); + const ScrollableControl({ + super.key, + required this.control, + required this.child, + required this.scrollDirection, + this.scrollController, + }); @override State<ScrollableControl> createState() => _ScrollableControlState(); } -class _ScrollableControlState extends State<ScrollableControl> { +class _ScrollableControlState extends State<ScrollableControl> + with FletControlStatefulMixin { late final ScrollController _controller; late bool _ownController = false; String? _method; @@ -88,13 +87,7 @@ class _ScrollableControlState extends State<ScrollableControl> { } else if (method != null && method != _method) { _method = method; debugPrint("ScrollableControl JSON method: $method"); - - List<Map<String, String>> props = [ - {"i": widget.control.id, "method": ""} - ]; - widget.dispatch( - UpdateControlPropsAction(UpdateControlPropsPayload(props: props))); - FletAppServices.of(context).server.updateControlProps(props: props); + updateControlProps(widget.control.id, {"method": ""}); var mj = json.decode(method); var name = mj["n"] as String; diff --git a/package/lib/src/controls/search_anchor.dart b/package/lib/src/controls/search_anchor.dart index a54e873e3..bcbe6c373 100644 --- a/package/lib/src/controls/search_anchor.dart +++ b/package/lib/src/controls/search_anchor.dart @@ -1,39 +1,33 @@ import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:flutter_redux/flutter_redux.dart'; -import '../actions.dart'; -import '../flet_app_services.dart'; -import '../models/app_state.dart'; import '../models/control.dart'; -import '../protocol/update_control_props_payload.dart'; import '../utils/borders.dart'; -import '../utils/buttons.dart'; import '../utils/colors.dart'; import '../utils/text.dart'; import 'create_control.dart'; +import 'flet_control_stateful_mixin.dart'; class SearchAnchorControl extends StatefulWidget { final Control? parent; final Control control; final List<Control> children; final bool parentDisabled; - final dynamic dispatch; const SearchAnchorControl( {super.key, this.parent, required this.control, required this.children, - required this.parentDisabled, - required this.dispatch}); + required this.parentDisabled}); @override State<SearchAnchorControl> createState() => _SearchAnchorControlState(); } -class _SearchAnchorControlState extends State<SearchAnchorControl> { +class _SearchAnchorControlState extends State<SearchAnchorControl> + with FletControlStatefulMixin { late final SearchController _controller; @override @@ -57,12 +51,7 @@ class _SearchAnchorControlState extends State<SearchAnchorControl> { void _updateValue(String value) { debugPrint("SearchBar.changeValue: $value"); - List<Map<String, String>> props = [ - {"i": widget.control.id, "value": value} - ]; - widget.dispatch( - UpdateControlPropsAction(UpdateControlPropsPayload(props: props))); - FletAppServices.of(context).server.updateControlProps(props: props); + updateControlProps(widget.control.id, {"value": value}); } @override @@ -72,162 +61,138 @@ class _SearchAnchorControlState extends State<SearchAnchorControl> { debugPrint(widget.control.attrs.toString()); - return StoreConnector<AppState, Function>( - distinct: true, - converter: (store) => store.dispatch, - builder: (context, dispatch) { - debugPrint("SearchAnchor StoreConnector build: ${widget.control.id}"); - - var value = widget.control.attrString("value"); - if (value != null && value != _controller.text) { - WidgetsBinding.instance.addPostFrameCallback((_) { - _controller.text = value; - }); - } + debugPrint("SearchAnchor build: ${widget.control.id}"); - bool onChange = widget.control.attrBool("onChange", false)!; - bool onTap = widget.control.attrBool("onTap", false)!; - bool onSubmit = widget.control.attrBool("onSubmit", false)!; - - var suggestionCtrls = - widget.children.where((c) => c.name == "controls" && c.isVisible); - var barLeadingCtrls = widget.children - .where((c) => c.name == "barLeading" && c.isVisible); - var barTrailingCtrls = widget.children - .where((c) => c.name == "barTrailing" && c.isVisible); - var viewLeadingCtrls = widget.children - .where((c) => c.name == "viewLeading" && c.isVisible); - var viewTrailingCtrls = widget.children - .where((c) => c.name == "viewTrailing" && c.isVisible); - - var viewBgcolor = HexColor.fromString( - Theme.of(context), widget.control.attrString("viewBgcolor", "")!); - var dividerColor = HexColor.fromString(Theme.of(context), - widget.control.attrString("dividerColor", "")!); - - TextStyle? viewHeaderTextStyle = parseTextStyle( - Theme.of(context), widget.control, "viewHeaderTextStyle"); - TextStyle? viewHintTextStyle = parseTextStyle( - Theme.of(context), widget.control, "viewHintTextStyle"); - - var method = widget.control.attrString("method"); - - if (method != null) { - debugPrint("SearchAnchor JSON method: $method"); - - void resetMethod() { - List<Map<String, String>> props = [ - {"i": widget.control.id, "method": ""} - ]; - widget.dispatch(UpdateControlPropsAction( - UpdateControlPropsPayload(props: props))); - FletAppServices.of(context) - .server - .updateControlProps(props: props); - } - - var mj = json.decode(method); - var name = mj["n"] as String; - var params = Map<String, dynamic>.from(mj["p"] as Map); - - if (name == "closeView") { - WidgetsBinding.instance.addPostFrameCallback((_) { - resetMethod(); - if (_controller.isOpen) { - var text = params["text"].toString(); - _updateValue(text); - _controller.closeView(text); - } - }); - } else if (name == "openView") { - WidgetsBinding.instance.addPostFrameCallback((_) { - resetMethod(); - if (!_controller.isOpen) { - _controller.openView(); - } - }); - } + var value = widget.control.attrString("value"); + if (value != null && value != _controller.text) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _controller.text = value; + }); + } + + bool onChange = widget.control.attrBool("onChange", false)!; + bool onTap = widget.control.attrBool("onTap", false)!; + bool onSubmit = widget.control.attrBool("onSubmit", false)!; + + var suggestionCtrls = + widget.children.where((c) => c.name == "controls" && c.isVisible); + var barLeadingCtrls = + widget.children.where((c) => c.name == "barLeading" && c.isVisible); + var barTrailingCtrls = + widget.children.where((c) => c.name == "barTrailing" && c.isVisible); + var viewLeadingCtrls = + widget.children.where((c) => c.name == "viewLeading" && c.isVisible); + var viewTrailingCtrls = + widget.children.where((c) => c.name == "viewTrailing" && c.isVisible); + + var viewBgcolor = HexColor.fromString( + Theme.of(context), widget.control.attrString("viewBgcolor", "")!); + var dividerColor = HexColor.fromString( + Theme.of(context), widget.control.attrString("dividerColor", "")!); + + TextStyle? viewHeaderTextStyle = parseTextStyle( + Theme.of(context), widget.control, "viewHeaderTextStyle"); + TextStyle? viewHintTextStyle = + parseTextStyle(Theme.of(context), widget.control, "viewHintTextStyle"); + + var method = widget.control.attrString("method"); + + if (method != null) { + debugPrint("SearchAnchor JSON method: $method"); + + void resetMethod() { + updateControlProps(widget.control.id, {"method": ""}); + } + + var mj = json.decode(method); + var name = mj["n"] as String; + var params = Map<String, dynamic>.from(mj["p"] as Map); + + if (name == "closeView") { + WidgetsBinding.instance.addPostFrameCallback((_) { + resetMethod(); + if (_controller.isOpen) { + var text = params["text"].toString(); + _updateValue(text); + _controller.closeView(text); } - - Widget anchor = SearchAnchor( - searchController: _controller, - headerHintStyle: viewHintTextStyle, - headerTextStyle: viewHeaderTextStyle, - viewSide: parseBorderSide( - Theme.of(context), widget.control, "viewSide"), - isFullScreen: widget.control.attrBool("fullScreen", false), - viewBackgroundColor: viewBgcolor, - dividerColor: dividerColor, - viewHintText: widget.control.attrString("viewHintText"), - viewElevation: widget.control.attrDouble("viewElevation"), - viewShape: parseOutlinedBorder(widget.control, "viewShape"), - viewTrailing: viewTrailingCtrls.isNotEmpty - ? viewTrailingCtrls.map((ctrl) { - return createControl(widget.parent, ctrl.id, disabled); - }) - : null, - viewLeading: viewLeadingCtrls.isNotEmpty - ? createControl( - widget.parent, viewLeadingCtrls.first.id, disabled) - : null, - builder: (BuildContext context, SearchController controller) { - return SearchBar( - controller: controller, - hintText: widget.control.attrString("barHintText"), - backgroundColor: parseMaterialStateColor( - Theme.of(context), widget.control, "barBgcolor"), - overlayColor: parseMaterialStateColor( - Theme.of(context), widget.control, "barOverlayColor"), - leading: barLeadingCtrls.isNotEmpty - ? createControl( - widget.parent, barLeadingCtrls.first.id, disabled) - : null, - trailing: barTrailingCtrls.isNotEmpty - ? barTrailingCtrls.map((ctrl) { - return createControl( - widget.parent, ctrl.id, disabled); - }) - : null, - onTap: () { - if (onTap) { - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "tap", - eventData: ""); - } - controller.openView(); - }, - onSubmitted: onSubmit - ? (String value) { - debugPrint("SearchBar.onSubmit: $value"); - _updateValue(value); - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "submit", - eventData: value); - } - : null, - onChanged: onChange - ? (String value) { - debugPrint("SearchBar.onChange: $value"); - _updateValue(value); - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "change", - eventData: value); - } - : null, - ); - }, - suggestionsBuilder: - (BuildContext context, SearchController controller) { - return suggestionCtrls.map((ctrl) { - return createControl(widget.parent, ctrl.id, disabled); - }); - }); - - return constrainedControl( - context, anchor, widget.parent, widget.control); }); + } else if (name == "openView") { + WidgetsBinding.instance.addPostFrameCallback((_) { + resetMethod(); + if (!_controller.isOpen) { + _controller.openView(); + } + }); + } + } + + Widget anchor = SearchAnchor( + searchController: _controller, + headerHintStyle: viewHintTextStyle, + headerTextStyle: viewHeaderTextStyle, + viewSide: + parseBorderSide(Theme.of(context), widget.control, "viewSide"), + isFullScreen: widget.control.attrBool("fullScreen", false), + viewBackgroundColor: viewBgcolor, + dividerColor: dividerColor, + viewHintText: widget.control.attrString("viewHintText"), + viewElevation: widget.control.attrDouble("viewElevation"), + viewShape: parseOutlinedBorder(widget.control, "viewShape"), + viewTrailing: viewTrailingCtrls.isNotEmpty + ? viewTrailingCtrls.map((ctrl) { + return createControl(widget.parent, ctrl.id, disabled); + }) + : null, + viewLeading: viewLeadingCtrls.isNotEmpty + ? createControl(widget.parent, viewLeadingCtrls.first.id, disabled) + : null, + builder: (BuildContext context, SearchController controller) { + return SearchBar( + controller: controller, + hintText: widget.control.attrString("barHintText"), + backgroundColor: parseMaterialStateColor( + Theme.of(context), widget.control, "barBgcolor"), + overlayColor: parseMaterialStateColor( + Theme.of(context), widget.control, "barOverlayColor"), + leading: barLeadingCtrls.isNotEmpty + ? createControl( + widget.parent, barLeadingCtrls.first.id, disabled) + : null, + trailing: barTrailingCtrls.isNotEmpty + ? barTrailingCtrls.map((ctrl) { + return createControl(widget.parent, ctrl.id, disabled); + }) + : null, + onTap: () { + if (onTap) { + sendControlEvent(widget.control.id, "tap", ""); + } + controller.openView(); + }, + onSubmitted: onSubmit + ? (String value) { + debugPrint("SearchBar.onSubmit: $value"); + _updateValue(value); + sendControlEvent(widget.control.id, "submit", value); + } + : null, + onChanged: onChange + ? (String value) { + debugPrint("SearchBar.onChange: $value"); + _updateValue(value); + sendControlEvent(widget.control.id, "change", value); + } + : null, + ); + }, + suggestionsBuilder: + (BuildContext context, SearchController controller) { + return suggestionCtrls.map((ctrl) { + return createControl(widget.parent, ctrl.id, disabled); + }); + }); + + return constrainedControl(context, anchor, widget.parent, widget.control); } } diff --git a/package/lib/src/controls/segmented_button.dart b/package/lib/src/controls/segmented_button.dart index 0e80e14e8..5bfc14650 100644 --- a/package/lib/src/controls/segmented_button.dart +++ b/package/lib/src/controls/segmented_button.dart @@ -1,24 +1,19 @@ import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:flutter_redux/flutter_redux.dart'; -import '../actions.dart'; -import '../flet_app_services.dart'; -import '../models/app_state.dart'; import '../models/control.dart'; -import '../models/controls_view_model.dart'; -import '../protocol/update_control_props_payload.dart'; import '../utils/buttons.dart'; import 'create_control.dart'; import 'error.dart'; +import 'flet_control_stateful_mixin.dart'; +import 'flet_store_mixin.dart'; class SegmentedButtonControl extends StatefulWidget { final Control? parent; final Control control; final List<Control> children; final bool parentDisabled; - final dynamic dispatch; const SegmentedButtonControl({ super.key, @@ -26,30 +21,18 @@ class SegmentedButtonControl extends StatefulWidget { required this.control, required this.children, required this.parentDisabled, - required this.dispatch, }); @override State<SegmentedButtonControl> createState() => _SegmentedButtonControlState(); } -class _SegmentedButtonControlState extends State<SegmentedButtonControl> { +class _SegmentedButtonControlState extends State<SegmentedButtonControl> + with FletControlStatefulMixin, FletStoreMixin { void onChange(Set<String> selection) { var s = jsonEncode(selection.toList()); - - List<Map<String, String>> props = [ - { - "i": widget.control.id, - "selected": s, - } - ]; - widget.dispatch( - UpdateControlPropsAction(UpdateControlPropsPayload(props: props))); - - final server = FletAppServices.of(context).server; - server.updateControlProps(props: props); - server.sendPageEvent( - eventTarget: widget.control.id, eventName: "change", eventData: s); + updateControlProps(widget.control.id, {"selected": s}); + sendControlEvent(widget.control.id, "change", s); } @override @@ -106,52 +89,46 @@ class _SegmentedButtonControlState extends State<SegmentedButtonControl> { bool showSelectedIcon = widget.control.attrBool("showSelectedIcon", true)!; bool disabled = widget.control.isDisabled || widget.parentDisabled; - debugPrint( - "SegmentedButtonControl StoreConnector build: ${widget.control.id}"); - - var sb = StoreConnector<AppState, ControlsViewModel>( - distinct: true, - converter: (store) => - ControlsViewModel.fromStore(store, segments.map((s) => s.id)), - builder: (content, segmentViews) { - return SegmentedButton<String>( - emptySelectionAllowed: allowEmptySelection, - multiSelectionEnabled: allowMultipleSelection, - selected: selected.isNotEmpty ? selected : {}, - showSelectedIcon: showSelectedIcon, - style: style, - selectedIcon: selectedIcon.isNotEmpty - ? createControl( - widget.control, selectedIcon.first.id, disabled) - : null, - onSelectionChanged: !disabled - ? (newSelection) { - onChange(newSelection.toSet()); - } - : null, - segments: segmentViews.controlViews.map((segmentView) { - var iconCtrls = segmentView.children - .where((c) => c.name == "icon" && c.isVisible); - var labelCtrls = segmentView.children - .where((c) => c.name == "label" && c.isVisible); - var enabled = !segmentView.control.attrBool("disabled", false)!; - - return ButtonSegment( - value: segmentView.control.attrString("value")!, - enabled: enabled, - tooltip: enabled && !disabled - ? segmentView.control.attrString("tooltip") - : null, - icon: iconCtrls.isNotEmpty - ? createControl( - segmentView.control, iconCtrls.first.id, disabled) - : null, - label: labelCtrls.isNotEmpty - ? createControl( - segmentView.control, labelCtrls.first.id, disabled) - : null); - }).toList()); - }); + debugPrint("SegmentedButtonControl build: ${widget.control.id}"); + + var sb = withControls(segments.map((s) => s.id), (content, segmentViews) { + return SegmentedButton<String>( + emptySelectionAllowed: allowEmptySelection, + multiSelectionEnabled: allowMultipleSelection, + selected: selected.isNotEmpty ? selected : {}, + showSelectedIcon: showSelectedIcon, + style: style, + selectedIcon: selectedIcon.isNotEmpty + ? createControl(widget.control, selectedIcon.first.id, disabled) + : null, + onSelectionChanged: !disabled + ? (newSelection) { + onChange(newSelection.toSet()); + } + : null, + segments: segmentViews.controlViews.map((segmentView) { + var iconCtrls = segmentView.children + .where((c) => c.name == "icon" && c.isVisible); + var labelCtrls = segmentView.children + .where((c) => c.name == "label" && c.isVisible); + var enabled = !segmentView.control.attrBool("disabled", false)!; + + return ButtonSegment( + value: segmentView.control.attrString("value")!, + enabled: enabled, + tooltip: enabled && !disabled + ? segmentView.control.attrString("tooltip") + : null, + icon: iconCtrls.isNotEmpty + ? createControl( + segmentView.control, iconCtrls.first.id, disabled) + : null, + label: labelCtrls.isNotEmpty + ? createControl( + segmentView.control, labelCtrls.first.id, disabled) + : null); + }).toList()); + }); return constrainedControl(context, sb, widget.parent, widget.control); } diff --git a/package/lib/src/controls/shake_detector.dart b/package/lib/src/controls/shake_detector.dart index 6cb6eea10..197a46ac2 100644 --- a/package/lib/src/controls/shake_detector.dart +++ b/package/lib/src/controls/shake_detector.dart @@ -4,8 +4,8 @@ import 'dart:math'; import 'package:flutter/widgets.dart'; import 'package:sensors_plus/sensors_plus.dart'; -import '../flet_app_services.dart'; import '../models/control.dart'; +import 'flet_control_stateful_mixin.dart'; class ShakeDetectorControl extends StatefulWidget { final Control? parent; @@ -22,7 +22,8 @@ class ShakeDetectorControl extends StatefulWidget { State<ShakeDetectorControl> createState() => _ShakeDetectorControlState(); } -class _ShakeDetectorControlState extends State<ShakeDetectorControl> { +class _ShakeDetectorControlState extends State<ShakeDetectorControl> + with FletControlStatefulMixin { ShakeDetector? _shakeDetector; int? _minimumShakeCount; int? _shakeSlopTimeMs; @@ -58,10 +59,7 @@ class _ShakeDetectorControlState extends State<ShakeDetectorControl> { _shakeDetector?.stopListening(); _shakeDetector = ShakeDetector.autoStart( onPhoneShake: () { - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "shake", - eventData: ""); + sendControlEvent(widget.control.id, "shake", ""); }, minimumShakeCount: minimumShakeCount, shakeSlopTimeMS: shakeSlopTimeMs, diff --git a/package/lib/src/controls/slider.dart b/package/lib/src/controls/slider.dart index c43558d18..a0af2e236 100644 --- a/package/lib/src/controls/slider.dart +++ b/package/lib/src/controls/slider.dart @@ -1,33 +1,31 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import '../actions.dart'; -import '../flet_app_services.dart'; + import '../models/control.dart'; -import '../protocol/update_control_props_payload.dart'; import '../utils/colors.dart'; -import '../utils/desktop.dart'; import '../utils/debouncer.dart'; +import '../utils/desktop.dart'; import 'create_control.dart'; import 'cupertino_slider.dart'; +import 'flet_control_stateful_mixin.dart'; +import 'flet_store_mixin.dart'; class SliderControl extends StatefulWidget { final Control? parent; final Control control; final bool parentDisabled; - final dynamic dispatch; const SliderControl( {super.key, this.parent, required this.control, - required this.parentDisabled, - required this.dispatch}); + required this.parentDisabled}); @override State<SliderControl> createState() => _SliderControlState(); } -class _SliderControlState extends State<SliderControl> { +class _SliderControlState extends State<SliderControl> + with FletControlStatefulMixin, FletStoreMixin { double _value = 0; final _debouncer = Debouncer(milliseconds: isDesktop() ? 10 : 100); late final FocusNode _focusNode; @@ -48,30 +46,19 @@ class _SliderControlState extends State<SliderControl> { } void _onFocusChange() { - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, - eventName: _focusNode.hasFocus ? "focus" : "blur", - eventData: ""); + sendControlEvent( + widget.control.id, _focusNode.hasFocus ? "focus" : "blur", ""); } void onChange(double value) { var svalue = value.toString(); debugPrint(svalue); - setState(() { - _value = value; - }); - - List<Map<String, String>> props = [ - {"i": widget.control.id, "value": svalue} - ]; - widget.dispatch( - UpdateControlPropsAction(UpdateControlPropsPayload(props: props))); - + _value = value; + var props = {"value": svalue}; + updateControlProps(widget.control.id, props, clientOnly: true); _debouncer.run(() { - final server = FletAppServices.of(context).server; - server.updateControlProps(props: props); - server.sendPageEvent( - eventTarget: widget.control.id, eventName: "change", eventData: ''); + updateControlProps(widget.control.id, props); + sendControlEvent(widget.control.id, "change", ''); }); } @@ -79,77 +66,71 @@ class _SliderControlState extends State<SliderControl> { Widget build(BuildContext context) { debugPrint("SliderControl build: ${widget.control.id}"); - bool adaptive = widget.control.attrBool("adaptive", false)!; - if (adaptive && - (defaultTargetPlatform == TargetPlatform.iOS || - defaultTargetPlatform == TargetPlatform.macOS)) { - return CupertinoSliderControl( - control: widget.control, - parentDisabled: widget.parentDisabled, - dispatch: widget.dispatch); - } - - String? label = widget.control.attrString("label"); - bool autofocus = widget.control.attrBool("autofocus", false)!; - bool disabled = widget.control.isDisabled || widget.parentDisabled; - - double min = widget.control.attrDouble("min", 0)!; - double max = widget.control.attrDouble("max", 1)!; - int? divisions = widget.control.attrInt("divisions"); - int round = widget.control.attrInt("round", 0)!; - - final server = FletAppServices.of(context).server; - - debugPrint("SliderControl StoreConnector build: ${widget.control.id}"); - - double value = widget.control.attrDouble("value", 0)!; - if (_value != value) { - // verify limits - if (value < min) { - _value = min; - } else if (value > max) { - _value = max; - } else { - _value = value; + return withPagePlatform((context, platform) { + bool adaptive = widget.control.attrBool("adaptive", false)!; + if (adaptive && + (platform == TargetPlatform.iOS || + platform == TargetPlatform.macOS)) { + return CupertinoSliderControl( + control: widget.control, parentDisabled: widget.parentDisabled); + } + + String? label = widget.control.attrString("label"); + bool autofocus = widget.control.attrBool("autofocus", false)!; + bool disabled = widget.control.isDisabled || widget.parentDisabled; + + double min = widget.control.attrDouble("min", 0)!; + double max = widget.control.attrDouble("max", 1)!; + int? divisions = widget.control.attrInt("divisions"); + int round = widget.control.attrInt("round", 0)!; + + debugPrint("SliderControl build: ${widget.control.id}"); + + double value = widget.control.attrDouble("value", 0)!; + if (_value != value) { + // verify limits + if (value < min) { + _value = min; + } else if (value > max) { + _value = max; + } else { + _value = value; + } } - } - - var slider = Slider( - autofocus: autofocus, - focusNode: _focusNode, - value: _value, - min: min, - max: max, - divisions: divisions, - label: label?.replaceAll("{value}", _value.toStringAsFixed(round)), - activeColor: HexColor.fromString( - Theme.of(context), widget.control.attrString("activeColor", "")!), - inactiveColor: HexColor.fromString( - Theme.of(context), widget.control.attrString("inactiveColor", "")!), - thumbColor: HexColor.fromString( - Theme.of(context), widget.control.attrString("thumbColor", "")!), - onChanged: !disabled - ? (double value) { - onChange(value); - } - : null, - onChangeStart: !disabled - ? (double value) { - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "change_start", - eventData: value.toString()); - } - : null, - onChangeEnd: !disabled - ? (double value) { - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "change_end", - eventData: value.toString()); - } - : null); - - return constrainedControl(context, slider, widget.parent, widget.control); + + var slider = Slider( + autofocus: autofocus, + focusNode: _focusNode, + value: _value, + min: min, + max: max, + divisions: divisions, + label: label?.replaceAll("{value}", _value.toStringAsFixed(round)), + activeColor: HexColor.fromString( + Theme.of(context), widget.control.attrString("activeColor", "")!), + inactiveColor: HexColor.fromString(Theme.of(context), + widget.control.attrString("inactiveColor", "")!), + thumbColor: HexColor.fromString( + Theme.of(context), widget.control.attrString("thumbColor", "")!), + onChanged: !disabled + ? (double value) { + onChange(value); + } + : null, + onChangeStart: !disabled + ? (double value) { + sendControlEvent( + widget.control.id, "change_start", value.toString()); + } + : null, + onChangeEnd: !disabled + ? (double value) { + sendControlEvent( + widget.control.id, "change_end", value.toString()); + } + : null); + + return constrainedControl(context, slider, widget.parent, widget.control); + }); } } diff --git a/package/lib/src/controls/snack_bar.dart b/package/lib/src/controls/snack_bar.dart index 6b09fc828..e8f8e8ba6 100644 --- a/package/lib/src/controls/snack_bar.dart +++ b/package/lib/src/controls/snack_bar.dart @@ -1,16 +1,12 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_redux/flutter_redux.dart'; -import '../actions.dart'; -import '../flet_app_services.dart'; -import '../models/app_state.dart'; import '../models/control.dart'; -import '../protocol/update_control_props_payload.dart'; import '../utils/colors.dart'; import '../utils/edge_insets.dart'; import 'create_control.dart'; import 'error.dart'; +import 'flet_control_stateful_mixin.dart'; class SnackBarControl extends StatefulWidget { final Control? parent; @@ -31,7 +27,8 @@ class SnackBarControl extends StatefulWidget { State<SnackBarControl> createState() => _SnackBarControlState(); } -class _SnackBarControlState extends State<SnackBarControl> { +class _SnackBarControlState extends State<SnackBarControl> + with FletControlStatefulMixin { bool _open = false; Widget _createSnackBar() { @@ -50,10 +47,7 @@ class _SnackBarControlState extends State<SnackBarControl> { widget.control.attrString("actionColor", "")!), onPressed: () { debugPrint("SnackBar ${widget.control.id} clicked!"); - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "action", - eventData: ""); + sendControlEvent(widget.control.id, "action", ""); }) : null; @@ -89,47 +83,34 @@ class _SnackBarControlState extends State<SnackBarControl> { Widget build(BuildContext context) { debugPrint("SnackBar build: ${widget.control.id}"); - return StoreConnector<AppState, Function>( - distinct: true, - converter: (store) => store.dispatch, - builder: (context, dispatch) { - debugPrint("SnackBar StoreConnector build: ${widget.control.id}"); - - var open = widget.control.attrBool("open", false)!; - var removeCurrentSnackbar = true; + debugPrint("SnackBar build: ${widget.control.id}"); - //widget.control.attrBool("removeCurrentSnackBar", false)!; + var open = widget.control.attrBool("open", false)!; + var removeCurrentSnackbar = true; - debugPrint("Current open state: $_open"); - debugPrint("New open state: $open"); + //widget.control.attrBool("removeCurrentSnackBar", false)!; - if (open && (open != _open)) { - var snackBar = _createSnackBar(); - if (snackBar is ErrorControl) { - return snackBar; - } + debugPrint("Current open state: $_open"); + debugPrint("New open state: $open"); - WidgetsBinding.instance.addPostFrameCallback((_) { - if (removeCurrentSnackbar) { - ScaffoldMessenger.of(context).removeCurrentSnackBar(); - } + if (open && (open != _open)) { + var snackBar = _createSnackBar(); + if (snackBar is ErrorControl) { + return snackBar; + } - ScaffoldMessenger.of(context).showSnackBar(snackBar as SnackBar); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (removeCurrentSnackbar) { + ScaffoldMessenger.of(context).removeCurrentSnackBar(); + } + ScaffoldMessenger.of(context).showSnackBar(snackBar as SnackBar); - List<Map<String, String>> props = [ - {"i": widget.control.id, "open": "false"} - ]; - dispatch(UpdateControlPropsAction( - UpdateControlPropsPayload(props: props))); - FletAppServices.of(context) - .server - .updateControlProps(props: props); - }); - } + updateControlProps(widget.control.id, {"open": "false"}); + }); + } - _open = open; + _open = open; - return widget.nextChild ?? const SizedBox.shrink(); - }); + return widget.nextChild ?? const SizedBox.shrink(); } } diff --git a/package/lib/src/controls/submenu_button.dart b/package/lib/src/controls/submenu_button.dart index ff2048667..a96483b0f 100644 --- a/package/lib/src/controls/submenu_button.dart +++ b/package/lib/src/controls/submenu_button.dart @@ -1,13 +1,11 @@ import 'package:flutter/material.dart'; -import '../flet_app_services.dart'; import '../models/control.dart'; import '../utils/buttons.dart'; import '../utils/menu.dart'; import '../utils/transforms.dart'; - import 'create_control.dart'; - +import 'flet_control_stateful_mixin.dart'; class SubMenuButtonControl extends StatefulWidget { final Control? parent; @@ -26,7 +24,8 @@ class SubMenuButtonControl extends StatefulWidget { State<SubMenuButtonControl> createState() => _SubMenuButtonControlState(); } -class _SubMenuButtonControlState extends State<SubMenuButtonControl> { +class _SubMenuButtonControlState extends State<SubMenuButtonControl> + with FletControlStatefulMixin { late final FocusNode _focusNode; String? _lastFocusValue; @@ -45,10 +44,8 @@ class _SubMenuButtonControlState extends State<SubMenuButtonControl> { } void _onFocusChange() { - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, - eventName: _focusNode.hasFocus ? "focus" : "blur", - eventData: ""); + sendControlEvent( + widget.control.id, _focusNode.hasFocus ? "focus" : "blur", ""); } @override @@ -94,8 +91,6 @@ class _SubMenuButtonControlState extends State<SubMenuButtonControl> { bool onClose = widget.control.attrBool("onClose", false)!; bool onHover = widget.control.attrBool("onHover", false)!; - var server = FletAppServices.of(context).server; - var subMenu = SubmenuButton( focusNode: _focusNode, clipBehavior: clipBehavior, @@ -106,26 +101,17 @@ class _SubMenuButtonControlState extends State<SubMenuButtonControl> { : null, onClose: onClose && !disabled ? () { - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "close", - eventData: ""); + sendControlEvent(widget.control.id, "close", ""); } : null, onHover: onHover && !disabled ? (bool value) { - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "hover", - eventData: "$value"); + sendControlEvent(widget.control.id, "hover", "$value"); } : null, onOpen: onOpen && !disabled ? () { - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "open", - eventData: ""); + sendControlEvent(widget.control.id, "open", ""); } : null, leadingIcon: leading.isNotEmpty diff --git a/package/lib/src/controls/switch.dart b/package/lib/src/controls/switch.dart index a4f5f0cec..df1cd7b42 100644 --- a/package/lib/src/controls/switch.dart +++ b/package/lib/src/controls/switch.dart @@ -1,18 +1,13 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_redux/flutter_redux.dart'; -import '../actions.dart'; -import '../flet_app_services.dart'; -import '../models/app_state.dart'; import '../models/control.dart'; -import '../protocol/update_control_props_payload.dart'; -import '../utils/buttons.dart'; import '../utils/colors.dart'; import '../utils/icons.dart'; import 'create_control.dart'; -import 'list_tile.dart'; import 'cupertino_switch.dart'; +import 'flet_control_stateful_mixin.dart'; +import 'flet_store_mixin.dart'; +import 'list_tile.dart'; enum LabelPosition { right, left } @@ -20,20 +15,19 @@ class SwitchControl extends StatefulWidget { final Control? parent; final Control control; final bool parentDisabled; - final dynamic dispatch; const SwitchControl( {super.key, this.parent, required this.control, - required this.parentDisabled, - required this.dispatch}); + required this.parentDisabled}); @override State<SwitchControl> createState() => _SwitchControlState(); } -class _SwitchControlState extends State<SwitchControl> { +class _SwitchControlState extends State<SwitchControl> + with FletControlStatefulMixin, FletStoreMixin { bool _value = false; late final FocusNode _focusNode; @@ -54,112 +48,94 @@ class _SwitchControlState extends State<SwitchControl> { void _onChange(bool value) { var svalue = value.toString(); debugPrint(svalue); - setState(() { - _value = value; - }); - List<Map<String, String>> props = [ - {"i": widget.control.id, "value": svalue} - ]; - widget.dispatch( - UpdateControlPropsAction(UpdateControlPropsPayload(props: props))); - final server = FletAppServices.of(context).server; - server.updateControlProps(props: props); - server.sendPageEvent( - eventTarget: widget.control.id, eventName: "change", eventData: svalue); + _value = value; + updateControlProps(widget.control.id, {"value": svalue}); + sendControlEvent(widget.control.id, "change", svalue); } void _onFocusChange() { - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, - eventName: _focusNode.hasFocus ? "focus" : "blur", - eventData: ""); + sendControlEvent( + widget.control.id, _focusNode.hasFocus ? "focus" : "blur", ""); } @override Widget build(BuildContext context) { debugPrint("SwitchControl build: ${widget.control.id}"); - bool adaptive = widget.control.attrBool("adaptive", false)!; - if (adaptive && - (defaultTargetPlatform == TargetPlatform.iOS || - defaultTargetPlatform == TargetPlatform.macOS)) { - return CupertinoSwitchControl( - control: widget.control, - parentDisabled: widget.parentDisabled, - dispatch: widget.dispatch); - } - - String label = widget.control.attrString("label", "")!; - LabelPosition labelPosition = LabelPosition.values.firstWhere( - (p) => - p.name.toLowerCase() == - widget.control.attrString("labelPosition", "")!.toLowerCase(), - orElse: () => LabelPosition.right); - bool autofocus = widget.control.attrBool("autofocus", false)!; - bool disabled = widget.control.isDisabled || widget.parentDisabled; - - return StoreConnector<AppState, Function>( - distinct: true, - converter: (store) => store.dispatch, - builder: (context, dispatch) { - debugPrint("Switch StoreConnector build: ${widget.control.id}"); - - bool value = widget.control.attrBool("value", false)!; - if (_value != value) { - _value = value; - } - - var swtch = Switch( - autofocus: autofocus, - focusNode: _focusNode, - activeColor: HexColor.fromString(Theme.of(context), - widget.control.attrString("activeColor", "")!), - activeTrackColor: HexColor.fromString(Theme.of(context), - widget.control.attrString("activeTrackColor", "")!), - inactiveThumbColor: HexColor.fromString(Theme.of(context), - widget.control.attrString("inactiveThumbColor", "")!), - inactiveTrackColor: HexColor.fromString(Theme.of(context), - widget.control.attrString("inactiveTrackColor", "")!), - thumbColor: parseMaterialStateColor( - Theme.of(context), widget.control, "thumbColor"), - thumbIcon: parseMaterialStateIcon( - Theme.of(context), widget.control, "thumbIcon"), - trackColor: parseMaterialStateColor( - Theme.of(context), widget.control, "trackColor"), - focusColor: HexColor.fromString(Theme.of(context), - widget.control.attrString("focusColor", "")!), - value: _value, - onChanged: !disabled - ? (bool value) { - _onChange(value); - } - : null); - - ListTileClicks.of(context)?.notifier.addListener(() { - _onChange(!_value); - }); - - Widget result = swtch; - if (label != "") { - var labelWidget = disabled - ? Text(label, - style: TextStyle(color: Theme.of(context).disabledColor)) - : MouseRegion( - cursor: SystemMouseCursors.click, child: Text(label)); - result = MergeSemantics( - child: GestureDetector( - onTap: !disabled - ? () { - _onChange(!_value); - } - : null, - child: labelPosition == LabelPosition.right - ? Row(children: [swtch, labelWidget]) - : Row(children: [labelWidget, swtch]))); - } - - return constrainedControl( - context, result, widget.parent, widget.control); - }); + return withPagePlatform((context, platform) { + bool adaptive = widget.control.attrBool("adaptive", false)!; + if (adaptive && + (platform == TargetPlatform.iOS || + platform == TargetPlatform.macOS)) { + return CupertinoSwitchControl( + control: widget.control, parentDisabled: widget.parentDisabled); + } + + String label = widget.control.attrString("label", "")!; + LabelPosition labelPosition = LabelPosition.values.firstWhere( + (p) => + p.name.toLowerCase() == + widget.control.attrString("labelPosition", "")!.toLowerCase(), + orElse: () => LabelPosition.right); + bool autofocus = widget.control.attrBool("autofocus", false)!; + bool disabled = widget.control.isDisabled || widget.parentDisabled; + + debugPrint("Switch build: ${widget.control.id}"); + + bool value = widget.control.attrBool("value", false)!; + if (_value != value) { + _value = value; + } + + var swtch = Switch( + autofocus: autofocus, + focusNode: _focusNode, + activeColor: HexColor.fromString( + Theme.of(context), widget.control.attrString("activeColor", "")!), + activeTrackColor: HexColor.fromString(Theme.of(context), + widget.control.attrString("activeTrackColor", "")!), + inactiveThumbColor: HexColor.fromString(Theme.of(context), + widget.control.attrString("inactiveThumbColor", "")!), + inactiveTrackColor: HexColor.fromString(Theme.of(context), + widget.control.attrString("inactiveTrackColor", "")!), + thumbColor: parseMaterialStateColor( + Theme.of(context), widget.control, "thumbColor"), + thumbIcon: parseMaterialStateIcon( + Theme.of(context), widget.control, "thumbIcon"), + trackColor: parseMaterialStateColor( + Theme.of(context), widget.control, "trackColor"), + focusColor: HexColor.fromString( + Theme.of(context), widget.control.attrString("focusColor", "")!), + value: _value, + onChanged: !disabled + ? (bool value) { + _onChange(value); + } + : null); + + ListTileClicks.of(context)?.notifier.addListener(() { + _onChange(!_value); + }); + + Widget result = swtch; + if (label != "") { + var labelWidget = disabled + ? Text(label, + style: TextStyle(color: Theme.of(context).disabledColor)) + : MouseRegion(cursor: SystemMouseCursors.click, child: Text(label)); + result = MergeSemantics( + child: GestureDetector( + onTap: !disabled + ? () { + _onChange(!_value); + } + : null, + child: labelPosition == LabelPosition.right + ? Row(children: [swtch, labelWidget]) + : Row(children: [labelWidget, swtch]))); + } + + return constrainedControl(context, result, widget.parent, widget.control); + }); } } diff --git a/package/lib/src/controls/tabs.dart b/package/lib/src/controls/tabs.dart index fb174f580..b09461428 100644 --- a/package/lib/src/controls/tabs.dart +++ b/package/lib/src/controls/tabs.dart @@ -1,42 +1,46 @@ import 'dart:convert'; -import 'package:flet/src/utils/alignment.dart'; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; -import '../actions.dart'; -import '../flet_app_services.dart'; import '../models/app_state.dart'; import '../models/control.dart'; import '../models/controls_view_model.dart'; -import '../protocol/update_control_props_payload.dart'; +import '../utils/alignment.dart'; import '../utils/borders.dart'; import '../utils/colors.dart'; import '../utils/edge_insets.dart'; import '../utils/icons.dart'; import '../utils/material_state.dart'; import 'create_control.dart'; +import 'flet_control_stateful_mixin.dart'; class TabsControl extends StatefulWidget { final Control? parent; final Control control; final List<Control> children; final bool parentDisabled; - final dynamic dispatch; const TabsControl( {super.key, this.parent, required this.control, required this.children, - required this.parentDisabled, - required this.dispatch}); + required this.parentDisabled}); @override State<TabsControl> createState() => _TabsControlState(); } -class _TabsControlState extends State<TabsControl> +class _TabsControlStateWithControlState extends State<TabsControl> + with FletControlStatefulMixin { + @override + Widget build(BuildContext context) { + throw UnimplementedError(); + } +} + +class _TabsControlState extends _TabsControlStateWithControlState with TickerProviderStateMixin { String? _tabsSnapshot; TabController? _tabController; @@ -56,17 +60,9 @@ class _TabsControlState extends State<TabsControl> var index = _tabController!.index; if (_selectedIndex != index) { debugPrint("Selected index: $index"); - List<Map<String, String>> props = [ - {"i": widget.control.id, "selectedindex": index.toString()} - ]; - widget.dispatch( - UpdateControlPropsAction(UpdateControlPropsPayload(props: props))); - final server = FletAppServices.of(context).server; - server.updateControlProps(props: props); - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "change", - eventData: index.toString()); + updateControlProps( + widget.control.id, {"selectedindex": index.toString()}); + sendControlEvent(widget.control.id, "change", index.toString()); _selectedIndex = index; } } @@ -187,8 +183,7 @@ class _TabsControlState extends State<TabsControl> TabBarTheme.of(context).overlayColor, tabs: viewModel.controlViews.map((tabView) { var text = tabView.control.attrString("text"); - var icon = - parseIcon(tabView.control.attrString("icon", "")!); + var icon = parseIcon(tabView.control.attrString("icon", "")!); var tabContentCtrls = tabView.children .where((c) => c.name == "tab_content" && c.isVisible); diff --git a/package/lib/src/controls/text.dart b/package/lib/src/controls/text.dart index 1762ae652..6cce0fd95 100644 --- a/package/lib/src/controls/text.dart +++ b/package/lib/src/controls/text.dart @@ -1,18 +1,17 @@ import 'dart:ui'; import 'package:flutter/material.dart'; -import 'package:flutter_redux/flutter_redux.dart'; -import '../flet_app_services.dart'; -import '../models/app_state.dart'; import '../models/control.dart'; -import '../models/control_tree_view_model.dart'; import '../utils/colors.dart'; import '../utils/numbers.dart'; import '../utils/text.dart'; import 'create_control.dart'; +import 'flet_control_stateless_mixin.dart'; +import 'flet_store_mixin.dart'; -class TextControl extends StatelessWidget { +class TextControl extends StatelessWidget + with FletControlStatelessMixin, FletStoreMixin { final Control? parent; final Control control; final bool parentDisabled; @@ -26,121 +25,121 @@ class TextControl extends StatelessWidget { @override Widget build(BuildContext context) { - var result = StoreConnector<AppState, ControlTreeViewModel>( - distinct: true, - converter: (store) => ControlTreeViewModel.fromStore(store, control), - builder: (context, viewModel) { - debugPrint("Text build: ${control.id}"); - - bool disabled = control.isDisabled || parentDisabled; - - String text = control.attrString("value", "")!; - List<InlineSpan>? spans = parseTextSpans( - Theme.of(context), - viewModel, - disabled, - FletAppServices.of(context).server, - ); - String? semanticsLabel = control.attrString("semanticsLabel"); - bool noWrap = control.attrBool("noWrap", false)!; - int? maxLines = control.attrInt("maxLines"); - - TextStyle? style; - var styleNameOrData = control.attrString("style", null); - if (styleNameOrData != null) { - style = getTextStyle(context, styleNameOrData); - } - if (style == null && styleNameOrData != null) { - try { - style = parseTextStyle(Theme.of(context), control, "style"); - } on FormatException catch (_) { - style = null; - } - } - - TextStyle? themeStyle; - var styleName = control.attrString("theme_style", null); - if (styleName != null) { - themeStyle = getTextStyle(context, styleName); - } - - if (style == null && themeStyle != null) { - style = themeStyle; - } else if (style != null && themeStyle != null) { - style = themeStyle.merge(style); - } - - var fontWeight = control.attrString("weight", "")!; - - List<FontVariation> variations = []; - if (fontWeight.startsWith("w")) { - variations.add( - FontVariation('wght', parseDouble(fontWeight.substring(1)))); - } - - style = (style ?? const TextStyle()).copyWith( - fontSize: control.attrDouble("size", null), - fontWeight: getFontWeight(fontWeight), - fontStyle: control.attrBool( - "italic", - false, - )! - ? FontStyle.italic - : null, - fontFamily: control.attrString("fontFamily"), - fontVariations: variations, - color: HexColor.fromString( - Theme.of(context), control.attrString("color", "")!) ?? - (spans.isNotEmpty - ? DefaultTextStyle.of(context).style.color - : null), - backgroundColor: HexColor.fromString( - Theme.of(context), control.attrString("bgcolor", "")!)); - - TextAlign textAlign = TextAlign.values.firstWhere( - (a) => - a.name.toLowerCase() == - control.attrString("textAlign", "")!.toLowerCase(), - orElse: () => TextAlign.start); - - TextOverflow overflow = TextOverflow.values.firstWhere( - (v) => - v.name.toLowerCase() == - control.attrString("overflow", "")!.toLowerCase(), - orElse: () => TextOverflow.clip); - - return control.attrBool("selectable", false)! - ? (spans.isNotEmpty) - ? SelectableText.rich( - TextSpan(text: text, style: style, children: spans), - maxLines: maxLines, - textAlign: textAlign, - ) - : SelectableText( - text, - semanticsLabel: semanticsLabel, - maxLines: maxLines, - style: style, - textAlign: textAlign, - ) - : (spans.isNotEmpty) - ? RichText( - text: TextSpan(text: text, style: style, children: spans), - maxLines: maxLines, - softWrap: !noWrap, - textAlign: textAlign, - overflow: overflow, - ) - : Text( - text, - semanticsLabel: semanticsLabel, - maxLines: maxLines, - softWrap: !noWrap, - style: style, - textAlign: textAlign, - overflow: overflow, - ); - }); + var result = withControlTree(control, (context, viewModel) { + debugPrint("Text build: ${control.id}"); + + bool disabled = control.isDisabled || parentDisabled; + + String text = control.attrString("value", "")!; + + List<InlineSpan>? spans = parseTextSpans( + Theme.of(context), + viewModel, + disabled, + (String controlId, String eventName, String eventData) { + sendControlEvent(context, controlId, eventName, eventData); + }, + ); + String? semanticsLabel = control.attrString("semanticsLabel"); + bool noWrap = control.attrBool("noWrap", false)!; + int? maxLines = control.attrInt("maxLines"); + + TextStyle? style; + var styleNameOrData = control.attrString("style", null); + if (styleNameOrData != null) { + style = getTextStyle(context, styleNameOrData); + } + if (style == null && styleNameOrData != null) { + try { + style = parseTextStyle(Theme.of(context), control, "style"); + } on FormatException catch (_) { + style = null; + } + } + + TextStyle? themeStyle; + var styleName = control.attrString("theme_style", null); + if (styleName != null) { + themeStyle = getTextStyle(context, styleName); + } + + if (style == null && themeStyle != null) { + style = themeStyle; + } else if (style != null && themeStyle != null) { + style = themeStyle.merge(style); + } + + var fontWeight = control.attrString("weight", "")!; + + List<FontVariation> variations = []; + if (fontWeight.startsWith("w")) { + variations + .add(FontVariation('wght', parseDouble(fontWeight.substring(1)))); + } + + style = (style ?? const TextStyle()).copyWith( + fontSize: control.attrDouble("size", null), + fontWeight: getFontWeight(fontWeight), + fontStyle: control.attrBool( + "italic", + false, + )! + ? FontStyle.italic + : null, + fontFamily: control.attrString("fontFamily"), + fontVariations: variations, + color: HexColor.fromString( + Theme.of(context), control.attrString("color", "")!) ?? + (spans.isNotEmpty + ? DefaultTextStyle.of(context).style.color + : null), + backgroundColor: HexColor.fromString( + Theme.of(context), control.attrString("bgcolor", "")!)); + + TextAlign textAlign = TextAlign.values.firstWhere( + (a) => + a.name.toLowerCase() == + control.attrString("textAlign", "")!.toLowerCase(), + orElse: () => TextAlign.start); + + TextOverflow overflow = TextOverflow.values.firstWhere( + (v) => + v.name.toLowerCase() == + control.attrString("overflow", "")!.toLowerCase(), + orElse: () => TextOverflow.clip); + + return control.attrBool("selectable", false)! + ? (spans.isNotEmpty) + ? SelectableText.rich( + TextSpan(text: text, style: style, children: spans), + maxLines: maxLines, + textAlign: textAlign, + ) + : SelectableText( + text, + semanticsLabel: semanticsLabel, + maxLines: maxLines, + style: style, + textAlign: textAlign, + ) + : (spans.isNotEmpty) + ? RichText( + text: TextSpan(text: text, style: style, children: spans), + maxLines: maxLines, + softWrap: !noWrap, + textAlign: textAlign, + overflow: overflow, + ) + : Text( + text, + semanticsLabel: semanticsLabel, + maxLines: maxLines, + softWrap: !noWrap, + style: style, + textAlign: textAlign, + overflow: overflow, + ); + }); return constrainedControl(context, result, parent, control); } diff --git a/package/lib/src/controls/text_button.dart b/package/lib/src/controls/text_button.dart index c3e03af60..5ac1fab31 100644 --- a/package/lib/src/controls/text_button.dart +++ b/package/lib/src/controls/text_button.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; -import '../flet_app_services.dart'; import '../models/control.dart'; import '../utils/buttons.dart'; import '../utils/colors.dart'; import '../utils/icons.dart'; import '../utils/launch_url.dart'; import 'create_control.dart'; +import 'flet_control_stateful_mixin.dart'; class TextButtonControl extends StatefulWidget { final Control? parent; @@ -25,7 +25,8 @@ class TextButtonControl extends StatefulWidget { State<TextButtonControl> createState() => _TextButtonControlState(); } -class _TextButtonControlState extends State<TextButtonControl> { +class _TextButtonControlState extends State<TextButtonControl> + with FletControlStatefulMixin { late final FocusNode _focusNode; String? _lastFocusValue; @@ -44,18 +45,14 @@ class _TextButtonControlState extends State<TextButtonControl> { } void _onFocusChange() { - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, - eventName: _focusNode.hasFocus ? "focus" : "blur", - eventData: ""); + sendControlEvent( + widget.control.id, _focusNode.hasFocus ? "focus" : "blur", ""); } @override Widget build(BuildContext context) { debugPrint("Button build: ${widget.control.id}"); - final server = FletAppServices.of(context).server; - String text = widget.control.attrString("text", "")!; IconData? icon = parseIcon(widget.control.attrString("icon", "")!); Color? iconColor = HexColor.fromString( @@ -74,30 +71,21 @@ class _TextButtonControlState extends State<TextButtonControl> { if (url != "") { openWebBrowser(url, webWindowName: urlTarget); } - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "click", - eventData: ""); + sendControlEvent(widget.control.id, "click", ""); } : null; Function()? onLongPressHandler = onLongPress && !disabled ? () { debugPrint("Button ${widget.control.id} long pressed!"); - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "long_press", - eventData: ""); + sendControlEvent(widget.control.id, "long_press", ""); } : null; Function(bool)? onHoverHandler = onHover && !disabled ? (state) { debugPrint("Button ${widget.control.id} hovered!"); - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "hover", - eventData: state.toString()); + sendControlEvent(widget.control.id, "hover", state.toString()); } : null; diff --git a/package/lib/src/controls/textfield.dart b/package/lib/src/controls/textfield.dart index 513efc90f..201066a8c 100644 --- a/package/lib/src/controls/textfield.dart +++ b/package/lib/src/controls/textfield.dart @@ -1,19 +1,15 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_redux/flutter_redux.dart'; -import '../actions.dart'; -import '../flet_app_services.dart'; -import '../models/app_state.dart'; import '../models/control.dart'; -import '../protocol/update_control_props_payload.dart'; import '../utils/borders.dart'; import '../utils/colors.dart'; import '../utils/text.dart'; import '../utils/textfield.dart'; import 'create_control.dart'; import 'cupertino_textfield.dart'; +import 'flet_control_stateful_mixin.dart'; +import 'flet_store_mixin.dart'; import 'form_field.dart'; class TextFieldControl extends StatefulWidget { @@ -33,7 +29,8 @@ class TextFieldControl extends StatefulWidget { State<TextFieldControl> createState() => _TextFieldControlState(); } -class _TextFieldControlState extends State<TextFieldControl> { +class _TextFieldControlState extends State<TextFieldControl> + with FletControlStatefulMixin, FletStoreMixin { String _value = ""; bool _revealPassword = false; bool _focused = false; @@ -50,10 +47,7 @@ class _TextFieldControlState extends State<TextFieldControl> { onKey: (FocusNode node, RawKeyEvent evt) { if (!evt.isShiftPressed && evt.logicalKey.keyLabel == 'Enter') { if (evt is RawKeyDownEvent) { - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "submit", - eventData: ""); + sendControlEvent(widget.control.id, "submit", ""); } return KeyEventResult.handled; } else { @@ -80,247 +74,220 @@ class _TextFieldControlState extends State<TextFieldControl> { setState(() { _focused = _shiftEnterfocusNode.hasFocus; }); - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, - eventName: _shiftEnterfocusNode.hasFocus ? "focus" : "blur", - eventData: ""); + sendControlEvent(widget.control.id, + _shiftEnterfocusNode.hasFocus ? "focus" : "blur", ""); } void _onFocusChange() { setState(() { _focused = _focusNode.hasFocus; }); - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, - eventName: _focusNode.hasFocus ? "focus" : "blur", - eventData: ""); + sendControlEvent( + widget.control.id, _focusNode.hasFocus ? "focus" : "blur", ""); } @override Widget build(BuildContext context) { debugPrint("TextField build: ${widget.control.id}"); - bool autofocus = widget.control.attrBool("autofocus", false)!; - bool disabled = widget.control.isDisabled || widget.parentDisabled; - - bool adaptive = widget.control.attrBool("adaptive", false)!; - if (adaptive && - (defaultTargetPlatform == TargetPlatform.iOS || - defaultTargetPlatform == TargetPlatform.macOS)) { - return CupertinoTextFieldControl( - control: widget.control, - children: widget.children, - parent: widget.parent, - parentDisabled: widget.parentDisabled); - } + return withPagePlatform((context, platform) { + bool autofocus = widget.control.attrBool("autofocus", false)!; + bool disabled = widget.control.isDisabled || widget.parentDisabled; + + bool adaptive = widget.control.attrBool("adaptive", false)!; + if (adaptive && + (platform == TargetPlatform.iOS || + platform == TargetPlatform.macOS)) { + return CupertinoTextFieldControl( + control: widget.control, + children: widget.children, + parent: widget.parent, + parentDisabled: widget.parentDisabled); + } - return StoreConnector<AppState, Function>( - distinct: true, - converter: (store) => store.dispatch, - builder: (context, dispatch) { - debugPrint("TextField StoreConnector build: ${widget.control.id}"); + debugPrint("TextField build: ${widget.control.id}"); - String value = widget.control.attrs["value"] ?? ""; - if (_value != value) { - _value = value; - _controller.text = value; - } + String value = widget.control.attrs["value"] ?? ""; + if (_value != value) { + _value = value; + _controller.text = value; + } - var prefixControls = - widget.children.where((c) => c.name == "prefix" && c.isVisible); - var suffixControls = - widget.children.where((c) => c.name == "suffix" && c.isVisible); - - bool shiftEnter = widget.control.attrBool("shiftEnter", false)!; - bool multiline = - widget.control.attrBool("multiline", false)! || shiftEnter; - int minLines = widget.control.attrInt("minLines", 1)!; - int? maxLines = - widget.control.attrInt("maxLines", multiline ? null : 1); - - bool readOnly = widget.control.attrBool("readOnly", false)!; - bool password = widget.control.attrBool("password", false)!; - bool canRevealPassword = - widget.control.attrBool("canRevealPassword", false)!; - bool onChange = widget.control.attrBool("onChange", false)!; - - var cursorColor = HexColor.fromString( - Theme.of(context), widget.control.attrString("cursorColor", "")!); - var selectionColor = HexColor.fromString(Theme.of(context), - widget.control.attrString("selectionColor", "")!); - - int? maxLength = widget.control.attrInt("maxLength"); - - var textSize = widget.control.attrDouble("textSize"); - - var color = HexColor.fromString( - Theme.of(context), widget.control.attrString("color", "")!); - var focusedColor = HexColor.fromString(Theme.of(context), - widget.control.attrString("focusedColor", "")!); - - TextStyle? textStyle = - parseTextStyle(Theme.of(context), widget.control, "textStyle"); - if (textSize != null || color != null || focusedColor != null) { - textStyle = (textStyle ?? const TextStyle()).copyWith( - fontSize: textSize, - color: _focused ? focusedColor ?? color : color); - } + var prefixControls = + widget.children.where((c) => c.name == "prefix" && c.isVisible); + var suffixControls = + widget.children.where((c) => c.name == "suffix" && c.isVisible); + + bool shiftEnter = widget.control.attrBool("shiftEnter", false)!; + bool multiline = + widget.control.attrBool("multiline", false)! || shiftEnter; + int minLines = widget.control.attrInt("minLines", 1)!; + int? maxLines = widget.control.attrInt("maxLines", multiline ? null : 1); + + bool readOnly = widget.control.attrBool("readOnly", false)!; + bool password = widget.control.attrBool("password", false)!; + bool canRevealPassword = + widget.control.attrBool("canRevealPassword", false)!; + bool onChange = widget.control.attrBool("onChange", false)!; + + var cursorColor = HexColor.fromString( + Theme.of(context), widget.control.attrString("cursorColor", "")!); + var selectionColor = HexColor.fromString( + Theme.of(context), widget.control.attrString("selectionColor", "")!); + + int? maxLength = widget.control.attrInt("maxLength"); + + var textSize = widget.control.attrDouble("textSize"); + + var color = HexColor.fromString( + Theme.of(context), widget.control.attrString("color", "")!); + var focusedColor = HexColor.fromString( + Theme.of(context), widget.control.attrString("focusedColor", "")!); + + TextStyle? textStyle = + parseTextStyle(Theme.of(context), widget.control, "textStyle"); + if (textSize != null || color != null || focusedColor != null) { + textStyle = (textStyle ?? const TextStyle()).copyWith( + fontSize: textSize, + color: _focused ? focusedColor ?? color : color); + } - TextCapitalization? textCapitalization = TextCapitalization.values - .firstWhere( - (a) => - a.name.toLowerCase() == - widget.control - .attrString("capitalization", "")! - .toLowerCase(), - orElse: () => TextCapitalization.none); - - FilteringTextInputFormatter? inputFilter = - parseInputFilter(widget.control, "inputFilter"); - - List<TextInputFormatter>? inputFormatters = []; - // add non-null input formatters - if (inputFilter != null) { - inputFormatters.add(inputFilter); - } - if (textCapitalization != TextCapitalization.none) { - inputFormatters - .add(TextCapitalizationFormatter(textCapitalization)); - } + TextCapitalization? textCapitalization = TextCapitalization.values + .firstWhere( + (a) => + a.name.toLowerCase() == + widget.control + .attrString("capitalization", "")! + .toLowerCase(), + orElse: () => TextCapitalization.none); + + FilteringTextInputFormatter? inputFilter = + parseInputFilter(widget.control, "inputFilter"); + + List<TextInputFormatter>? inputFormatters = []; + // add non-null input formatters + if (inputFilter != null) { + inputFormatters.add(inputFilter); + } + if (textCapitalization != TextCapitalization.none) { + inputFormatters.add(TextCapitalizationFormatter(textCapitalization)); + } - Widget? revealPasswordIcon; - if (password && canRevealPassword) { - revealPasswordIcon = GestureDetector( - child: Icon( - _revealPassword ? Icons.visibility_off : Icons.visibility, - ), - onTap: () { - setState(() { - _revealPassword = !_revealPassword; - }); - }); - } + Widget? revealPasswordIcon; + if (password && canRevealPassword) { + revealPasswordIcon = GestureDetector( + child: Icon( + _revealPassword ? Icons.visibility_off : Icons.visibility, + ), + onTap: () { + setState(() { + _revealPassword = !_revealPassword; + }); + }); + } - TextInputType keyboardType = parseTextInputType( - widget.control.attrString("keyboardType", "")!); + TextInputType keyboardType = + parseTextInputType(widget.control.attrString("keyboardType", "")!); - if (multiline) { - keyboardType = TextInputType.multiline; - } + if (multiline) { + keyboardType = TextInputType.multiline; + } - TextAlign textAlign = TextAlign.values.firstWhere( - ((b) => - b.name == - widget.control.attrString("textAlign", "")!.toLowerCase()), - orElse: () => TextAlign.start, - ); - - bool autocorrect = widget.control.attrBool("autocorrect", true)!; - bool enableSuggestions = - widget.control.attrBool("enableSuggestions", true)!; - bool smartDashesType = - widget.control.attrBool("smartDashesType", true)!; - bool smartQuotesType = - widget.control.attrBool("smartQuotesType", true)!; - - FocusNode focusNode = shiftEnter ? _shiftEnterfocusNode : _focusNode; - - var focusValue = widget.control.attrString("focus"); - if (focusValue != null && focusValue != _lastFocusValue) { - _lastFocusValue = focusValue; - focusNode.requestFocus(); - } + TextAlign textAlign = TextAlign.values.firstWhere( + ((b) => + b.name == + widget.control.attrString("textAlign", "")!.toLowerCase()), + orElse: () => TextAlign.start, + ); + + bool autocorrect = widget.control.attrBool("autocorrect", true)!; + bool enableSuggestions = + widget.control.attrBool("enableSuggestions", true)!; + bool smartDashesType = widget.control.attrBool("smartDashesType", true)!; + bool smartQuotesType = widget.control.attrBool("smartQuotesType", true)!; + + FocusNode focusNode = shiftEnter ? _shiftEnterfocusNode : _focusNode; + + var focusValue = widget.control.attrString("focus"); + if (focusValue != null && focusValue != _lastFocusValue) { + _lastFocusValue = focusValue; + focusNode.requestFocus(); + } - Widget textField = TextFormField( - style: textStyle, - autofocus: autofocus, - enabled: !disabled, - onFieldSubmitted: !multiline - ? (_) { - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "submit", - eventData: ""); - } - : null, - decoration: buildInputDecoration( - context, - widget.control, - prefixControls.isNotEmpty ? prefixControls.first : null, - suffixControls.isNotEmpty ? suffixControls.first : null, - revealPasswordIcon, - _focused), - showCursor: widget.control.attrBool("showCursor"), - cursorHeight: widget.control.attrDouble("cursorHeight"), - cursorWidth: widget.control.attrDouble("cursorWidth") ?? 2.0, - cursorRadius: parseRadius(widget.control, "cursorRadius"), - keyboardType: keyboardType, - autocorrect: autocorrect, - enableSuggestions: enableSuggestions, - smartDashesType: smartDashesType - ? SmartDashesType.enabled - : SmartDashesType.disabled, - smartQuotesType: smartQuotesType - ? SmartQuotesType.enabled - : SmartQuotesType.disabled, - textAlign: textAlign, - minLines: minLines, - maxLines: maxLines, - maxLength: maxLength, - readOnly: readOnly, - inputFormatters: - inputFormatters.isNotEmpty ? inputFormatters : null, - obscureText: password && !_revealPassword, - controller: _controller, - focusNode: focusNode, - onChanged: (String value) { - //debugPrint(value); - setState(() { - _value = value; - }); - List<Map<String, String>> props = [ - {"i": widget.control.id, "value": value} - ]; - dispatch(UpdateControlPropsAction( - UpdateControlPropsPayload(props: props))); - FletAppServices.of(context) - .server - .updateControlProps(props: props); - if (onChange) { - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "change", - eventData: value); + Widget textField = TextFormField( + style: textStyle, + autofocus: autofocus, + enabled: !disabled, + onFieldSubmitted: !multiline + ? (_) { + sendControlEvent(widget.control.id, "submit", ""); } - }); + : null, + decoration: buildInputDecoration( + context, + widget.control, + prefixControls.isNotEmpty ? prefixControls.first : null, + suffixControls.isNotEmpty ? suffixControls.first : null, + revealPasswordIcon, + _focused), + showCursor: widget.control.attrBool("showCursor"), + cursorHeight: widget.control.attrDouble("cursorHeight"), + cursorWidth: widget.control.attrDouble("cursorWidth") ?? 2.0, + cursorRadius: parseRadius(widget.control, "cursorRadius"), + keyboardType: keyboardType, + autocorrect: autocorrect, + enableSuggestions: enableSuggestions, + smartDashesType: smartDashesType + ? SmartDashesType.enabled + : SmartDashesType.disabled, + smartQuotesType: smartQuotesType + ? SmartQuotesType.enabled + : SmartQuotesType.disabled, + textAlign: textAlign, + minLines: minLines, + maxLines: maxLines, + maxLength: maxLength, + readOnly: readOnly, + inputFormatters: inputFormatters.isNotEmpty ? inputFormatters : null, + obscureText: password && !_revealPassword, + controller: _controller, + focusNode: focusNode, + onChanged: (String value) { + //debugPrint(value); + _value = value; + updateControlProps(widget.control.id, {"value": value}); + if (onChange) { + sendControlEvent(widget.control.id, "change", value); + } + }); + + if (cursorColor != null || selectionColor != null) { + textField = TextSelectionTheme( + data: TextSelectionTheme.of(context).copyWith( + cursorColor: cursorColor, selectionColor: selectionColor), + child: textField); + } - if (cursorColor != null || selectionColor != null) { - textField = TextSelectionTheme( - data: TextSelectionTheme.of(context).copyWith( - cursorColor: cursorColor, selectionColor: selectionColor), - child: textField); - } + if (widget.control.attrInt("expand", 0)! > 0) { + return constrainedControl( + context, textField, widget.parent, widget.control); + } else { + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + if (constraints.maxWidth == double.infinity && + widget.control.attrDouble("width") == null) { + textField = ConstrainedBox( + constraints: const BoxConstraints.tightFor(width: 300), + child: textField, + ); + } - if (widget.control.attrInt("expand", 0)! > 0) { return constrainedControl( context, textField, widget.parent, widget.control); - } else { - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - if (constraints.maxWidth == double.infinity && - widget.control.attrDouble("width") == null) { - textField = ConstrainedBox( - constraints: const BoxConstraints.tightFor(width: 300), - child: textField, - ); - } - - return constrainedControl( - context, textField, widget.parent, widget.control); - }, - ); - } - }); + }, + ); + } + }); } } diff --git a/package/lib/src/controls/time_picker.dart b/package/lib/src/controls/time_picker.dart index 5d612b15b..dadad9a77 100644 --- a/package/lib/src/controls/time_picker.dart +++ b/package/lib/src/controls/time_picker.dart @@ -1,16 +1,13 @@ import 'package:flutter/material.dart'; -import '../actions.dart'; -import '../flet_app_services.dart'; import '../models/control.dart'; -import '../protocol/update_control_props_payload.dart'; +import 'flet_control_stateful_mixin.dart'; class TimePickerControl extends StatefulWidget { final Control? parent; final Control control; final List<Control> children; final bool parentDisabled; - final dynamic dispatch; const TimePickerControl({ super.key, @@ -18,14 +15,14 @@ class TimePickerControl extends StatefulWidget { required this.control, required this.children, required this.parentDisabled, - required this.dispatch, }); @override State<TimePickerControl> createState() => _TimePickerControlState(); } -class _TimePickerControlState extends State<TimePickerControl> { +class _TimePickerControlState extends State<TimePickerControl> + with FletControlStatefulMixin { @override Widget build(BuildContext context) { debugPrint("TimePicker build: ${widget.control.id}"); @@ -64,17 +61,9 @@ class _TimePickerControlState extends State<TimePickerControl> { eventName = "change"; } widget.control.state["open"] = false; - List<Map<String, String>> props = [ - {"i": widget.control.id, "value": stringValue, "open": "false"} - ]; - widget.dispatch( - UpdateControlPropsAction(UpdateControlPropsPayload(props: props))); - FletAppServices.of(context).server.updateControlProps(props: props); - - FletAppServices.of(context).server.sendPageEvent( - eventTarget: widget.control.id, - eventName: eventName, - eventData: stringValue); + updateControlProps( + widget.control.id, {"value": stringValue, "open": "false"}); + sendControlEvent(widget.control.id, eventName, stringValue); } Widget createSelectTimeDialog() { diff --git a/package/lib/src/controls/webview.dart b/package/lib/src/controls/webview.dart index 8dc45386d..434616bc1 100644 --- a/package/lib/src/controls/webview.dart +++ b/package/lib/src/controls/webview.dart @@ -2,18 +2,15 @@ import 'dart:io' show Platform; import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:flutter_redux/flutter_redux.dart'; import 'package:webview_flutter/webview_flutter.dart'; -import '../flet_app_services.dart'; -import '../models/app_state.dart'; import '../models/control.dart'; -import '../models/control_tree_view_model.dart'; import '../utils/colors.dart'; import 'create_control.dart'; import 'error.dart'; +import 'flet_control_stateless_mixin.dart'; -class WebViewControl extends StatelessWidget { +class WebViewControl extends StatelessWidget with FletControlStatelessMixin { final Control? parent; final Control control; final bool parentDisabled; @@ -26,68 +23,55 @@ class WebViewControl extends StatelessWidget { @override Widget build(BuildContext context) { - var result = StoreConnector<AppState, ControlTreeViewModel>( - distinct: true, - converter: (store) => ControlTreeViewModel.fromStore(store, control), - builder: (context, viewModel) { - debugPrint("WebViewControl build: ${control.id}"); + debugPrint("WebViewControl build: ${control.id}"); - String url = control.attrString("url", "")!; - if (url == "") { - return const ErrorControl("WebView.url cannot be empty."); - } + Widget? result; - bool javascriptEnabled = - control.attrBool("javascriptEnabled", false)!; - var bgcolor = HexColor.fromString( - Theme.of(context), control.attrString("bgcolor", "")!); - String preventLink = control.attrString("preventLink", "")!; + String url = control.attrString("url", "")!; + if (url == "") { + return const ErrorControl("WebView.url cannot be empty."); + } - if (Platform.isIOS || Platform.isAndroid) { - var controller = WebViewController() - ..setJavaScriptMode(javascriptEnabled - ? JavaScriptMode.unrestricted - : JavaScriptMode.disabled) - ..setNavigationDelegate( - NavigationDelegate( - onProgress: (int progress) {}, - onPageStarted: (String url) { - FletAppServices.of(context).server.sendPageEvent( - eventTarget: control.id, - eventName: "page_started", - eventData: url); - }, - onPageFinished: (String url) { - FletAppServices.of(context).server.sendPageEvent( - eventTarget: control.id, - eventName: "page_ended", - eventData: url); - }, - onWebResourceError: (WebResourceError error) { - FletAppServices.of(context).server.sendPageEvent( - eventTarget: control.id, - eventName: "web_resource_error", - eventData: error.toString()); - }, - onNavigationRequest: (NavigationRequest request) { - if (preventLink != "" && - request.url.startsWith(preventLink)) { - return NavigationDecision.prevent; - } - return NavigationDecision.navigate; - }, - ), - ); - if (bgcolor != null) { - controller.setBackgroundColor(bgcolor); - } - controller.loadRequest(Uri.parse(url)); - return WebViewWidget(controller: controller); - } else { - return const ErrorControl( - "WebView control is not supported on this platform yet."); - } - }); + bool javascriptEnabled = control.attrBool("javascriptEnabled", false)!; + var bgcolor = HexColor.fromString( + Theme.of(context), control.attrString("bgcolor", "")!); + String preventLink = control.attrString("preventLink", "")!; + + if (Platform.isIOS || Platform.isAndroid) { + var controller = WebViewController() + ..setJavaScriptMode(javascriptEnabled + ? JavaScriptMode.unrestricted + : JavaScriptMode.disabled) + ..setNavigationDelegate( + NavigationDelegate( + onProgress: (int progress) {}, + onPageStarted: (String url) { + sendControlEvent(context, control.id, "page_started", url); + }, + onPageFinished: (String url) { + sendControlEvent(context, control.id, "page_ended", url); + }, + onWebResourceError: (WebResourceError error) { + sendControlEvent( + context, control.id, "web_resource_error", error.toString()); + }, + onNavigationRequest: (NavigationRequest request) { + if (preventLink != "" && request.url.startsWith(preventLink)) { + return NavigationDecision.prevent; + } + return NavigationDecision.navigate; + }, + ), + ); + if (bgcolor != null) { + controller.setBackgroundColor(bgcolor); + } + controller.loadRequest(Uri.parse(url)); + result = WebViewWidget(controller: controller); + } else { + result = const ErrorControl( + "WebView control is not supported on this platform yet."); + } return constrainedControl(context, result, parent, control); } diff --git a/package/lib/src/flet_app.dart b/package/lib/src/flet_app.dart index 7d59716fe..8e1aa6b91 100644 --- a/package/lib/src/flet_app.dart +++ b/package/lib/src/flet_app.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'control_factory.dart'; import 'flet_app_errors_handler.dart'; import 'flet_app_main.dart'; import 'flet_app_services.dart'; @@ -13,6 +14,7 @@ class FletApp extends StatefulWidget { final FletAppErrorsHandler? errorsHandler; final int? reconnectIntervalMs; final int? reconnectTimeoutMs; + final List<CreateControlFactory>? createControlFactories; const FletApp( {super.key, @@ -23,7 +25,8 @@ class FletApp extends StatefulWidget { this.title, this.errorsHandler, this.reconnectIntervalMs, - this.reconnectTimeoutMs}); + this.reconnectTimeoutMs, + this.createControlFactories}); @override State<FletApp> createState() => _FletAppState(); @@ -52,6 +55,7 @@ class _FletAppState extends State<FletApp> { pageUrl: widget.pageUrl, assetsDir: widget.assetsDir, errorsHandler: widget.errorsHandler, + createControlFactories: widget.createControlFactories ?? [], child: FletAppMain(title: widget.title ?? "Flet")); } return _appServices!; diff --git a/package/lib/src/flet_app_services.dart b/package/lib/src/flet_app_services.dart index 2e85a33ec..6081b3ca2 100644 --- a/package/lib/src/flet_app_services.dart +++ b/package/lib/src/flet_app_services.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:redux/redux.dart'; import 'actions.dart'; +import 'control_factory.dart'; import 'flet_app_errors_handler.dart'; import 'flet_server.dart'; import 'flet_server_protocol.dart'; @@ -21,6 +22,7 @@ class FletAppServices extends InheritedWidget { late final Store<AppState> store; final Map<String, GlobalKey> globalKeys = {}; final Map<String, ControlInvokeMethodCallback> controlInvokeMethods = {}; + final List<CreateControlFactory> createControlFactories; FletAppServices( {super.key, @@ -32,7 +34,8 @@ class FletAppServices extends InheritedWidget { this.hideLoadingPage, this.controlId, this.reconnectIntervalMs, - this.reconnectTimeoutMs}) { + this.reconnectTimeoutMs, + required this.createControlFactories}) { store = Store<AppState>(appReducer, initialState: AppState.initial()); server = FletServer(store, controlInvokeMethods, reconnectIntervalMs: reconnectIntervalMs, diff --git a/package/lib/src/models/barchart_event_data.dart b/package/lib/src/models/barchart_event_data.dart deleted file mode 100644 index f438069e1..000000000 --- a/package/lib/src/models/barchart_event_data.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:equatable/equatable.dart'; - -class BarChartEventData extends Equatable { - final String eventType; - final int? groupIndex; - final int? rodIndex; - final int? stackItemIndex; - - const BarChartEventData( - {required this.eventType, - required this.groupIndex, - required this.rodIndex, - required this.stackItemIndex}); - - Map<String, dynamic> toJson() => <String, dynamic>{ - 'type': eventType, - 'group_index': groupIndex, - 'rod_index': rodIndex, - 'stack_item_index': stackItemIndex - }; - - @override - List<Object?> get props => [eventType, groupIndex, rodIndex, stackItemIndex]; -} diff --git a/package/lib/src/models/barchart_group_view_model.dart b/package/lib/src/models/barchart_group_view_model.dart deleted file mode 100644 index 9358b4bcc..000000000 --- a/package/lib/src/models/barchart_group_view_model.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:equatable/equatable.dart'; -import 'package:redux/redux.dart'; - -import 'app_state.dart'; -import 'barchart_rod_view_model.dart'; -import 'control.dart'; - -class BarChartGroupViewModel extends Equatable { - final Control control; - final List<BarChartRodViewModel> barRods; - - const BarChartGroupViewModel({required this.control, required this.barRods}); - - static BarChartGroupViewModel fromStore( - Store<AppState> store, Control control) { - return BarChartGroupViewModel( - control: control, - barRods: store.state.controls[control.id]!.childIds - .map((childId) => store.state.controls[childId]) - .whereNotNull() - .where((c) => c.isVisible) - .map((c) => BarChartRodViewModel.fromStore(store, c)) - .toList()); - } - - @override - List<Object?> get props => [control, barRods]; -} diff --git a/package/lib/src/models/barchart_rod_stack_item_view_model.dart b/package/lib/src/models/barchart_rod_stack_item_view_model.dart deleted file mode 100644 index 4b31084f1..000000000 --- a/package/lib/src/models/barchart_rod_stack_item_view_model.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:redux/redux.dart'; - -import 'app_state.dart'; -import 'control.dart'; - -class BarChartRodStackItemViewModel extends Equatable { - final Control control; - - const BarChartRodStackItemViewModel({required this.control}); - - static BarChartRodStackItemViewModel fromStore( - Store<AppState> store, Control control) { - return BarChartRodStackItemViewModel(control: control); - } - - @override - List<Object?> get props => [control]; -} diff --git a/package/lib/src/models/barchart_rod_view_model.dart b/package/lib/src/models/barchart_rod_view_model.dart deleted file mode 100644 index 15cf92149..000000000 --- a/package/lib/src/models/barchart_rod_view_model.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:equatable/equatable.dart'; -import 'package:redux/redux.dart'; - -import 'app_state.dart'; -import 'barchart_rod_stack_item_view_model.dart'; -import 'control.dart'; - -class BarChartRodViewModel extends Equatable { - final Control control; - final List<BarChartRodStackItemViewModel> rodStackItems; - - const BarChartRodViewModel( - {required this.control, required this.rodStackItems}); - - static BarChartRodViewModel fromStore( - Store<AppState> store, Control control) { - return BarChartRodViewModel( - control: control, - rodStackItems: store.state.controls[control.id]!.childIds - .map((childId) => store.state.controls[childId]) - .whereNotNull() - .where((c) => c.isVisible) - .map((c) => BarChartRodStackItemViewModel.fromStore(store, c)) - .toList()); - } - - @override - List<Object?> get props => [control, rodStackItems]; -} diff --git a/package/lib/src/models/barchart_view_model.dart b/package/lib/src/models/barchart_view_model.dart deleted file mode 100644 index 5d1c64ba2..000000000 --- a/package/lib/src/models/barchart_view_model.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:redux/redux.dart'; - -import 'app_state.dart'; -import 'barchart_group_view_model.dart'; -import 'chart_axis_view_model.dart'; -import 'control.dart'; - -class BarChartViewModel extends Equatable { - final Control control; - final ChartAxisViewModel? leftAxis; - final ChartAxisViewModel? topAxis; - final ChartAxisViewModel? rightAxis; - final ChartAxisViewModel? bottomAxis; - final List<BarChartGroupViewModel> barGroups; - final dynamic dispatch; - - const BarChartViewModel( - {required this.control, - required this.leftAxis, - required this.topAxis, - required this.rightAxis, - required this.bottomAxis, - required this.barGroups, - required this.dispatch}); - - static BarChartViewModel fromStore( - Store<AppState> store, Control control, List<Control> children) { - var leftAxisCtrls = - children.where((c) => c.type == "axis" && c.name == "l" && c.isVisible); - var topAxisCtrls = - children.where((c) => c.type == "axis" && c.name == "t" && c.isVisible); - var rightAxisCtrls = - children.where((c) => c.type == "axis" && c.name == "r" && c.isVisible); - var bottomAxisCtrls = - children.where((c) => c.type == "axis" && c.name == "b" && c.isVisible); - return BarChartViewModel( - control: control, - leftAxis: leftAxisCtrls.isNotEmpty - ? ChartAxisViewModel.fromStore(store, leftAxisCtrls.first) - : null, - topAxis: topAxisCtrls.isNotEmpty - ? ChartAxisViewModel.fromStore(store, topAxisCtrls.first) - : null, - rightAxis: rightAxisCtrls.isNotEmpty - ? ChartAxisViewModel.fromStore(store, rightAxisCtrls.first) - : null, - bottomAxis: bottomAxisCtrls.isNotEmpty - ? ChartAxisViewModel.fromStore(store, bottomAxisCtrls.first) - : null, - barGroups: children - .where((c) => c.type == "group" && c.isVisible) - .map((c) => BarChartGroupViewModel.fromStore(store, c)) - .toList(), - dispatch: store.dispatch); - } - - @override - List<Object?> get props => - [control, leftAxis, rightAxis, topAxis, bottomAxis, barGroups, dispatch]; -} diff --git a/package/lib/src/models/canvas_view_model.dart b/package/lib/src/models/canvas_view_model.dart deleted file mode 100644 index cf7e9053c..000000000 --- a/package/lib/src/models/canvas_view_model.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:equatable/equatable.dart'; -import 'package:redux/redux.dart'; - -import 'app_state.dart'; -import 'control.dart'; -import 'control_tree_view_model.dart'; - -class CanvasViewModel extends Equatable { - final Control control; - final Control? child; - final List<ControlTreeViewModel> shapes; - final dynamic dispatch; - - const CanvasViewModel( - {required this.control, - required this.child, - required this.shapes, - required this.dispatch}); - - static CanvasViewModel fromStore( - Store<AppState> store, Control control, List<Control> children) { - return CanvasViewModel( - control: control, - child: store.state.controls[control.id]!.childIds - .map((childId) => store.state.controls[childId]) - .whereNotNull() - .where((c) => c.name == "content" && c.isVisible) - .firstOrNull, - shapes: children - .where((c) => c.name != "content" && c.isVisible) - .map((c) => ControlTreeViewModel.fromStore(store, c)) - .toList(), - dispatch: store.dispatch); - } - - @override - List<Object?> get props => [control, shapes, dispatch]; -} diff --git a/package/lib/src/models/chart_axis_label_view_model.dart b/package/lib/src/models/chart_axis_label_view_model.dart deleted file mode 100644 index 794fe0834..000000000 --- a/package/lib/src/models/chart_axis_label_view_model.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:equatable/equatable.dart'; -import 'package:redux/redux.dart'; - -import 'app_state.dart'; -import 'control.dart'; - -class ChartAxisLabelViewModel extends Equatable { - final double value; - final Control? control; - - const ChartAxisLabelViewModel({required this.value, required this.control}); - - static ChartAxisLabelViewModel fromStore( - Store<AppState> store, Control control) { - return ChartAxisLabelViewModel( - value: control.attrDouble("value")!, - control: store.state.controls[control.id]!.childIds - .map((childId) => store.state.controls[childId]) - .whereNotNull() - .where((c) => c.isVisible) - .firstOrNull); - } - - @override - List<Object?> get props => [value, control]; -} diff --git a/package/lib/src/models/control_children_view_model.dart b/package/lib/src/models/control_children_view_model.dart deleted file mode 100644 index 6fb80b6d4..000000000 --- a/package/lib/src/models/control_children_view_model.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:redux/redux.dart'; - -import 'app_state.dart'; -import 'control.dart'; - -class ControlChildrenViewModel extends Equatable { - final List<Control> children; - - const ControlChildrenViewModel({required this.children}); - - static ControlChildrenViewModel fromStore(Store<AppState> store, String id, - {dynamic dispatch}) { - return ControlChildrenViewModel( - children: store.state.controls[id] != null - ? store.state.controls[id]!.childIds - .map((childId) => store.state.controls[childId]!) - .toList() - : []); - } - - @override - List<Object?> get props => [children]; -} diff --git a/package/lib/src/models/linechart_data_point_view_model.dart b/package/lib/src/models/linechart_data_point_view_model.dart deleted file mode 100644 index b39209bd3..000000000 --- a/package/lib/src/models/linechart_data_point_view_model.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:redux/redux.dart'; - -import 'app_state.dart'; -import 'control.dart'; - -class LineChartDataPointViewModel extends Equatable { - final Control control; - final double x; - final double y; - final String? tooltip; - - const LineChartDataPointViewModel( - {required this.control, - required this.x, - required this.y, - required this.tooltip}); - - static LineChartDataPointViewModel fromStore( - Store<AppState> store, Control control) { - return LineChartDataPointViewModel( - control: control, - x: control.attrDouble("x")!, - y: control.attrDouble("y")!, - tooltip: control.attrString("tooltip")); - } - - @override - List<Object?> get props => [control]; -} diff --git a/package/lib/src/models/linechart_data_view_model.dart b/package/lib/src/models/linechart_data_view_model.dart deleted file mode 100644 index 07cd2bf11..000000000 --- a/package/lib/src/models/linechart_data_view_model.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:equatable/equatable.dart'; -import 'package:redux/redux.dart'; - -import 'app_state.dart'; -import 'control.dart'; -import 'linechart_data_point_view_model.dart'; - -class LineChartDataViewModel extends Equatable { - final Control control; - final List<LineChartDataPointViewModel> dataPoints; - - const LineChartDataViewModel( - {required this.control, required this.dataPoints}); - - static LineChartDataViewModel fromStore( - Store<AppState> store, Control control) { - return LineChartDataViewModel( - control: control, - dataPoints: store.state.controls[control.id]!.childIds - .map((childId) => store.state.controls[childId]) - .whereNotNull() - .where((c) => c.isVisible) - .map((c) => LineChartDataPointViewModel.fromStore(store, c)) - .toList()); - } - - @override - List<Object?> get props => [control, dataPoints]; -} diff --git a/package/lib/src/models/linechart_event_data.dart b/package/lib/src/models/linechart_event_data.dart deleted file mode 100644 index f158ddbcf..000000000 --- a/package/lib/src/models/linechart_event_data.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:equatable/equatable.dart'; - -class LineChartEventData extends Equatable { - final String eventType; - final List<LineChartEventDataSpot> barSpots; - - const LineChartEventData({required this.eventType, required this.barSpots}); - - Map<String, dynamic> toJson() => <String, dynamic>{ - 'type': eventType, - 'spots': barSpots, - }; - - @override - List<Object?> get props => [eventType, barSpots]; -} - -class LineChartEventDataSpot extends Equatable { - final int barIndex; - final int spotIndex; - - const LineChartEventDataSpot( - {required this.barIndex, required this.spotIndex}); - - Map<String, dynamic> toJson() => <String, dynamic>{ - 'bar_index': barIndex, - 'spot_index': spotIndex, - }; - - @override - List<Object?> get props => [barIndex, spotIndex]; -} diff --git a/package/lib/src/models/linechart_view_model.dart b/package/lib/src/models/linechart_view_model.dart deleted file mode 100644 index c4d2d3546..000000000 --- a/package/lib/src/models/linechart_view_model.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:redux/redux.dart'; - -import 'app_state.dart'; -import 'control.dart'; -import 'chart_axis_view_model.dart'; -import 'linechart_data_view_model.dart'; - -class LineChartViewModel extends Equatable { - final Control control; - final ChartAxisViewModel? leftAxis; - final ChartAxisViewModel? topAxis; - final ChartAxisViewModel? rightAxis; - final ChartAxisViewModel? bottomAxis; - final List<LineChartDataViewModel> dataSeries; - final dynamic dispatch; - - const LineChartViewModel( - {required this.control, - required this.leftAxis, - required this.topAxis, - required this.rightAxis, - required this.bottomAxis, - required this.dataSeries, - required this.dispatch}); - - static LineChartViewModel fromStore( - Store<AppState> store, Control control, List<Control> children) { - var leftAxisCtrls = - children.where((c) => c.type == "axis" && c.name == "l" && c.isVisible); - var topAxisCtrls = - children.where((c) => c.type == "axis" && c.name == "t" && c.isVisible); - var rightAxisCtrls = - children.where((c) => c.type == "axis" && c.name == "r" && c.isVisible); - var bottomAxisCtrls = - children.where((c) => c.type == "axis" && c.name == "b" && c.isVisible); - return LineChartViewModel( - control: control, - leftAxis: leftAxisCtrls.isNotEmpty - ? ChartAxisViewModel.fromStore(store, leftAxisCtrls.first) - : null, - topAxis: topAxisCtrls.isNotEmpty - ? ChartAxisViewModel.fromStore(store, topAxisCtrls.first) - : null, - rightAxis: rightAxisCtrls.isNotEmpty - ? ChartAxisViewModel.fromStore(store, rightAxisCtrls.first) - : null, - bottomAxis: bottomAxisCtrls.isNotEmpty - ? ChartAxisViewModel.fromStore(store, bottomAxisCtrls.first) - : null, - dataSeries: children - .where((c) => c.type == "data" && c.isVisible) - .map((c) => LineChartDataViewModel.fromStore(store, c)) - .toList(), - dispatch: store.dispatch); - } - - @override - List<Object?> get props => - [control, leftAxis, rightAxis, topAxis, bottomAxis, dataSeries, dispatch]; -} diff --git a/package/lib/src/models/page_view_model.dart b/package/lib/src/models/page_view_model.dart deleted file mode 100644 index c654001a0..000000000 --- a/package/lib/src/models/page_view_model.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:redux/redux.dart'; - -import 'app_state.dart'; -import 'control.dart'; - -class PageViewModel extends Equatable { - final bool isLoading; - final String error; - final Control? page; - - const PageViewModel( - {required this.isLoading, required this.error, this.page}); - - static PageViewModel fromStore(Store<AppState> store) { - return PageViewModel( - isLoading: store.state.isLoading, - error: store.state.error, - page: store.state.controls["page"]); - } - - @override - List<Object?> get props => [isLoading, error, page]; -} diff --git a/package/lib/src/models/piechart_event_data.dart b/package/lib/src/models/piechart_event_data.dart deleted file mode 100644 index e2498ad92..000000000 --- a/package/lib/src/models/piechart_event_data.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:equatable/equatable.dart'; - -class PieChartEventData extends Equatable { - final String eventType; - final int? sectionIndex; - // final double? angle; - // final double? radius; - - const PieChartEventData( - {required this.eventType, required this.sectionIndex}); - - Map<String, dynamic> toJson() => - <String, dynamic>{'type': eventType, 'section_index': sectionIndex}; - - @override - List<Object?> get props => [eventType, sectionIndex]; -} diff --git a/package/lib/src/models/piechart_section_view_model.dart b/package/lib/src/models/piechart_section_view_model.dart deleted file mode 100644 index d6b40f041..000000000 --- a/package/lib/src/models/piechart_section_view_model.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:equatable/equatable.dart'; -import 'package:redux/redux.dart'; - -import 'app_state.dart'; -import 'control.dart'; - -class PieChartSectionViewModel extends Equatable { - final Control control; - final Control? badge; - - const PieChartSectionViewModel({required this.control, required this.badge}); - - static PieChartSectionViewModel fromStore( - Store<AppState> store, Control control) { - var children = store.state.controls[control.id]!.childIds - .map((childId) => store.state.controls[childId]) - .whereNotNull() - .where((c) => c.isVisible); - - return PieChartSectionViewModel( - control: control, - badge: children.firstWhereOrNull((c) => c.name == "badge")); - } - - @override - List<Object?> get props => [control, badge]; -} diff --git a/package/lib/src/models/piechart_view_model.dart b/package/lib/src/models/piechart_view_model.dart deleted file mode 100644 index 960a6fb5d..000000000 --- a/package/lib/src/models/piechart_view_model.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:redux/redux.dart'; - -import '../models/piechart_section_view_model.dart'; -import 'app_state.dart'; -import 'control.dart'; - -class PieChartViewModel extends Equatable { - final Control control; - final List<PieChartSectionViewModel> sections; - final dynamic dispatch; - - const PieChartViewModel( - {required this.control, required this.sections, required this.dispatch}); - - static PieChartViewModel fromStore( - Store<AppState> store, Control control, List<Control> children) { - return PieChartViewModel( - control: control, - sections: children - .where((c) => c.type == "section" && c.isVisible) - .map((c) => PieChartSectionViewModel.fromStore(store, c)) - .toList(), - dispatch: store.dispatch); - } - - @override - List<Object?> get props => [control, sections, dispatch]; -} diff --git a/package/lib/src/models/routes_view_model.dart b/package/lib/src/models/routes_view_model.dart deleted file mode 100644 index d72e1045e..000000000 --- a/package/lib/src/models/routes_view_model.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:equatable/equatable.dart'; -import 'package:redux/redux.dart'; - -import 'app_state.dart'; -import 'control.dart'; - -class RoutesViewModel extends Equatable { - final Control page; - final bool isLoading; - final String error; - final List<Control> offstageControls; - final List<Control> views; - - const RoutesViewModel( - {required this.page, - required this.isLoading, - required this.error, - required this.offstageControls, - required this.views}); - - static RoutesViewModel fromStore(Store<AppState> store) { - Control? offstageControl = store.state.controls["page"]!.childIds - .map((childId) => store.state.controls[childId]!) - .firstWhereOrNull((c) => c.type == "offstage"); - - return RoutesViewModel( - page: store.state.controls["page"]!, - isLoading: store.state.isLoading, - error: store.state.error, - offstageControls: offstageControl != null - ? store.state.controls[offstageControl.id]!.childIds - .map((childId) => store.state.controls[childId]!) - .where((c) => c.isVisible) - .toList() - : [], - views: store.state.controls["page"]!.childIds - .map((childId) => store.state.controls[childId]!) - .where((c) => c.type != "offstage" && c.isVisible) - .toList()); - } - - @override - List<Object?> get props => [page, isLoading, error, offstageControls, views]; -} diff --git a/package/lib/src/protocol/container_tap_event.dart b/package/lib/src/protocol/container_tap_event.dart deleted file mode 100644 index 3ca0d82b7..000000000 --- a/package/lib/src/protocol/container_tap_event.dart +++ /dev/null @@ -1,19 +0,0 @@ -class ContainerTapEvent { - final double localX; - final double localY; - final double globalX; - final double globalY; - - ContainerTapEvent( - {required this.localX, - required this.localY, - required this.globalX, - required this.globalY}); - - Map<String, dynamic> toJson() => <String, dynamic>{ - 'lx': localX, - 'ly': localY, - 'gx': globalX, - 'gy': globalY - }; -} diff --git a/package/lib/src/protocol/drag_target_accept_event.dart b/package/lib/src/protocol/drag_target_accept_event.dart deleted file mode 100644 index 0153520d9..000000000 --- a/package/lib/src/protocol/drag_target_accept_event.dart +++ /dev/null @@ -1,17 +0,0 @@ -class DragTargetAcceptEvent { - final String srcId; - final double x; - final double y; - - DragTargetAcceptEvent({ - required this.srcId, - required this.x, - required this.y, - }); - - Map<String, dynamic> toJson() => <String, dynamic>{ - 'src_id': srcId, - 'x': x, - 'y': y, - }; -} diff --git a/package/lib/src/protocol/file_picker_result_event.dart b/package/lib/src/protocol/file_picker_result_event.dart deleted file mode 100644 index a078dcb80..000000000 --- a/package/lib/src/protocol/file_picker_result_event.dart +++ /dev/null @@ -1,22 +0,0 @@ -class FilePickerResultEvent { - final String? path; - final List<FilePickerFile>? files; - - FilePickerResultEvent({required this.path, required this.files}); - - Map<String, dynamic> toJson() => <String, dynamic>{ - 'path': path, - 'files': files?.map((f) => f.toJson()).toList() - }; -} - -class FilePickerFile { - final String name; - final String? path; - final int size; - - FilePickerFile({required this.name, required this.path, required this.size}); - - Map<String, dynamic> toJson() => - <String, dynamic>{'name': name, 'path': path, 'size': size}; -} diff --git a/package/lib/src/protocol/file_picker_upload_file.dart b/package/lib/src/protocol/file_picker_upload_file.dart deleted file mode 100644 index f64fa23b4..000000000 --- a/package/lib/src/protocol/file_picker_upload_file.dart +++ /dev/null @@ -1,8 +0,0 @@ -class FilePickerUploadFile { - final String name; - final String uploadUrl; - final String method; - - FilePickerUploadFile( - {required this.name, required this.uploadUrl, required this.method}); -} diff --git a/package/lib/src/protocol/file_picker_upload_progress_event.dart b/package/lib/src/protocol/file_picker_upload_progress_event.dart deleted file mode 100644 index 7c02a4c4e..000000000 --- a/package/lib/src/protocol/file_picker_upload_progress_event.dart +++ /dev/null @@ -1,14 +0,0 @@ -class FilePickerUploadProgressEvent { - final String name; - final double? progress; - final String? error; - - FilePickerUploadProgressEvent( - {required this.name, required this.progress, required this.error}); - - Map<String, dynamic> toJson() => <String, dynamic>{ - 'file_name': name, - 'progress': progress, - 'error': error - }; -} diff --git a/package/lib/src/protocol/keyboard_event.dart b/package/lib/src/protocol/keyboard_event.dart deleted file mode 100644 index 7c25e614f..000000000 --- a/package/lib/src/protocol/keyboard_event.dart +++ /dev/null @@ -1,22 +0,0 @@ -class KeyboardEvent { - final String key; - final bool isShiftPressed; - final bool isControlPressed; - final bool isAltPressed; - final bool isMetaPressed; - - KeyboardEvent( - {required this.key, - required this.isShiftPressed, - required this.isControlPressed, - required this.isAltPressed, - required this.isMetaPressed}); - - Map<String, dynamic> toJson() => <String, dynamic>{ - 'key': key, - 'shift': isShiftPressed, - 'ctrl': isControlPressed, - 'alt': isAltPressed, - 'meta': isMetaPressed - }; -} diff --git a/package/lib/src/reducers.dart b/package/lib/src/reducers.dart index a833ab154..450363b2f 100644 --- a/package/lib/src/reducers.dart +++ b/package/lib/src/reducers.dart @@ -1,10 +1,10 @@ import 'dart:convert'; -import 'package:flet/src/flet_server.dart'; import 'package:flutter/foundation.dart'; import 'package:url_launcher/url_launcher.dart'; import 'actions.dart'; +import 'flet_server.dart'; import 'models/app_state.dart'; import 'models/control.dart'; import 'models/window_media_data.dart'; diff --git a/package/lib/src/utils/buttons.dart b/package/lib/src/utils/buttons.dart index c54a70e4d..57845f8bc 100644 --- a/package/lib/src/utils/buttons.dart +++ b/package/lib/src/utils/buttons.dart @@ -39,18 +39,6 @@ ButtonStyle? parseButtonStyle(ThemeData theme, Control control, String propName, defaultShape); } -MaterialStateProperty<Color?>? parseMaterialStateColor( - ThemeData theme, Control control, String propName) { - var v = control.attrString(propName, null); - if (v == null) { - return null; - } - - final j1 = json.decode(v); - return getMaterialStateProperty<Color?>( - j1, (jv) => HexColor.fromString(theme, jv as String), null); -} - ButtonStyle? buttonStyleFromJSON( ThemeData theme, Map<String, dynamic> json, diff --git a/package/lib/src/utils/colors.dart b/package/lib/src/utils/colors.dart index bd98b6134..158319b93 100644 --- a/package/lib/src/utils/colors.dart +++ b/package/lib/src/utils/colors.dart @@ -1,6 +1,10 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; +import '../models/control.dart'; import 'cupertino_colors.dart'; +import 'material_state.dart'; import 'numbers.dart'; Color? _getThemeColor(ThemeData theme, String colorName) { @@ -231,3 +235,15 @@ extension ColorExtension on Color { ); } } + +MaterialStateProperty<Color?>? parseMaterialStateColor( + ThemeData theme, Control control, String propName) { + var v = control.attrString(propName, null); + if (v == null) { + return null; + } + + final j1 = json.decode(v); + return getMaterialStateProperty<Color?>( + j1, (jv) => HexColor.fromString(theme, jv as String), null); +} diff --git a/package/lib/src/utils/dismissible.dart b/package/lib/src/utils/dismissible.dart index 9e504ef97..94d0168ec 100644 --- a/package/lib/src/utils/dismissible.dart +++ b/package/lib/src/utils/dismissible.dart @@ -1,9 +1,9 @@ import 'dart:convert'; -import 'package:flet/src/utils/numbers.dart'; import 'package:flutter/material.dart'; import '../models/control.dart'; +import '../utils/numbers.dart'; Map<DismissDirection, double>? parseDismissThresholds( Control control, String propName) { diff --git a/package/lib/src/utils/images.dart b/package/lib/src/utils/images.dart index f2b274d45..f545dad0d 100644 --- a/package/lib/src/utils/images.dart +++ b/package/lib/src/utils/images.dart @@ -2,11 +2,11 @@ import 'dart:convert'; import 'dart:ui'; import 'package:collection/collection.dart'; -import 'package:flet/src/utils/numbers.dart'; import 'package:flutter/material.dart'; import '../models/control.dart'; import 'gradient.dart'; +import 'numbers.dart'; export 'images_io.dart' if (dart.library.js) "images_web.dart"; diff --git a/package/lib/src/utils/menu.dart b/package/lib/src/utils/menu.dart index 8d9478bf3..0bd4a29f1 100644 --- a/package/lib/src/utils/menu.dart +++ b/package/lib/src/utils/menu.dart @@ -41,18 +41,6 @@ MenuStyle? parseMenuStyle(ThemeData theme, Control control, String propName, defaultShape); } -MaterialStateProperty<Color?>? parseMaterialStateColor( - ThemeData theme, Control control, String propName) { - var v = control.attrString(propName, null); - if (v == null) { - return null; - } - - final j1 = json.decode(v); - return getMaterialStateProperty<Color?>( - j1, (jv) => HexColor.fromString(theme, jv as String), null); -} - MenuStyle? menuStyleFromJSON( ThemeData theme, Map<String, dynamic> json, diff --git a/package/lib/src/utils/text.dart b/package/lib/src/utils/text.dart index 9b8ac878a..22b2b14cb 100644 --- a/package/lib/src/utils/text.dart +++ b/package/lib/src/utils/text.dart @@ -5,7 +5,6 @@ import 'package:collection/collection.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import '../flet_server.dart'; import '../models/control.dart'; import '../models/control_tree_view_model.dart'; import '../utils/drawing.dart'; @@ -79,16 +78,22 @@ FontWeight? getFontWeight(String weightName) { return null; } -List<InlineSpan> parseTextSpans(ThemeData theme, ControlTreeViewModel viewModel, - bool parentDisabled, FletServer? server) { +List<InlineSpan> parseTextSpans( + ThemeData theme, + ControlTreeViewModel viewModel, + bool parentDisabled, + void Function(String, String, String)? sendControlEvent) { return viewModel.children - .map((c) => parseInlineSpan(theme, c, parentDisabled, server)) + .map((c) => parseInlineSpan(theme, c, parentDisabled, sendControlEvent)) .whereNotNull() .toList(); } -InlineSpan? parseInlineSpan(ThemeData theme, ControlTreeViewModel spanViewModel, - bool parentDisabled, FletServer? server) { +InlineSpan? parseInlineSpan( + ThemeData theme, + ControlTreeViewModel spanViewModel, + bool parentDisabled, + void Function(String, String, String)? sendControlEvent) { if (spanViewModel.control.type == "textspan") { bool disabled = spanViewModel.control.isDisabled || parentDisabled; var onClick = spanViewModel.control.attrBool("onClick", false)!; @@ -97,45 +102,38 @@ InlineSpan? parseInlineSpan(ThemeData theme, ControlTreeViewModel spanViewModel, return TextSpan( text: spanViewModel.control.attrString("text"), style: parseTextStyle(theme, spanViewModel.control, "style"), - children: parseTextSpans(theme, spanViewModel, parentDisabled, server), - mouseCursor: onClick && !disabled && server != null + children: parseTextSpans( + theme, spanViewModel, parentDisabled, sendControlEvent), + mouseCursor: onClick && !disabled && sendControlEvent != null ? SystemMouseCursors.click : null, - recognizer: (onClick || url != "") && !disabled && server != null - ? (TapGestureRecognizer() - ..onTap = () { - debugPrint("TextSpan ${spanViewModel.control.id} clicked!"); - if (url != "") { - openWebBrowser(url, webWindowName: urlTarget); - } - if (onClick) { - server.sendPageEvent( - eventTarget: spanViewModel.control.id, - eventName: "click", - eventData: ""); - } - }) - : null, + recognizer: + (onClick || url != "") && !disabled && sendControlEvent != null + ? (TapGestureRecognizer() + ..onTap = () { + debugPrint("TextSpan ${spanViewModel.control.id} clicked!"); + if (url != "") { + openWebBrowser(url, webWindowName: urlTarget); + } + if (onClick) { + sendControlEvent(spanViewModel.control.id, "click", ""); + } + }) + : null, onEnter: spanViewModel.control.attrBool("onEnter", false)! && !disabled && - server != null + sendControlEvent != null ? (event) { debugPrint("TextSpan ${spanViewModel.control.id} entered!"); - server.sendPageEvent( - eventTarget: spanViewModel.control.id, - eventName: "enter", - eventData: ""); + sendControlEvent(spanViewModel.control.id, "enter", ""); } : null, onExit: spanViewModel.control.attrBool("onExit", false)! && !disabled && - server != null + sendControlEvent != null ? (event) { debugPrint("TextSpan ${spanViewModel.control.id} exited!"); - server.sendPageEvent( - eventTarget: spanViewModel.control.id, - eventName: "exit", - eventData: ""); + sendControlEvent(spanViewModel.control.id, "exit", ""); } : null, ); diff --git a/package/lib/src/utils/textfield.dart b/package/lib/src/utils/textfield.dart index fb35d25e2..9e7e003d2 100644 --- a/package/lib/src/utils/textfield.dart +++ b/package/lib/src/utils/textfield.dart @@ -1,10 +1,10 @@ import 'dart:convert'; -import 'package:flet/src/utils/numbers.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../models/control.dart'; +import '../utils/numbers.dart'; FilteringTextInputFormatter? parseInputFilter( Control control, String propName) { diff --git a/package/lib/src/utils/theme.dart b/package/lib/src/utils/theme.dart index 927600682..689cf1b7e 100644 --- a/package/lib/src/utils/theme.dart +++ b/package/lib/src/utils/theme.dart @@ -1,14 +1,14 @@ import 'dart:convert'; -import 'package:flet/src/utils/borders.dart'; -import 'package:flet/src/utils/edge_insets.dart'; -import 'package:flet/src/utils/numbers.dart'; -import 'package:flet/src/utils/text.dart'; import 'package:flutter/material.dart'; import '../models/control.dart'; +import 'borders.dart'; import 'colors.dart'; +import 'edge_insets.dart'; import 'material_state.dart'; +import 'numbers.dart'; +import 'text.dart'; ThemeData parseTheme(Control control, String propName, Brightness? brightness, {ThemeData? parentTheme}) { @@ -47,7 +47,6 @@ ThemeData themeFromJson(Map<String, dynamic>? json, Brightness? brightness, useMaterial3: json?["use_material3"] ?? primarySwatch == null); return theme.copyWith( - useMaterial3: json?["use_material3"] ?? theme.useMaterial3, visualDensity: json?["visual_density"] != null ? parseVisualDensity(json?["visual_density"]) : theme.visualDensity, diff --git a/package/lib/src/widgets/window_media.dart b/package/lib/src/widgets/window_media.dart index e00cd077b..750b7ceec 100644 --- a/package/lib/src/widgets/window_media.dart +++ b/package/lib/src/widgets/window_media.dart @@ -1,14 +1,13 @@ import 'package:flutter/widgets.dart'; -import 'package:flutter_redux/flutter_redux.dart'; import 'package:window_manager/window_manager.dart'; import '../actions.dart'; import '../flet_app_services.dart'; -import '../models/app_state.dart'; import '../utils/desktop.dart'; class WindowMedia extends StatefulWidget { - const WindowMedia({super.key}); + final dynamic dispatch; + const WindowMedia({super.key, required this.dispatch}); @override // ignore: library_private_types_in_public_api @@ -16,8 +15,6 @@ class WindowMedia extends StatefulWidget { } class WindowMediaState extends State<WindowMedia> with WindowListener { - Function? _dispatch; - @override void initState() { super.initState(); @@ -32,13 +29,7 @@ class WindowMediaState extends State<WindowMedia> with WindowListener { @override Widget build(BuildContext context) { - return StoreConnector<AppState, Function>( - distinct: true, - converter: (store) => store.dispatch, - builder: (context, dispatch) { - _dispatch = dispatch; - return const SizedBox.shrink(); - }); + return const SizedBox.shrink(); } @override @@ -52,7 +43,7 @@ class WindowMediaState extends State<WindowMedia> with WindowListener { debugPrint('[WindowManager] onWindowEvent: $eventName'); getWindowMediaData().then((wmd) { debugPrint("WindowMediaData: $wmd"); - _dispatch!(WindowEventAction( + widget.dispatch!(WindowEventAction( eventName, wmd, FletAppServices.of(context).server)); }); } diff --git a/package/test/models/linechart_event_data_test.dart b/package/test/models/linechart_event_data_test.dart index 83f70fbc9..9cff525e2 100644 --- a/package/test/models/linechart_event_data_test.dart +++ b/package/test/models/linechart_event_data_test.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:flet/src/models/linechart_event_data.dart'; +import 'package:flet/src/controls/linechart.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { diff --git a/sdk/python/packages/flet-core/src/flet_core/__init__.py b/sdk/python/packages/flet-core/src/flet_core/__init__.py index c5b158cb1..d4132fdd8 100644 --- a/sdk/python/packages/flet-core/src/flet_core/__init__.py +++ b/sdk/python/packages/flet-core/src/flet_core/__init__.py @@ -234,7 +234,7 @@ MaterialState, NotchShape, PaddingValue, - PageDesignLanguage, + PagePlatform, ScrollMode, TabAlignment, TextAlign, diff --git a/sdk/python/packages/flet-core/src/flet_core/page.py b/sdk/python/packages/flet-core/src/flet_core/page.py index c856f6bbe..539127a4b 100644 --- a/sdk/python/packages/flet-core/src/flet_core/page.py +++ b/sdk/python/packages/flet-core/src/flet_core/page.py @@ -41,8 +41,7 @@ MainAxisAlignment, OffsetValue, PaddingValue, - PageDesignLanguage, - PageDesignString, + PagePlatform, ScrollMode, ThemeMode, ThemeModeString, @@ -1227,7 +1226,13 @@ def debug(self) -> bool: # platform @property def platform(self): - return self._get_attr("platform") + return PagePlatform(self._get_attr("platform")) + + @platform.setter + def platform(self, value: PagePlatform): + self._set_attr( + "platform", value.value if isinstance(value, PagePlatform) else value + ) # platform_brightness @property @@ -1246,22 +1251,6 @@ def client_ip(self): def client_user_agent(self): return self._get_attr("clientUserAgent") - # design - @property - def design(self) -> Optional[PageDesignLanguage]: - return self.__design - - @design.setter - def design(self, value: Optional[PageDesignLanguage]): - self.__design = value - if isinstance(value, PageDesignLanguage): - self._set_attr("design", value.value) - else: - self.__set_design(value) - - def __set_design(self, value: PageDesignString): - self._set_attr("design", value) - # fonts @property def fonts(self) -> Optional[Dict[str, str]]: diff --git a/sdk/python/packages/flet-core/src/flet_core/types.py b/sdk/python/packages/flet-core/src/flet_core/types.py index c07f9b4e9..451a70962 100644 --- a/sdk/python/packages/flet-core/src/flet_core/types.py +++ b/sdk/python/packages/flet-core/src/flet_core/types.py @@ -286,15 +286,12 @@ class ImageRepeat(Enum): REPEAT_Y = "repeatY" -PageDesignString = Literal[None, "material", "cupertino", "fluent", "macos", "adaptive"] - - -class PageDesignLanguage(Enum): - MATERIAL = "material" - CUPERTINO = "cupertino" - FLUENT = "fluent" +class PagePlatform(Enum): + IOS = "ios" + ANDROID = "android" MACOS = "macos" - ADAPTIVE = "adaptive" + WINDOWS = "windows" + LINUX = "linux" ThemeModeString = Literal[None, "system", "light", "dark"] diff --git a/sdk/python/packages/flet/src/flet/cli/commands/build.py b/sdk/python/packages/flet/src/flet/cli/commands/build.py index 06b059fe8..b8adc8225 100644 --- a/sdk/python/packages/flet/src/flet/cli/commands/build.py +++ b/sdk/python/packages/flet/src/flet/cli/commands/build.py @@ -9,7 +9,9 @@ import tempfile import urllib.request from pathlib import Path +from typing import Optional +import flet.version import yaml from flet.cli.commands.base import BaseCommand from flet_core.utils import random_string, slugify @@ -20,6 +22,7 @@ from ctypes import windll PYODIDE_ROOT_URL = "https://cdn.jsdelivr.net/pyodide/v0.24.1/full" +DEFAULT_TEMPLATE_URL = "gh:flet-dev/flet-build-template" class Command(BaseCommand): @@ -235,7 +238,6 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None: "--template", dest="template", type=str, - default="gh:flet-dev/flet-build-template", help="a directory containing Flutter bootstrap template, or a URL to a git repository template", ) parser.add_argument( @@ -254,10 +256,17 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None: def handle(self, options: argparse.Namespace) -> None: from cookiecutter.main import cookiecutter + self.verbose = options.verbose + self.flutter_dir = None + # get `flutter` and `dart` executables from PATH flutter_exe = self.find_flutter_batch("flutter") dart_exe = self.find_flutter_batch("dart") + if self.verbose > 1: + print("Flutter executable:", flutter_exe) + print("Dart executable:", dart_exe) + target_platform = options.target_platform.lower() # platform check current_platform = platform.system() @@ -266,25 +275,22 @@ def handle(self, options: argparse.Namespace) -> None: if current_platform == "Darwin": current_platform = "macOS" - print(f"Can't build {target_platform} on {current_platform}") - sys.exit(1) - - self.verbose = options.verbose + self.cleanup(1, f"Can't build {target_platform} on {current_platform}") python_app_path = Path(options.python_app_path).resolve() if not os.path.exists(python_app_path) or not os.path.isdir(python_app_path): - print( - f"Path to Flet app does not exist or is not a directory: {python_app_path}" + self.cleanup( + 1, + f"Path to Flet app does not exist or is not a directory: {python_app_path}", ) - sys.exit(1) python_module_name = Path(options.module_name).stem python_module_filename = f"{python_module_name}.py" if not os.path.exists(os.path.join(python_app_path, python_module_filename)): - print( - f"{python_module_filename} not found in the root of Flet app directory. Use --module-name option to specify an entry point for your Flet app." + self.cleanup( + 1, + f"{python_module_filename} not found in the root of Flet app directory. Use --module-name option to specify an entry point for your Flet app.", ) - sys.exit(1) self.flutter_dir = Path(tempfile.gettempdir()).joinpath( f"flet_flutter_build_{random_string(10)}" @@ -311,6 +317,7 @@ def handle(self, options: argparse.Namespace) -> None: project_name = slugify( options.project_name if options.project_name else python_app_path.name ).replace("-", "_") + template_data["project_name"] = project_name if options.description is not None: @@ -337,24 +344,65 @@ def handle(self, options: argparse.Namespace) -> None: "true" if options.use_color_emoji else "false" ) + src_pubspec = None + src_pubspec_path = python_app_path.joinpath("pubspec.yaml") + if src_pubspec_path.exists(): + with open(src_pubspec_path, encoding="utf8") as f: + src_pubspec = pubspec = yaml.safe_load(f) + + flutter_dependencies = [] + if src_pubspec and src_pubspec["dependencies"]: + for dep in src_pubspec["dependencies"].keys(): + flutter_dependencies.append(dep) + + template_data["flutter"] = {"dependencies": flutter_dependencies} + + template_url = options.template + template_ref = options.template_ref + if not template_url: + template_url = DEFAULT_TEMPLATE_URL + if flet.version.version and not template_ref: + template_ref = flet.version.version + # create Flutter project from a template print("Creating Flutter bootstrap project...", end="") - cookiecutter( - template=options.template, - checkout=options.template_ref, - directory=options.template_dir, - output_dir=str(self.flutter_dir.parent), - no_input=True, - overwrite_if_exists=True, - extra_context=template_data, - ) + try: + cookiecutter( + template=template_url, + checkout=template_ref, + directory=options.template_dir, + output_dir=str(self.flutter_dir.parent), + no_input=True, + overwrite_if_exists=True, + extra_context=template_data, + ) + except Exception as e: + self.cleanup(1, f"{e}") print("[spring_green3]OK[/spring_green3]") # load pubspec.yaml pubspec_path = str(self.flutter_dir.joinpath("pubspec.yaml")) - with open(pubspec_path) as f: + with open(pubspec_path, encoding="utf8") as f: pubspec = yaml.safe_load(f) + # merge dependencies to a dest pubspec.yaml + if src_pubspec and src_pubspec["dependencies"]: + for k, v in src_pubspec["dependencies"].items(): + pubspec["dependencies"][k] = "any" + + if src_pubspec and src_pubspec["dependency_overrides"]: + pubspec["dependency_overrides"] = {} + for k, v in src_pubspec["dependency_overrides"].items(): + pubspec["dependency_overrides"][k] = v + + # make sure project name is not named as any of dependencies + for dep in pubspec["dependencies"].keys(): + if dep == project_name: + self.cleanup( + 1, + f"Project name cannot have the same name as one of its dependencies: {dep}. Use --project option to specify a different project name.", + ) + # copy icons to `flutter_dir` print("Customizing app icons and splash images...", end="") assets_path = python_app_path.joinpath("assets") @@ -510,7 +558,7 @@ def fallback_image(yaml_path: str, images: list): print("[spring_green3]OK[/spring_green3]") # save pubspec.yaml - with open(pubspec_path, "w") as f: + with open(pubspec_path, "w", encoding="utf8") as f: yaml.dump(pubspec, f) # generate icons @@ -604,12 +652,11 @@ def fallback_image(yaml_path: str, images: list): # make sure app/app.zip exists app_zip_path = self.flutter_dir.joinpath("app", "app.zip") if not os.path.exists(app_zip_path): - print("Flet app package app/app.zip was not created.") - self.cleanup(1) + self.cleanup(1, "Flet app package app/app.zip was not created.") # create {flutter_dir}/app/app.hash app_hash_path = self.flutter_dir.joinpath("app", "app.zip.hash") - with open(app_hash_path, "w") as hf: + with open(app_hash_path, "w", encoding="utf8") as hf: hf.write(calculate_file_hash(app_zip_path)) print("[spring_green3]OK[/spring_green3]") @@ -638,8 +685,8 @@ def fallback_image(yaml_path: str, images: list): for flutter_build_arg in flutter_build_arg_arr: build_args.append(flutter_build_arg) - if self.verbose > 0: - print(build_args) + if self.verbose > 1: + build_args.append("--verbose") build_result = self.run(build_args, cwd=str(self.flutter_dir)) @@ -686,16 +733,13 @@ def ignore_build_output(path, files): print("[spring_green3]OK[/spring_green3]") - # print(self.flutter_dir) - # return - self.cleanup(0) def create_pyodide_find_links(self): with urllib.request.urlopen(f"{PYODIDE_ROOT_URL}/pyodide-lock.json") as j: data = json.load(j) find_links_path = str(self.flutter_dir.joinpath("find-links.html")) - with open(find_links_path, "w") as f: + with open(find_links_path, "w", encoding="utf8") as f: for package in data["packages"].values(): file_name = package["file_name"] f.write(f'<a href="{PYODIDE_ROOT_URL}/{file_name}">{file_name}</a>\n') @@ -713,10 +757,10 @@ def copy_icon_image(self, src_path: Path, dest_path: Path, image_name: str): def find_flutter_batch(self, exe_filename: str): batch_path = shutil.which(exe_filename) if not batch_path: - print( - f"`{exe_filename}` command is not available in PATH. Install Flutter SDK." + self.cleanup( + 1, + f"`{exe_filename}` command is not available in PATH. Install Flutter SDK.", ) - sys.exit(1) if is_windows() and batch_path.endswith(".file"): return batch_path.replace(".file", ".bat") return batch_path @@ -728,10 +772,13 @@ def run(self, args, cwd): previousCp = windll.kernel32.GetConsoleOutputCP() windll.kernel32.SetConsoleOutputCP(65001) + if self.verbose > 0: + print(f"\nRun subprocess: {args}") + r = subprocess.run( args, cwd=cwd, - capture_output=self.verbose < 2, + capture_output=self.verbose < 1, text=True, encoding="utf8", ) @@ -742,14 +789,19 @@ def run(self, args, cwd): return r - def cleanup(self, exit_code: int): - if self.verbose > 0: - print(f"Deleting Flutter bootstrap directory {self.flutter_dir}") - shutil.rmtree(str(self.flutter_dir), ignore_errors=False, onerror=None) + def cleanup(self, exit_code: int, message: Optional[str] = None): + if self.flutter_dir and os.path.exists(self.flutter_dir): + if self.verbose > 0: + print(f"Deleting Flutter bootstrap directory {self.flutter_dir}") + shutil.rmtree(str(self.flutter_dir), ignore_errors=True, onerror=None) if exit_code == 0: - print("[spring_green3]Success![/spring_green3]") + msg = message if message else "Success!" + print(f"[spring_green3]{msg}[/spring_green3]") else: - print( - "[red]Error building Flet app - see the log of failed command above.[/red]" + msg = ( + message + if message + else "Error building Flet app - see the log of failed command above." ) + print(f"[red]{msg}[/red]") sys.exit(exit_code)