diff --git a/.appveyor.yml b/.appveyor.yml index fc6648e536..a320c35e01 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -1,14 +1,9 @@ skip_branch_with_pr: true -skip_commits: - files: - - docs/**/* - - media/**/* - - '*.md' - environment: GO_VERSION: 1.18 GO_TAGS: --tags release + python_version: 3.10 GITHUB_TOKEN: secure: uX7wMPjOz72c6zs4QS2/m3vsOE5Fh7e68YqMNSf0mRmb6hP9Ij8haCefQjxBDlEb DOCKER_REGISTRY_USER: @@ -29,155 +24,268 @@ environment: # job_group: tests # APPVEYOR_BUILD_WORKER_IMAGE: macOS - - job_name: Release + - job_name: Build Server binaries + job_group: build_server job_depends_on: tests APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu + - job_name: Test Python 3.7 + job_group: python_tests + job_depends_on: build_server + python_version: 3.7 + APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu + + - job_name: Test Python 3.8 + job_group: python_tests + job_depends_on: build_server + python_version: 3.8 + APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu + + - job_name: Test Python 3.9 + job_group: python_tests + job_depends_on: build_server + python_version: 3.9 + APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu + + - job_name: Test Python 3.10 + job_group: python_tests + job_depends_on: build_server + python_version: 3.10 + APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu + + - job_name: Build Python wheels + job_group: python_build + job_depends_on: python_tests + APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu + TWINE_USERNAME: __token__ + TWINE_NON_INTERACTIVE: true + pypi_key: + secure: 174ncAbF5IjSIkmioPt62jeSnzmTlRNchUkE4QdjDWH8xK1olYtySXLJpo2q95HcP7lWJky1hv4APESiRRHnBWoY0XRFafzM/mbCDMzG1tZXiXZmpP1qzHAtRP2QSCIg18xh1TMktraUdTi7sbJnjjRhqzgbW1k0kLBxKw79MPFBhYQ/TiGcmaYWZbWVZNY3HCUCb6Dt7bG1OE2Ul9rD1gvs55xwO9Oq9FOVA1VnMYw= + test_pypi_key: + secure: cMCzqE9PcLcAiQ7POU0eVmLsXpy/n3WA9USIQNDKKbvUeajtURVITKpQ4MmwUXZAGv8giPPDUppiIf22AwIfx3O43tBVCp/HjvKNCbgY8sTaQBx60mLRbVBfD1F/+VfeuSTm57qtuSxUkZWF1JlWp8UQqIwCMHHDd0/wqDfmPNKj6U617Lp3vIfhsfgaDofspCKSGfG8+Z+6gcpmI+mA1wFHQB+l/BAbsGbgih8HiH6EzcuyIphxQKEA6r2XDPWE + matrix: fast_finish: true - + +stack: python $python_version + for: - -# ====================================== -# Windows -# ====================================== + # ====================================== + # Windows + # ====================================== -- - matrix: - only: - - job_name: Windows - - install: - - ps: .\install_go.ps1 - - set GOPATH=%USERPROFILE%\go - - set PATH=%GOPATH%\bin;%PATH% - - echo %GOPATH% - - echo %GOROOT% - - go version - - build_script: - - ps: Install-Product node 12 x64 - - cd client - - yarn - - yarn build - - cd .. - - mkdir internal\server\content - - xcopy client\build internal\server\content\ /E /Y - - .\build.cmd - - dir %USERPROFILE%\Go\bin - - test_script: - - run-tests.cmd - -# ====================================== -# Linux -# ====================================== - -- - matrix: - only: - - job_name: Linux - - install: - - gvm install go${GO_VERSION} -B - - gvm use go${GO_VERSION} - - go version - - build_script: - - nvm use 12 - - cd client - - yarn - - yarn build - - cd .. - - mkdir server/content - - cp -r client/build/* server/content - - ./build.sh - - ls $GOPATH/bin - - test_script: - - ./run-tests.sh - -# ====================================== -# macOS -# ====================================== - -- - matrix: - only: - - job_name: macOS - - install: - - gvm install go${GO_VERSION} -B - - gvm use go${GO_VERSION} - - go version - - HOMEBREW_NO_AUTO_UPDATE=1 brew install yarn - - build_script: - - nvm use 14 - - cd client - - yarn - - yarn build - - cd .. - - mkdir server/content - - cp -r client/build/* server/content - - ./build.sh - - ls $GOPATH/bin - - test_script: - - ./run-tests.sh - -# ====================================== -# Release -# ====================================== - -- - matrix: - only: - - job_name: Release - - install: - # Flutter SDK - - sudo snap install flutter --classic - - flutter sdk-path - - flutter --version - - # Go and GoReleaser - - gvm install go${GO_VERSION} -B - - gvm use go${GO_VERSION} - - go version - - bash ./ci/install_goreleaser.sh - - goreleaser --version - - build_script: - - cd client - - flutter build web --release --web-renderer auto - - rm -rf build/web/canvaskit - - ls -alR build/web - - - cd .. - - mkdir server/server/content - - cp -r client/build/web/* server/server/content - - cd server - - sh: | - if [[ "$APPVEYOR_REPO_TAG" == "true" ]]; then - goreleaser - else - goreleaser --snapshot --skip-publish - fi - - cd .. - - test_script: - - docker images - - docker run --name flet-test -d flet/server - - sleep 10 - - docker logs flet-test - - # publish to docker.io on tagged builds only - - sh: | - if [[ "$APPVEYOR_REPO_TAG" == "true" ]]; then - echo "$DOCKER_REGISTRY_PASS" | docker login --username $DOCKER_REGISTRY_USER --password-stdin - docker image push --all-tags flet/server - fi - - artifacts: - - path: server/dist/flet-* + - matrix: + only: + - job_name: Windows + + install: + - ps: .\install_go.ps1 + - set GOPATH=%USERPROFILE%\go + - set PATH=%GOPATH%\bin;%PATH% + - echo %GOPATH% + - echo %GOROOT% + - go version + + build_script: + - ps: Install-Product node 12 x64 + - cd client + - yarn + - yarn build + - cd .. + - mkdir internal\server\content + - xcopy client\build internal\server\content\ /E /Y + - .\build.cmd + - dir %USERPROFILE%\Go\bin + + test_script: + - run-tests.cmd + + # ====================================== + # Linux + # ====================================== + + - matrix: + only: + - job_name: Linux + + install: + - gvm install go${GO_VERSION} -B + - gvm use go${GO_VERSION} + - go version + + build_script: + - nvm use 12 + - cd client + - yarn + - yarn build + - cd .. + - mkdir server/content + - cp -r client/build/* server/content + - ./build.sh + - ls $GOPATH/bin + + test_script: + - ./run-tests.sh + + # ====================================== + # macOS + # ====================================== + + - matrix: + only: + - job_name: macOS + + install: + - gvm install go${GO_VERSION} -B + - gvm use go${GO_VERSION} + - go version + - HOMEBREW_NO_AUTO_UPDATE=1 brew install yarn + + build_script: + - nvm use 14 + - cd client + - yarn + - yarn build + - cd .. + - mkdir server/content + - cp -r client/build/* server/content + - ./build.sh + - ls $GOPATH/bin + + test_script: + - ./run-tests.sh + + # ====================================== + # Build Server binaries + # ====================================== + + - matrix: + only: + - job_group: build_server + + install: + # Flutter SDK + - sudo snap install flutter --classic + - flutter sdk-path + - flutter --version + + # Go and GoReleaser + - gvm install go${GO_VERSION} -B + - gvm use go${GO_VERSION} + - go version + - bash ./ci/install_goreleaser.sh + - goreleaser --version + + build_script: + # Flutter Web client + - cd client + - flutter test + - flutter build web --release --web-renderer auto + - rm -rf build/web/canvaskit + - ls -alR build/web + + # Flet Server in Go + - cd .. + - mkdir server/server/content + - cp -r client/build/web/* server/server/content + - cd server + - sh: | + if [[ "$APPVEYOR_REPO_TAG" == "true" ]]; then + goreleaser + else + goreleaser --snapshot --skip-publish + fi + - cd .. + + test_script: + - docker images + - docker run --name flet-test -d flet/server + - sleep 10 + - docker logs flet-test + + # publish to docker.io on tagged builds only + - sh: | + if [[ "$APPVEYOR_REPO_TAG" == "true" ]]; then + echo "$DOCKER_REGISTRY_PASS" | docker login --username $DOCKER_REGISTRY_USER --password-stdin + docker image push --all-tags flet/server + fi + + artifacts: + - path: server/dist/flet-* + - path: server/dist/flet_*/* + + ###################### + # Python Tests # + ###################### + - matrix: + only: + - job_group: python_tests + + install: + - python --version + - cd sdk/python + - pip install pdm + - pdm install + + build: off + + test_script: + - pdm run pytest tests + + ###################### + # Python Build # + ###################### + - matrix: + only: + - job_group: python_build + + install: + - python --version + - cd sdk/python + - pip install --upgrade setuptools wheel twine pdm + - pdm install + + test: off + + build_script: + - ps: | + $ErrorActionPreference = "Stop" + + if ($env:APPVEYOR_REPO_TAG -eq 'true') { + # release mode + + # version + $ver = $env:APPVEYOR_REPO_TAG_NAME + if ($ver.StartsWith('v')) { $ver = $ver.Substring(1) } + + # prerelease moniker + $idx = $ver.indexOf('-') + if ($idx -ne -1) { + $prerelease = $ver.Substring($idx + 1) + $ver = $ver.Substring(0, $idx) + } + $env:TWINE_PASSWORD = $env:pypi_key + } else { + + # build mode + $ver = $env:APPVEYOR_BUILD_VERSION + $env:TWINE_PASSWORD = $env:test_pypi_key + $env:TWINE_REPOSITORY = 'testpypi' + } + + # patch version + $env:PACKAGE_VERSION = $ver + (Get-Content pyproject.toml).replace("version = `"0.1.0`"", "version = `"$ver`"") | Set-Content pyproject.toml + + # build package + - pdm build + - python3 build-wheels.py + + # publish package + - sh: | + if [[ "$APPVEYOR_PULL_REQUEST_NUMBER" == "" ]]; then + twine upload dist/* + fi + + artifacts: + path: sdk/python/dist/* diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..6e5e6e21db --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +# Change Log - Flet client for Python + +## [0.1.0](https://pypi.org/project/flet/0.1.0) - Mar 30, 2022 diff --git a/README.md b/README.md index de8dc1f25d..70c7121276 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![Build status](https://ci.appveyor.com/api/projects/status/xwablctxslvey576/branch/main?svg=true)](https://ci.appveyor.com/project/flet-dev/flet/branch/main) + # Flet Flet is a framework that enables you to easily build realtime web, mobile and desktop apps in your favorite language and securely share them with your team. No frontend experience required. diff --git a/client/lib/actions.dart b/client/lib/actions.dart index 687e6dca3c..a7f1072c6e 100644 --- a/client/lib/actions.dart +++ b/client/lib/actions.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'protocol/append_control_props_request.dart'; import 'protocol/clean_control_payload.dart'; import 'protocol/page_controls_batch_payload.dart'; @@ -10,6 +12,11 @@ import 'protocol/register_webclient_response.dart'; import 'protocol/session_crashed_payload.dart'; import 'protocol/signout_payload.dart'; +class PageSizeChangeAction { + final Size newSize; + PageSizeChangeAction(this.newSize); +} + class RegisterWebClientAction { final RegisterWebClientResponse payload; RegisterWebClientAction(this.payload); diff --git a/client/lib/controls/button.dart b/client/lib/controls/button.dart index 5619fc3382..4f13ee500b 100644 --- a/client/lib/controls/button.dart +++ b/client/lib/controls/button.dart @@ -5,21 +5,25 @@ import '../web_socket_client.dart'; class ButtonControl extends StatelessWidget { final Control control; + final bool disabled; - const ButtonControl({Key? key, required this.control}) : super(key: key); + const ButtonControl({Key? key, required this.control, required this.disabled}) + : super(key: key); @override Widget build(BuildContext context) { debugPrint("Button build: ${control.id}"); - return ElevatedButton( - onPressed: () { - debugPrint("Button ${control.id} clicked!"); - ws.pageEventFromWeb( - eventTarget: control.id, - eventName: "click", - eventData: control.attrs["data"] ?? ""); - }, - child: Text(control.attrs["text"] ?? ""), + return SizedBox( + child: ElevatedButton( + onPressed: () { + debugPrint("Button ${control.id} clicked!"); + ws.pageEventFromWeb( + eventTarget: control.id, + eventName: "click", + eventData: control.attrs["data"] ?? ""); + }, + child: Text(control.attrs["text"] ?? ""), + ), ); } } diff --git a/client/lib/controls/create_control.dart b/client/lib/controls/create_control.dart index b35e12932c..1cf23ceee4 100644 --- a/client/lib/controls/create_control.dart +++ b/client/lib/controls/create_control.dart @@ -1,11 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_redux/flutter_redux.dart'; -import 'package:flet_view/controls/dropdown.dart'; -import 'package:flet_view/models/control_view_model.dart'; + +import '../models/control_view_model.dart'; import '../models/app_state.dart'; +import 'expanded.dart'; +import 'row.dart'; import 'textfield.dart'; - +import 'dropdown.dart'; import 'button.dart'; import 'page.dart'; import 'stack.dart'; @@ -31,10 +33,16 @@ Widget createControl(String id) { case "text": return TextControl(control: controlView.control); case "button": - return ButtonControl(control: controlView.control); + return ButtonControl(control: controlView.control, disabled: true); + case "expanded": + return ExpandedControl( + control: controlView.control, children: controlView.children); case "column": return ColumnControl( control: controlView.control, children: controlView.children); + case "row": + return RowControl( + control: controlView.control, children: controlView.children); case "stack": return StackControl( control: controlView.control, children: controlView.children); diff --git a/client/lib/controls/error.dart b/client/lib/controls/error.dart new file mode 100644 index 0000000000..0a19832e28 --- /dev/null +++ b/client/lib/controls/error.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +class ErrorControl extends StatelessWidget { + final String message; + + const ErrorControl(this.message, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + debugPrint("Error build"); + return Container( + padding: const EdgeInsets.all(5), + decoration: BoxDecoration( + color: Colors.red, borderRadius: BorderRadius.circular(3)), + child: Text(message, style: const TextStyle(color: Colors.white)), + ); + } +} diff --git a/client/lib/controls/expanded.dart b/client/lib/controls/expanded.dart new file mode 100644 index 0000000000..1adb35f518 --- /dev/null +++ b/client/lib/controls/expanded.dart @@ -0,0 +1,24 @@ +import 'package:flet_view/controls/error.dart'; +import 'package:flutter/widgets.dart'; +import '../models/control.dart'; +import 'create_control.dart'; + +class ExpandedControl extends StatelessWidget { + final Control control; + final List children; + + const ExpandedControl( + {Key? key, required this.control, required this.children}) + : super(key: key); + + @override + Widget build(BuildContext context) { + debugPrint("Expanded build: ${control.id}"); + if (control.childIds.isEmpty) { + return ErrorControl( + 'Error drawing Expanded with ID ${control.id}: it doesn\'t contain child control.'); + } else { + return Expanded(child: createControl(control.childIds.first)); + } + } +} diff --git a/client/lib/controls/page.dart b/client/lib/controls/page.dart index 8bc9acfc93..031497bf10 100644 --- a/client/lib/controls/page.dart +++ b/client/lib/controls/page.dart @@ -1,5 +1,5 @@ +import '../widgets/screen_size.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'create_control.dart'; import '../models/control.dart'; @@ -14,14 +14,12 @@ class PageControl extends StatelessWidget { Widget build(BuildContext context) { debugPrint("Page build: ${control.id}"); - // MediaQueryData media = MediaQuery.of(context); - // debugPrint("Screen size: ${media.size}"); return MaterialApp( home: Scaffold( body: Column( - children: control.childIds - .map((childId) => createControl(childId)) - .toList(), + children: + control.childIds.map((childId) => createControl(childId)).toList() + ..add(const ScreenSize()), ), ), ); diff --git a/client/lib/controls/row.dart b/client/lib/controls/row.dart new file mode 100644 index 0000000000..0f3f06fb25 --- /dev/null +++ b/client/lib/controls/row.dart @@ -0,0 +1,38 @@ +import '../models/page_breakpoint_view_model.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import '../models/app_state.dart'; +import '../models/control.dart'; +import 'create_control.dart'; + +class RowControl extends StatelessWidget { + final Control control; + final List children; + + const RowControl({Key? key, required this.control, required this.children}) + : super(key: key); + + @override + Widget build(BuildContext context) { + // debugPrint("Row build: ${control.id}"); + // return Row( + // mainAxisAlignment: MainAxisAlignment.start, + // children: + // control.childIds.map((childId) => createControl(childId)).toList()); + + return StoreConnector( + distinct: true, + converter: (store) => PageBreakpointViewModel.fromStore(store), + builder: (context, viewModel) { + debugPrint( + "Row build: ${control.id} with breakpoint: ${viewModel.breakpoint}"); + + return Row( + mainAxisAlignment: MainAxisAlignment.start, + children: control.childIds + .map((childId) => createControl(childId)) + .toList(), + ); + }); + } +} diff --git a/client/lib/controls/stack.dart b/client/lib/controls/stack.dart index 3fe2db77d9..140a420690 100644 --- a/client/lib/controls/stack.dart +++ b/client/lib/controls/stack.dart @@ -12,8 +12,7 @@ class StackControl extends StatelessWidget { @override Widget build(BuildContext context) { debugPrint("Stack build: ${control.id}"); - return Column( - mainAxisAlignment: MainAxisAlignment.center, + return Stack( children: control.childIds.map((childId) => createControl(childId)).toList(), ); diff --git a/client/lib/controls/textfield.dart b/client/lib/controls/textfield.dart index 77408d0650..1383c7db45 100644 --- a/client/lib/controls/textfield.dart +++ b/client/lib/controls/textfield.dart @@ -50,7 +50,8 @@ class _TextFieldControlState extends State { return TextFormField( //initialValue: widget.control.attrs["value"] ?? "", decoration: InputDecoration( - labelText: widget.control.attrs["label"] ?? ""), + labelText: widget.control.attrs["label"] ?? "", + border: const OutlineInputBorder()), controller: _controller, onChanged: (String value) { debugPrint(value); diff --git a/client/lib/main.dart b/client/lib/main.dart index 85a58ba762..48e98faa9b 100644 --- a/client/lib/main.dart +++ b/client/lib/main.dart @@ -1,6 +1,7 @@ import 'dart:developer'; import 'dart:io'; +import 'package:flet_view/widgets/loading_page.dart'; import 'package:flutter/foundation.dart'; import 'package:window_size/window_size.dart'; import 'package:flutter/material.dart'; @@ -13,7 +14,11 @@ import 'utils/uri.dart'; import 'web_socket_client.dart'; import 'controls/create_control.dart'; -void main([List? args]) { +import 'session_store/session_store.dart' + if (dart.library.io) "session_store/session_store_io.dart" + if (dart.library.js) "session_store/session_store_js.dart"; + +void main([List? args]) async { //setupWindow(); final store = Store(appReducer, initialState: AppState.initial()); @@ -21,7 +26,7 @@ void main([List? args]) { var pageUri = Uri.base; if (kDebugMode) { - pageUri = Uri.parse("http://localhost:8550/p/page-1"); + pageUri = Uri.parse("http://localhost:8550/p/test1"); } if (kIsWeb) { @@ -38,25 +43,32 @@ void main([List? args]) { debugPrint("Page URL: $pageUri"); + String pageName = getWebPageName(pageUri); + String? sessionId = SessionStore.get("sessionId"); + // connect WS ws.connect(serverUrl: getWebSocketEndpoint(pageUri), store: store); - ws.registerWebClient(pageName: getWebPageName(pageUri)); - runApp(FletApp( title: 'Flet', store: store, + pageName: pageName, + sessionId: sessionId, )); } class FletApp extends StatelessWidget { final Store store; final String title; + final String pageName; + final String? sessionId; const FletApp({ Key? key, required this.store, required this.title, + required this.pageName, + required this.sessionId, }) : super(key: key); @override @@ -68,11 +80,11 @@ class FletApp extends StatelessWidget { converter: (store) => PageViewModel.fromStore(store), builder: (context, viewModel) { if (viewModel.isLoading) { - return MaterialApp( - title: title, - home: const Scaffold( - body: Center(child: CircularProgressIndicator()), - )); + return LoadingPage( + title: title, + pageName: pageName, + sessionId: sessionId, + ); } else if (viewModel.error != "") { return MaterialApp( title: title, diff --git a/client/lib/models/app_state.dart b/client/lib/models/app_state.dart index 3779004dc6..e379ea10c6 100644 --- a/client/lib/models/app_state.dart +++ b/client/lib/models/app_state.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:equatable/equatable.dart'; import 'control.dart'; @@ -10,26 +12,51 @@ class AppState extends Equatable { final bool isLoading; final String error; final String sessionId; + final Size size; + final String sizeBreakpoint; + final Map sizeBreakpoints; final Map controls; const AppState( {required this.isLoading, required this.error, required this.sessionId, + required this.size, + required this.sizeBreakpoint, + required this.sizeBreakpoints, required this.controls}); - factory AppState.initial() => - const AppState(isLoading: true, error: "", sessionId: "", controls: {}); + factory AppState.initial() => const AppState( + isLoading: true, + error: "", + sessionId: "", + size: Size(0, 0), + sizeBreakpoint: "", + sizeBreakpoints: { + "xs": 0, + "sm": 576, + "md": 768, + "lg": 992, + "xl": 1200, + "xxl": 1400 + }, + controls: {}); AppState copyWith( {bool? isLoading, String? error, String? sessionId, + Size? size, + String? sizeBreakpoint, + Map? sizeBreakpoints, Map? controls}) => AppState( isLoading: isLoading ?? this.isLoading, error: error ?? this.error, sessionId: sessionId ?? this.sessionId, + size: size ?? this.size, + sizeBreakpoint: sizeBreakpoint ?? this.sizeBreakpoint, + sizeBreakpoints: sizeBreakpoints ?? this.sizeBreakpoints, controls: controls ?? this.controls); @override diff --git a/client/lib/models/page_breakpoint_view_model.dart b/client/lib/models/page_breakpoint_view_model.dart new file mode 100644 index 0000000000..a0331c1476 --- /dev/null +++ b/client/lib/models/page_breakpoint_view_model.dart @@ -0,0 +1,23 @@ +import 'dart:ui'; + +import 'package:equatable/equatable.dart'; +import 'package:redux/redux.dart'; + +import 'app_state.dart'; + +class PageBreakpointViewModel extends Equatable { + final String breakpoint; + final Map breakpoints; + + const PageBreakpointViewModel( + {required this.breakpoint, required this.breakpoints}); + + static PageBreakpointViewModel fromStore(Store store) { + return PageBreakpointViewModel( + breakpoint: store.state.sizeBreakpoint, + breakpoints: store.state.sizeBreakpoints); + } + + @override + List get props => [breakpoint, breakpoints]; +} diff --git a/client/lib/models/page_size_view_model.dart b/client/lib/models/page_size_view_model.dart new file mode 100644 index 0000000000..2fb3dddee6 --- /dev/null +++ b/client/lib/models/page_size_view_model.dart @@ -0,0 +1,20 @@ +import 'dart:ui'; + +import 'package:equatable/equatable.dart'; +import 'package:redux/redux.dart'; + +import 'app_state.dart'; + +class PageSizeViewModel extends Equatable { + final Size size; + final Function dispatch; + + const PageSizeViewModel({required this.size, required this.dispatch}); + + static PageSizeViewModel fromStore(Store store) { + return PageSizeViewModel(size: store.state.size, dispatch: store.dispatch); + } + + @override + List get props => [size, dispatch]; +} diff --git a/client/lib/reducers.dart b/client/lib/reducers.dart index a925e8f2a9..979a04b677 100644 --- a/client/lib/reducers.dart +++ b/client/lib/reducers.dart @@ -6,15 +6,37 @@ import 'package:flet_view/protocol/clean_control_payload.dart'; import 'package:flet_view/protocol/message.dart'; import 'package:flet_view/protocol/remove_control_payload.dart'; import 'package:flet_view/protocol/update_control_props_payload.dart'; +import 'package:flutter/cupertino.dart'; import 'actions.dart'; import 'models/app_state.dart'; import 'models/control.dart'; +import 'session_store/session_store.dart' + if (dart.library.io) "session_store/session_store_io.dart" + if (dart.library.js) "session_store/session_store_js.dart"; + enum Actions { increment, setText, setError } AppState appReducer(AppState state, dynamic action) { - if (action is RegisterWebClientAction) { + if (action is PageSizeChangeAction) { + // + // page size changed + // + // calculate break point + final width = action.newSize.width; + String newBreakpoint = ""; + state.sizeBreakpoints.forEach((bpName, bpWidth) { + if (width >= bpWidth) { + newBreakpoint = bpName; + } + }); + + debugPrint( + "New page size: ${action.newSize}, new breakpoint: $newBreakpoint"); + + return state.copyWith(size: action.newSize, sizeBreakpoint: newBreakpoint); + } else if (action is RegisterWebClientAction) { // // register web client // @@ -22,10 +44,15 @@ AppState appReducer(AppState state, dynamic action) { // error return state.copyWith(isLoading: false, error: action.payload.error); } else { + final sessionId = action.payload.session!.id; + + // store sessionId in a cookie + SessionStore.set("sessionId", sessionId); + // connected to the session return state.copyWith( isLoading: false, - sessionId: action.payload.session!.id, + sessionId: sessionId, controls: action.payload.session!.controls); } } else if (action is AppBecomeInactiveAction) { diff --git a/client/lib/session_store/session_store.dart b/client/lib/session_store/session_store.dart new file mode 100644 index 0000000000..97790bf93e --- /dev/null +++ b/client/lib/session_store/session_store.dart @@ -0,0 +1,9 @@ +class SessionStore { + static String get(String name) { + throw UnsupportedError("Not supported!"); + } + + static void set(String name, String value) { + throw UnsupportedError("Not supported!"); + } +} diff --git a/client/lib/session_store/session_store_io.dart b/client/lib/session_store/session_store_io.dart new file mode 100644 index 0000000000..2f47709cfc --- /dev/null +++ b/client/lib/session_store/session_store_io.dart @@ -0,0 +1,11 @@ +import 'package:flutter/foundation.dart'; + +class SessionStore { + static String? get(String name) { + return null; + } + + static void set(String name, String value) { + debugPrint("Do not set cookie!"); + } +} diff --git a/client/lib/session_store/session_store_js.dart b/client/lib/session_store/session_store_js.dart new file mode 100644 index 0000000000..ce6d189d98 --- /dev/null +++ b/client/lib/session_store/session_store_js.dart @@ -0,0 +1,16 @@ +import 'dart:html'; + +import 'package:flutter/foundation.dart'; + +class SessionStore { + static String? get(String name) { + debugPrint("Get session storage $name"); + + return window.sessionStorage[name]; + } + + static void set(String name, String value) { + debugPrint("Set session storage $name"); + window.sessionStorage[name] = value; + } +} diff --git a/client/lib/web_socket_client.dart b/client/lib/web_socket_client.dart index aa376edba8..09150ec60b 100644 --- a/client/lib/web_socket_client.dart +++ b/client/lib/web_socket_client.dart @@ -28,6 +28,11 @@ class WebSocketClient { String _serverUrl = ""; Store? _store; bool _connected = false; + String _pageName = ""; + String _pageHash = ""; + String _winWidth = ""; + String _winHeight = ""; + String? _sessionId; connect({required String serverUrl, required Store store}) async { _serverUrl = serverUrl; @@ -51,18 +56,32 @@ class WebSocketClient { registerWebClient( {required String pageName, - String? pageHash, - String? winWidth, - String? winHeight, + required String pageHash, + required String winWidth, + required String winHeight, String? sessionId}) { + bool firstCall = _pageName == ""; + _pageName = pageName; + _pageHash = pageHash; + _winWidth = winWidth; + _winHeight = winHeight; + _sessionId = sessionId; + + if (firstCall) { + _registerWebClient(); + } + } + + _registerWebClient() { + debugPrint("_registerWebClient"); send(Message( action: MessageAction.registerWebClient, payload: RegisterWebClientRequest( - pageName: pageName, - pageHash: pageHash, - winWidth: winWidth, - winHeight: winHeight, - sessionId: sessionId))); + pageName: _pageName, + pageHash: _pageHash, + winWidth: _winWidth, + winHeight: _winHeight, + sessionId: _sessionId))); } pageEventFromWeb( diff --git a/client/lib/widgets/loading_page.dart b/client/lib/widgets/loading_page.dart new file mode 100644 index 0000000000..85d0e4daf6 --- /dev/null +++ b/client/lib/widgets/loading_page.dart @@ -0,0 +1,48 @@ +import 'package:flet_view/actions.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; + +import '../models/app_state.dart'; +import '../models/page_size_view_model.dart'; +import '../web_socket_client.dart'; + +class LoadingPage extends StatelessWidget { + final String title; + final String pageName; + final String? sessionId; + + const LoadingPage( + {Key? key, + required this.title, + required this.pageName, + required this.sessionId}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: title, + home: Builder(builder: (context) { + return StoreConnector( + distinct: true, + converter: (store) => PageSizeViewModel.fromStore(store), + builder: (context, viewModel) { + MediaQueryData media = MediaQuery.of(context); + if (media.size != viewModel.size) { + viewModel.dispatch(PageSizeChangeAction(media.size)); + ws.registerWebClient( + pageName: pageName, + pageHash: "", + sessionId: sessionId, + winWidth: media.size.width.toInt().toString(), + winHeight: media.size.height.toInt().toString()); + } else { + debugPrint("Page size did not change on load."); + } + return const Scaffold( + body: Center(child: CircularProgressIndicator()), + ); + }); + })); + } +} diff --git a/client/lib/widgets/screen_size.dart b/client/lib/widgets/screen_size.dart new file mode 100644 index 0000000000..d189663f82 --- /dev/null +++ b/client/lib/widgets/screen_size.dart @@ -0,0 +1,49 @@ +import 'dart:async'; + +import 'package:flet_view/models/page_size_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; + +import '../actions.dart'; +import '../models/app_state.dart'; + +class ScreenSize extends StatefulWidget { + const ScreenSize({Key? key}) : super(key: key); + + @override + State createState() => _ScreenSizeState(); +} + +class _ScreenSizeState extends State { + Timer? _debounce; + + _onScreenSizeChanged(Size newSize, Function dispatch) { + if (_debounce?.isActive ?? false) _debounce!.cancel(); + _debounce = Timer(const Duration(milliseconds: 500), () { + debugPrint("Send current size to reducer: $newSize"); + dispatch(PageSizeChangeAction(newSize)); + }); + } + + @override + Widget build(BuildContext context) { + return StoreConnector( + distinct: true, + converter: (store) => PageSizeViewModel.fromStore(store), + builder: (context, viewModel) { + MediaQueryData media = MediaQuery.of(context); + if (media.size != viewModel.size) { + _onScreenSizeChanged(media.size, viewModel.dispatch); + } else { + debugPrint("Page size did not change."); + } + return const SizedBox.shrink(); + }); + } + + @override + void dispose() { + _debounce?.cancel(); + super.dispose(); + } +} diff --git a/client/test/utils/uri_test.dart b/client/test/utils/uri_test.dart index b9a23e7d46..647af25e32 100644 --- a/client/test/utils/uri_test.dart +++ b/client/test/utils/uri_test.dart @@ -7,6 +7,7 @@ void main() { getWebPageName(Uri.parse('http://localhost:8550/p/test/')), "p/test"); expect(getWebPageName(Uri.parse('http://localhost:8550/p/test')), "p/test"); expect(getWebPageName(Uri.parse('http://localhost:8550/')), ""); + expect(getWebPageName(Uri.parse('http://localhost:8550/#/')), ""); }); test("getWebSocketEndpoint returns correct URL", () { diff --git a/docs/controls.md b/docs/controls.md deleted file mode 100644 index 668180246b..0000000000 --- a/docs/controls.md +++ /dev/null @@ -1,262 +0,0 @@ -# Controls - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Flet - Pglet -
Layout
ContainerStack
RowStack horizontal=True
ColumnStack horizontal=False
Spacer-
Expanded-
Stack-
WrapStack wrap=True
ListViewStack horizontal=False
Divider-
GridView-
SplitViewSplitStack
Card-
ExpansionPanel-
Base controls
TextText
IconIcon
ImageImage
To-DoLink
ProgressBarProgress
ProgressRingSpinner
Buttons
ElevatedButtonButton primary=True
OutlinedButtonButton primary=False
TextButtonButton action=True
IconButtonButton icon={icon_name}
PopupMenuButtonButton with MenuItems
Any Button with a custom "child"Button compound=True
Forms
CheckboxCheckbox
RadioChoiceGroup
DropdownDropdown
-ComboBox
DatePickerDatePicker
TimePicker-
-SearchBox
SliderSlider
SpinBoxSpinButton
TextFieldTextbox
SwitchToggle
Utility controls
-Html
-IFrame
-Persona
- -## Container - -Docs: https://api.flutter.dev/flutter/widgets/Container-class.html - -* Padding -* Color (background color) -* Alignment -* Border - -## Row - -Docs: https://api.flutter.dev/flutter/widgets/Row-class.html - -## Column - -Docs: https://api.flutter.dev/flutter/widgets/Column-class.html - -## Expanded - -Docs: https://api.flutter.dev/flutter/widgets/Expanded-class.html - -## Stack - -Docs: https://api.flutter.dev/flutter/widgets/Stack-class.html - -## Wrap - -Docs: https://api.flutter.dev/flutter/widgets/Wrap-class.html - -## ListView - -Docs: https://api.flutter.dev/flutter/widgets/ListView-class.html - -## GridView - -Docs: https://api.flutter.dev/flutter/widgets/GridView-class.html - -## Card - -Docs: https://api.flutter.dev/flutter/widgets/Card-class.html - -## Spacer - -Docs: https://api.flutter.dev/flutter/widgets/Spacer-class.html - -## Divider - -Docs: https://api.flutter.dev/flutter/widgets/Divider-class.html - -## SplitView - -Docs: https://pub.dev/packages/split_view - -## Text - -Docs: https://api.flutter.dev/flutter/material/Text-class.html - -Selectable text docs: https://api.flutter.dev/flutter/material/SelectableText-class.html - -* Value -* Border? (inside Container) - -## Icon - -Docs: https://api.flutter.dev/flutter/widgets/Icon-class.html - -Properties: - -* Name ([The list of icons](https://api.flutter.dev/flutter/material/Icons-class.html)) -* Color ([more](https://api.flutter.dev/flutter/dart-ui/Color-class.html)) -* Size - -## Image - -Docs: https://api.flutter.dev/flutter/widgets/Image-class.html - -Properties: - -* Width -* Height -* Src -* SemanticLabel (Alt) -* Repeat: noRepeat, repeat, repeatX, repeatY -* Opacity -* Fit: contain, cover, fill, fitHeight, fitWidth, none, scaleDown -* Border? (inside Container) - -## TextField - -Docs: https://api.flutter.dev/flutter/material/TextField-class.html - -Example: https://gallery.flutter.dev/#/demo/text-field \ No newline at end of file diff --git a/docs/flet.md b/docs/flet.md index 9459984a42..277ced6449 100644 --- a/docs/flet.md +++ b/docs/flet.md @@ -12,10 +12,14 @@ Flet is for web, desktop and mobile platforms and is based on Flutter. Fluent UI is mostly an internal project... +Flutter is fresh and awesome ... Flutter is [ideal for Single Page Apps (SPA)](https://docs.flutter.dev/development/platform-integration/web#what-scenarios-are-ideal-for-flutter-on-the-web). Many developers critisize Flutter for rendering everything on canvas making SEO impossible. But I personally see rendering on canvas as an advantage: 1) the app looks less clunky, 2) the app looks the same as on desktop and mobile. Non-selectable text? Have you ever tried to select something in AWS or Azure console? If you strongly need SEO then you should choose HTML-based framework such as Next.js or similar. You obviously don't need SEO for your internal dashboard, game or admin panel. + Flutter will give access to mobile experiences: camera, location services, accelerometer, etc. A lot of renamings would create a mess in a project repo - it's easier to start over in a new repository. +Less scope. + A lot of functionality was added to support Bash: a custom protocol via named pipes with commands serialization/deserialization, CLI support with `pglet page` and `pglet app` commands. It's not necessary anymore with high level languages such like Python which communicate with Flet server via WebSockets directly. With Flet we are switching to monorepo storing Flet server, Flutter client and all language bindings (SDKs). Every CI build and release creates the same, unique version for all the apps and components. diff --git a/docs/roadmap.md b/docs/roadmap.md new file mode 100644 index 0000000000..226551f7a9 --- /dev/null +++ b/docs/roadmap.md @@ -0,0 +1,495 @@ +# Flet Roadmap + +## Controls + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
✓ StatusFletPgletPhase
Layout
ContainerStackS1
RowStack horizontal=TrueS1 (flex, wrap)
ColumnStack horizontal=FalseS1 (flex, wrap)
Stack-S1
ListViewStack horizontal=FalseS1
Divider-
Spacer-
GridView-S1
SplitViewSplitStack
Card-
Base controls
TextTextS1
MarkdownText markdown=True
IconIconS1
ImageImageS1
Chip-
To-DoLink
ProgressBarProgressS1
ProgressRingSpinnerS1
Buttons
ElevatedButtonButton primary=TrueS1
OutlinedButtonButton primary=FalseS1
TextButtonButton action=TrueS1
IconButtonButton icon={icon_name}S1
PopupMenuButtonButton with MenuItems
FloatingActionButton-
Forms
CheckboxCheckboxS1
RadioChoiceGroupS1
DropdownDropdownS1
-ComboBox
DatePickerDatePicker
TimePicker-
[Example]SearchBox
SliderSliderS1
TextFieldTextboxS1
SwitchToggleS1
SpinBoxSpinButton
Dialogs, alerts, and panels
BannerMessageS1
SnackBar-S1
AlertDialogDialog
SimpleDialog-
BottomSheet-
ExpansionPanel-
App structure and navigation
Appbar
BottomNavigationBar
Drawer
TabBar
Grids
DataTableGrid
Table-
Utility controls
-Html
-IFrame
-Persona
+ +## Colors + +[Full list of Material colors](https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/material/colors.dart) + +## Icons + +[Full list of Material icons](https://raw.githubusercontent.com/flutter/flutter/master/packages/flutter/lib/src/material/icons.dart) + +## Page + +Properties: + +- title +- design - `material` (default), `cupertino`, `fluent`, `macos`. +- theme - `light`, `dark`, `system` +- horizontalAlignment - `start` (default), `center`, `end`, `stretch` +- verticalAlignment - `start`, `end`, `center`, `spaceBetween`, `spaceAround`, `spaceEvenly`. +- spacing - gap between adjacent items, default +- color - background color +- windowWidth - current window width +- windowHeight - current window height +- themeMode - `system` (default), `light`, `dark` ([more info](https://stackoverflow.com/questions/60232070/how-to-implement-dark-mode-in-flutter)) + +## Control + +Base control class. + +Properties: + +- id +- visible +- disabled + +- Expanded (int) - The control is forced to fill the available space inside Row or Column. Flex factor specified by the property. Default is 1. The property has affect only for direct descendants of Row and Column controls. (Wrap control into Expanded). +- Flexible (int) - The child can be at most as large as the available space (but is allowed to be smaller) inside Row or Column. Flex factor specified by the property. Default is 1. The property has affect only for direct descendants of Row and Column controls. (Wrap control into Flexible with fit=FlexFit.loose). + +The only difference if you use Flexible instead of Expanded, is that Flexible lets its child have the same or smaller width than the Flexible itself, while Expanded forces its child to have the exact same width of the Expanded. But both Expanded and Flexible ignore their children’s width when sizing themselves. + +- width - wrap into SizedBox +- height - wrap into SizedBox +- minHeight - wrap into ConstrainedBox +- maxHeight - wrap into ConstrainedBox +- minWidth - wrap into ConstrainedBox +- maxWidth - wrap into ConstrainedBox + +- fit +- fitAlign - Wrap into FittedBox + +- opacity - allows to specify transparency of the control, hide it completely or blend with another if used with Stack. 0.0 - hidden, 1.0 - fully visible. See https://api.flutter.dev/flutter/widgets/Opacity-class.html. + +- marging +- padding + +More info: + +- https://api.flutter.dev/flutter/widgets/Expanded-class.html +- https://api.flutter.dev/flutter/widgets/Flexible-class.html + +## Container + +Docs: https://api.flutter.dev/flutter/widgets/Container-class.html + +- color (background color - `decoration: BoxDecoration.color`) +- alignment - `topLeft`, `topCenter`, `topRight`, `centerLeft`, `center`, `centerRight`, `bottomLeft`, `bottomCenter`, `bottomRight` +- borderColor +- borderWidth +- borderStyle - `solid`, `node` +- borderRadius +- verticalScroll +- horizontalScroll +- autoScroll - `end`, `start` ([example](https://stackoverflow.com/questions/43485529/programmatically-scrolling-to-the-end-of-a-listview)). + +## Row + +Docs: https://api.flutter.dev/flutter/widgets/Row-class.html + +- spacing - gap between adjacent items (SizedBox) +- wrap - switch to "Wrap" control +- runSpacing - gap between runs + +## Column + +Docs: https://api.flutter.dev/flutter/widgets/Column-class.html + +- spacing - gap between adjacent items (SizedBox) +- wrap - switch to "Wrap" control +- runSpacing - gap between runs + +## Stack + +Docs: https://api.flutter.dev/flutter/widgets/Stack-class.html + +## ListView + +Docs: https://api.flutter.dev/flutter/widgets/ListView-class.html + +## GridView + +Docs: https://api.flutter.dev/flutter/widgets/GridView-class.html + +## Card + +Docs: https://api.flutter.dev/flutter/widgets/Card-class.html + +## Divider + +Docs: https://api.flutter.dev/flutter/widgets/Divider-class.html + +Properties: + +- height +- thickness +- indent +- endIndent +- color + +## SplitView + +Docs: https://pub.dev/packages/split_view + +## Text + +Docs: https://api.flutter.dev/flutter/material/Text-class.html + +Selectable text docs: https://api.flutter.dev/flutter/material/SelectableText-class.html + +TextTheme: https://api.flutter.dev/flutter/material/TextTheme-class.html + +- value +- textAlign - `center`, `end`, `justify`, `left`, `right`, `start` (for RTL and LTR texts) +- size +- bold (weight=w700) +- weight +- italic +- themeStyle ([more details](https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/material/text_theme.dart)) +- pre (P2) - [more info](https://stackoverflow.com/questions/64145307/full-list-of-font-families-provided-with-flutter) +- color +- bgColor +- overflow - (TextOverflow) `clip`, `ellipsis`, `fade`, `visible` +- selectable + + +## Icon + +Docs: https://api.flutter.dev/flutter/widgets/Icon-class.html + +Icons list: https://raw.githubusercontent.com/flutter/flutter/master/packages/flutter/lib/src/material/icons.dart + +Properties: + +- Name ([The list of icons](https://api.flutter.dev/flutter/material/Icons-class.html)) +- Color ([more](https://api.flutter.dev/flutter/dart-ui/Color-class.html)) +- Size + +## Image + +Docs: https://api.flutter.dev/flutter/widgets/Image-class.html + +Properties: + +- Width +- Height +- Src +- SemanticLabel (Alt) +- Repeat: noRepeat, repeat, repeatX, repeatY +- Opacity +- Fit: contain, cover, fill, fitHeight, fitWidth, none, scaleDown +- Border? (inside Container) + +## TextField + +Docs: https://api.flutter.dev/flutter/material/TextField-class.html + +Example: https://gallery.flutter.dev/#/demo/text-field diff --git a/media/logo/flet-logo.cdr b/media/logo/flet-logo.cdr index 667021ec02..fa88398305 100644 Binary files a/media/logo/flet-logo.cdr and b/media/logo/flet-logo.cdr differ diff --git a/sdk/python/.gitattributes b/sdk/python/.gitattributes new file mode 100644 index 0000000000..dfe0770424 --- /dev/null +++ b/sdk/python/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/sdk/python/.gitignore b/sdk/python/.gitignore new file mode 100644 index 0000000000..466a6661b7 --- /dev/null +++ b/sdk/python/.gitignore @@ -0,0 +1,133 @@ +@@ -0,0 +1,125 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# VS Code +.vscode/ + +# PDM +.pdm.toml +__pypackages__/ \ No newline at end of file diff --git a/sdk/python/.pre-commit-config.yaml b/sdk/python/.pre-commit-config.yaml new file mode 100644 index 0000000000..c02671ee33 --- /dev/null +++ b/sdk/python/.pre-commit-config.yaml @@ -0,0 +1,9 @@ +repos: + - repo: https://github.com/pycqa/isort + rev: 5.10.1 + hooks: + - id: isort + - repo: https://github.com/ambv/black + rev: 22.1.0 + hooks: + - id: black diff --git a/sdk/python/CONTRIBUTING.md b/sdk/python/CONTRIBUTING.md new file mode 100644 index 0000000000..cb2b3b3bbd --- /dev/null +++ b/sdk/python/CONTRIBUTING.md @@ -0,0 +1,112 @@ +# Contributing to Flet for Python + +Thank you for your interest in contributing to Flet! + +## Clone repo + +``` +git clone https://github.com/flet-dev/flet +``` + +## Install PDM + +### Windows + +```powershell +(Invoke-WebRequest -Uri https://raw.githubusercontent.com/pdm-project/pdm/main/install-pdm.py -UseBasicParsing).Content | python - +``` + +Enable PEP 582: + +``` +pdm --pep582 +``` + +Run `refreshenv` after installing PDM on Windows or restart terminal. + +### macOS + +``` +brew install pdm +``` + +Enable PEP 582: + +``` +pdm --pep582 >> ~/.zprofile +``` + +Restart the terminal session to take effect. + +## Open worker directory + +``` +cd flet/sdk/python +``` + +## Install dependencies + +To install all Flet dependencies and enable the project as editable package run: + +``` +pdm install +``` + +## Check the installation + +Run "counter" example: + +``` +python3 examples/counter.py +``` + +During the first run Flet Server will be downloaded from GitHub releases to `$HOME/.flet/bin` directory and started from there. The version of Flet Server to download is taken from `FLET_VERSION` variable in `appveyor.yml` in the root of repository. + +You should see a new browser window opened with "counter" web app running. + +## Running tests + +Pytest should be run with `pdm run`: + +``` +pdm run pytest +``` + +## Code formatting + +The project uses [Black](https://github.com/psf/black) formatting style. All `.py` files in a PR must be black-formatted. + +IDE-specific Black integration guides: + +* [VSCode: Using Black to automatically format Python](https://dev.to/adamlombard/how-to-use-the-black-python-code-formatter-in-vscode-3lo0) + +### Sort imports on Save + +VS Code includes "isort" by default. + +Add the following to user's `settings.json` : + +```json +"[python]": { + "editor.codeActionsOnSave": { + "source.organizeImports": true + } +}, +"python.sortImports.args": [ + "--trailing-comma", + "--use-parentheses", + "--line-width", + "88", + "--multi-line", + "3", + "--float-to-top" +], +``` + +All isort command line options can be found [here](https://pycqa.github.io/isort/docs/configuration/options.html). + +## pre-commit + +[pre-commit](https://pre-commit.com) is a dev dependency of Flet and is automatically installed by `pdm install`. +To install the pre-commit hooks run: `pre-commit install`. +Once installed, everytime you commit, pre-commit will run the configured hooks against changed files. diff --git a/sdk/python/README.md b/sdk/python/README.md new file mode 100644 index 0000000000..0a113ed4a7 --- /dev/null +++ b/sdk/python/README.md @@ -0,0 +1,33 @@ +# Flet - quickly build interactive apps for Web, Desktop and Mobile in Python + +[Flet](https://flet.dev) is a rich User Interface (UI) framework to quickly build interactive Web, Desktop and Mobile apps in Python without prior knowledge of web technologies like HTTP, HTML, CSS or JavaSscript. You build UI with [controls](https://flet.dev/docs/reference/controls) based on [Flutter](https://flutter.dev/) to ensure your programs look cool and professional. + +## Requirements + +* Python 3.7 or above on Windows, Linux or macOS + +## Installation + +``` +pip install flet +``` + +## Hello, world! + +```python +import flet +from flet import Text + +p = flet.page() +p.add(Text("Hello, world!")) +``` + +Run the sample above and a new browser window will pop up: + +![Sample app in a browser](https://flet.dev/img/docs/quickstart-hello-world.png "Sample app in a browser") + +Continue with [Python tutorial](https://flet.dev/docs/tutorials/python) demonstrating how to build a simple To-Do web app and share it on the internet. + +Browse for more [Flet examples](https://github.com/flet-dev/flet/tree/main/sdk/python/examples). + +Join to a conversation on [Flet Discord server](https://discord.gg/rWjf7xx). \ No newline at end of file diff --git a/sdk/python/build-wheels.py b/sdk/python/build-wheels.py new file mode 100644 index 0000000000..0d3ae94171 --- /dev/null +++ b/sdk/python/build-wheels.py @@ -0,0 +1,209 @@ +import glob +import hashlib +import io +import json +import os +import pathlib +import shutil +import stat +import sys +import urllib.request +import zipfile +from base64 import urlsafe_b64encode + +packages = { + "Windows amd64": { + "asset": "windows_amd64", + "exec": "flet.exe", + "wheel_tags": ["py3-none-win_amd64"], + "file_suffix": "py3-none-win_amd64", + }, + "Linux amd64": { + "asset": "linux_amd64", + "exec": "flet", + "wheel_tags": [ + "py3-none-manylinux_2_17_x86_64", + "py3-none-manylinux2014_x86_64", + ], + "file_suffix": "py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64", + }, + "Linux arm64": { + "asset": "linux_arm64", + "exec": "flet", + "wheel_tags": [ + "py3-none-manylinux_2_17_aarch64", + "py3-none-manylinux2014_aarch64", + ], + "file_suffix": "py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64", + }, + "Linux arm": { + "asset": "linux_arm_7", + "exec": "flet", + "wheel_tags": [ + "py3-none-manylinux_2_17_armv7l", + "py3-none-manylinux2014_armv7l", + ], + "file_suffix": "py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l", + }, + "macOS amd64": { + "asset": "darwin_amd64", + "exec": "flet", + "wheel_tags": ["py3-none-macosx_10_14_x86_64"], + "file_suffix": "py3-none-macosx_10_14_x86_64", + }, + "macOS arm64": { + "asset": "darwin_arm64", + "exec": "flet", + "wheel_tags": ["py3-none-macosx_12_0_arm64"], + "file_suffix": "py3-none-macosx_12_0_arm64", + }, +} + + +def unpack_zip(zip_path, dest_dir): + zf = zipfile.ZipFile(zip_path) + zf.extractall(path=dest_dir) + + +def download_flet_server(jobId, asset, exec_filename, dest_file): + flet_url = f"https://ci.appveyor.com/api/buildjobs/{jobId}/artifacts/server%2Fdist%2Fflet_{asset}%2F{exec_filename}" + print(f"Downloading {flet_url}...") + urllib.request.urlretrieve(flet_url, dest_file) + st = os.stat(dest_file) + os.chmod(dest_file, st.st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH) + + +def get_flet_server_job_id(): + account_name = os.environ.get("APPVEYOR_ACCOUNT_NAME") + project_slug = os.environ.get("APPVEYOR_PROJECT_SLUG") + build_id = os.environ.get("APPVEYOR_BUILD_ID") + url = f"https://ci.appveyor.com/api/projects/{account_name}/{project_slug}/builds/{build_id}" + print(f"Fetching build details at {url}") + req = urllib.request.Request(url) + req.add_header("Content-type", "application/json") + project = json.loads(urllib.request.urlopen(req).read().decode()) + jobId = None + for job in project["build"]["jobs"]: + if job["name"] == "Build Server binaries": + jobId = job["jobId"] + break + return jobId + + +def read_chunks(file, size=io.DEFAULT_BUFFER_SIZE): + """Yield pieces of data from a file-like object until EOF.""" + while True: + chunk = file.read(size) + if not chunk: + break + yield chunk + + +def rehash(path, blocksize=1 << 20): + """Return (hash, length) for path using hashlib.sha256()""" + h = hashlib.sha256() + length = 0 + with open(path, "rb") as f: + for block in read_chunks(f, size=blocksize): + length += len(block) + h.update(block) + digest = "sha256=" + urlsafe_b64encode(h.digest()).decode("latin1").rstrip("=") + # unicode/str python2 issues + return (digest, str(length)) # type: ignore + + +current_dir = pathlib.Path(os.getcwd()) +print("current_dir", current_dir) + +whl_files = glob.glob(str(current_dir.joinpath("dist", "*.whl"))) +if len(whl_files) == 0: + print("No .whl files found. Run 'pdm build' first.") + sys.exit(1) + +orig_whl = whl_files[0] + +package_version = os.path.basename(orig_whl).split("-")[1] +flet_server_jobId = get_flet_server_job_id() + +print("package_version", package_version) +print("flet_server_jobId", flet_server_jobId) + +for name, package in packages.items(): + print(f"Building {name}...") + + print("Unpacking original wheel file...") + unpacked_whl = current_dir.joinpath("dist", "wheel") + unpacked_whl.mkdir(exist_ok=True) + unpack_zip(orig_whl, unpacked_whl) + + # read original WHEEL file omitting tags + wheel_path = str( + current_dir.joinpath( + "dist", "wheel", f"flet-{package_version}.dist-info", "WHEEL" + ) + ) + wheel_lines = [] + + with open(wheel_path, "r") as f: + for line in f.readlines(): + if not "Tag: " in line: + wheel_lines.append(line) + + # print(wheel_lines) + + # read original RECORD file + record_path = str( + current_dir.joinpath( + "dist", "wheel", f"flet-{package_version}.dist-info", "RECORD" + ) + ) + record_lines = [] + + with open(record_path, "r") as f: + for line in f.readlines(): + if not "dist-info/WHEEL," in line: + record_lines.append(line) + + # print(record_lines) + + # create "bin" directory + bin_path = current_dir.joinpath("dist", "wheel", "flet", "bin") + bin_path.mkdir(exist_ok=True) + asset = package["asset"] + exec_filename = package["exec"] + exec_path = str(bin_path.joinpath(exec_filename)) + download_flet_server(flet_server_jobId, asset, exec_filename, exec_path) + + # update RECORD + h, l = rehash(exec_path) + record_lines.insert(len(record_lines) - 3, f"flet/bin/{exec_filename},{h},{l}\n") + # for line in record_lines: + # print(line.strip()) + + # update WHEEL file + for tag in package["wheel_tags"]: + wheel_lines.append(f"Tag: {tag}\n") + + # save WHEEL + with open(wheel_path, "w") as f: + f.writelines(wheel_lines) + + # update RECORD + h, l = rehash(wheel_path) + record_lines.insert( + len(record_lines) - 3, + f"flet-{package_version}.dist-info/WHEEL,{h},{l}\n", + ) + + # save RECORD + with open(record_path, "w") as f: + f.writelines(record_lines) + + # zip + suffix = package["file_suffix"] + zip_filename = current_dir.joinpath("dist", f"flet-{package_version}-{suffix}") + shutil.make_archive(zip_filename, "zip", unpacked_whl) + os.rename(f"{zip_filename}.zip", f"{zip_filename}.whl") + + # cleanup + shutil.rmtree(str(unpacked_whl)) diff --git a/sdk/python/examples/autoscroll.py b/sdk/python/examples/autoscroll.py new file mode 100644 index 0000000000..eef5956a61 --- /dev/null +++ b/sdk/python/examples/autoscroll.py @@ -0,0 +1,50 @@ +import logging + +import flet +from flet import Button, Image, Stack, Text + +logging.basicConfig(level=logging.DEBUG) + +page = flet.page("autoscroll", update=False, no_window=True, permissions="") +# page.theme_primary_color = "green" +# page.gap = 100 +# page.padding = 100 +# page.update() + +st = Stack(scroll_y=True, auto_scroll=True) + +scroll_box = Stack( + height="400", width="100%", bgcolor="#f0f0f0", vertical_align="end", controls=[st] +) + + +def add_click(e): + page.i += 1 + st.controls.append( + Stack( + horizontal=True, + vertical_align="center", + controls=[ + Image( + src="https://avatars.githubusercontent.com/u/5041459?s=88&v=4", + width=30, + height=30, + border_radius=15, + fit="contain", + ), + Text(f"Line {page.i}"), + ], + ) + ) + st.update() + + +page.add(scroll_box, Button("Add line", primary=True, focused=True, on_click=add_click)) + +for i in range(0, 10): + st.controls.append(Text(f"Line {i}")) + st.update() + +page.i = i + +input() diff --git a/sdk/python/examples/barchart.py b/sdk/python/examples/barchart.py new file mode 100644 index 0000000000..11f6c1434f --- /dev/null +++ b/sdk/python/examples/barchart.py @@ -0,0 +1,42 @@ +import flet +from flet import Text, BarChart +from flet.barchart import Point + + +def main(page): + + # Fractions BarChart + chart1 = BarChart( + data_mode="fraction", + width="50%", + tooltips=True, + points=[ + Point( + legend="C:", x=20, y=250, x_tooltip="20%", y_tooltip="20 of 250 GB used" + ), + Point(legend="D:", x=50, y=250, x_tooltip="50%"), + Point(legend="E:", x=30, y=250, x_tooltip="30%"), + ], + ) + + # Percentage BarChart + chart2 = BarChart( + data_mode="percentage", + width="30%", + tooltips=True, + points=[ + Point(legend="/disk1", x=20, y=100, color="green"), + Point(legend="/disk2", x=50, y=100, color="yellow"), + Point(legend="/disk3", x=90, y=100, color="red"), + ], + ) + + page.add( + Text("Fractions BarChart", size="xLarge"), + chart1, + Text("Percentage BarChart", size="xLarge"), + chart2, + ) + + +flet.app("python-barchart", target=main) diff --git a/sdk/python/examples/chat-ref.py b/sdk/python/examples/chat-ref.py new file mode 100644 index 0000000000..ebeced40ea --- /dev/null +++ b/sdk/python/examples/chat-ref.py @@ -0,0 +1,103 @@ +# import logging + +import flet +from flet import Button, Dialog, Stack, Text, Textbox +from flet.ref import Ref + +# logging.basicConfig(level=logging.DEBUG) + +pub_sub = {} + + +def broadcast(user, message): + for session_id, handler in pub_sub.items(): + handler(user, message) + + +def main(page): + + page.padding = 10 + page.vertical_fill = True + page.title = "Flet Chat Example" + page.bgcolor = "neutralLight" + page.theme = "light" # "dark" + + username_dialog = Ref[Dialog]() + username = Ref[Textbox]() + messages = Ref[Stack]() + message = Ref[Textbox]() + + def on_message(user, message): + if user: + messages.current.controls.append(Text(f"{user}: {message}")) + else: + messages.current.controls.append( + Text(message, color="#888", size="small", italic=True) + ) + page.update() + + pub_sub[page.session_id] = on_message + + def send_click(e): + if message.current.value == "": + return + broadcast(page.user, message.current.value) + message.current.value = "" + page.update() + + page.user = page.session_id + + def join_click(e): + if username.current.value == "": + username.current.error_message = "Name cannot be blank!" + username.current.update() + else: + page.user = username.current.value + username_dialog.current.open = False + # user_name.focused = False + message.current.prefix = f"{page.user}:" + message.current.focused = True + page.update() + broadcast(None, f"{page.user} entered the chat!") + + # layout + page.add( + Stack( + height="100%", + width="100%", + bgcolor="white", + padding=10, + border_radius=5, + vertical_align="end", + controls=[Stack(ref=messages, scroll_y=True, auto_scroll=True)], + ), + Stack( + horizontal=True, + width="100%", + controls=[ + Textbox( + ref=message, + width="100%", + multiline=True, + rows=1, + auto_adjust_height=True, + shift_enter=True, + resizable=False, + ), + Button("Send", primary=True, on_click=send_click), + ], + on_submit=send_click, + ), + Dialog( + ref=username_dialog, + open=True, + blocking=True, + auto_dismiss=False, + title="Welcome!", + controls=[Textbox(ref=username, label="Enter your name", focused=True)], + footer=[Button(text="Join chat", primary=True, on_click=join_click)], + ), + ) + + +flet.app("chat", target=main, share=False) diff --git a/sdk/python/examples/chat.py b/sdk/python/examples/chat.py new file mode 100644 index 0000000000..46675ea70d --- /dev/null +++ b/sdk/python/examples/chat.py @@ -0,0 +1,94 @@ +import logging + +import flet +from flet import Button, Dialog, Stack, Text, Textbox + +logging.basicConfig(level=logging.DEBUG) + +pub_sub = {} + + +def broadcast(user, message): + for session_id, handler in pub_sub.items(): + handler(user, message) + + +def main(page): + + page.padding = 10 + page.vertical_fill = True + page.title = "Flet Chat Example" + page.bgcolor = "neutralLight" + page.theme = "light" # "dark" + + messages = Stack(scroll_y=True, auto_scroll=True) + messages_pane = Stack( + height="100%", + width="100%", + bgcolor="white", + padding=10, + border_radius=5, + vertical_align="end", + controls=[messages], + ) + message = Textbox( + width="100%", + multiline=True, + rows=1, + auto_adjust_height=True, + shift_enter=True, + resizable=False, + ) + + def on_message(user, message): + if user: + messages.controls.append(Text(f"{user}: {message}")) + else: + messages.controls.append( + Text(message, color="#888", size="small", italic=True) + ) + page.update() + + pub_sub[page.session_id] = on_message + + def send_click(e): + if message.value == "": + return + broadcast(page.user, message.value) + message.value = "" + page.update() + + user_name = Textbox(label="Enter your name", focused=True) + + page.user = page.session_id + + def join_click(e): + if user_name.value == "": + user_name.error_message = "Name cannot be blank!" + user_name.update() + else: + page.user = user_name.value + dlg.open = False + # user_name.focused = False + message.prefix = f"{page.user}:" + message.focused = True + page.update() + broadcast(None, f"{page.user} entered the chat!") + + dlg = Dialog( + open=True, + blocking=True, + auto_dismiss=False, + title="Welcome!", + controls=[user_name], + footer=[Button(text="Join chat", primary=True, on_click=join_click)], + ) + + send = Button("Send", primary=True, on_click=send_click) + form = Stack( + horizontal=True, width="100%", controls=[message, send], on_submit=send_click + ) + page.add(messages_pane, form, dlg) + + +flet.app("chat", target=main, share=False) diff --git a/sdk/python/examples/combobox.py b/sdk/python/examples/combobox.py new file mode 100644 index 0000000000..b034d1ddbe --- /dev/null +++ b/sdk/python/examples/combobox.py @@ -0,0 +1,68 @@ +import flet +from flet import ComboBox, combobox + +page = flet.page("combobox-test") +page.horizontal_align = "stretch" +page.add( + ComboBox( + label="Your favorite color", + value="c", + on_focus=lambda e: print("on_focus!"), + on_blur=lambda e: print("on_blur!"), + on_change=lambda e: print("on_change!"), + options=[ + combobox.Option("RGB", item_type="header"), + combobox.Option("red"), + combobox.Option("green"), + combobox.Option("blue"), + combobox.Option("div1", item_type="divider"), + combobox.Option("CMYK", item_type="header"), + combobox.Option("c", "Cyan"), + combobox.Option("m", "Magenta"), + combobox.Option("y", "Yellow"), + combobox.Option("k", "Black"), + ], + ), + ComboBox( + label="Your favorite car makers", + multi_select=True, + value=["BMW, Volkswagen"], + width="50%", + on_change=lambda e: print("selected cars:", e.control.value), + options=[ + combobox.Option("Select all", item_type="select_all"), + combobox.Option("div1", item_type="divider"), + combobox.Option("BMW"), + combobox.Option("Toyota"), + combobox.Option("Volkswagen"), + combobox.Option("Mercedes-Benz", disabled=True), + ], + ), + ComboBox( + label="Allows free form", + multi_select=False, + width="50%", + allow_free_form=True, + on_focus=lambda e: print("on_focus!"), + on_blur=lambda e: print("on_blur!"), + options=[ + combobox.Option("One"), + combobox.Option("Two"), + combobox.Option("Five"), + ], + ), + ComboBox( + label="Allows free form with multi-select and error message", + multi_select=True, + width="50%", + allow_free_form=True, + error_message="This field cannot be left blank!", + options=[ + combobox.Option("Red"), + combobox.Option("Green"), + combobox.Option("Blue"), + ], + ), +) + +input() diff --git a/sdk/python/examples/counter.py b/sdk/python/examples/counter.py new file mode 100644 index 0000000000..2d39ed2e42 --- /dev/null +++ b/sdk/python/examples/counter.py @@ -0,0 +1,52 @@ +import logging + +import flet +from flet import Button, Column, Row, Text, Textbox +from flet.expanded import Expanded + +logging.basicConfig(level=logging.DEBUG) + + +def main(page): + page.title = "Counter" + page.update() + + def on_click(e): + try: + count = int(txt_number.value) + + txt_number.error_message = "" + + if e.data == "+": + txt_number.value = count + 1 + + elif e.data == "-": + txt_number.value = count - 1 + + result.value = f"Clicked: {e.data}" + + except ValueError: + txt_number.error_message = "Please enter a number" + + page.update() + + txt_number = Textbox(value="0", align="right") + result = Text() + + page.add( + Expanded( + Row( + controls=[ + Button("-", on_click=on_click, data="-"), + Expanded(txt_number), + Expanded(Textbox(label="Another textbox")), + Button("+", on_click=on_click, data="+"), + Column(controls=[result]), + ], + ) + ), + Expanded(Column(controls=[Text("Just some text")])), + ) + + +flet.app(name="test1", port=8550, target=main) diff --git a/sdk/python/examples/dropdown-flutter.py b/sdk/python/examples/dropdown-flutter.py new file mode 100644 index 0000000000..eaa3377af7 --- /dev/null +++ b/sdk/python/examples/dropdown-flutter.py @@ -0,0 +1,35 @@ +import logging +import os + +import flet +from flet import Button, Dropdown, Stack, dropdown + +logging.basicConfig(level=logging.INFO) + +page = flet.page() + +dd = Dropdown( + options=[ + dropdown.Option("a", "Item A"), + dropdown.Option("b", "Item B"), + dropdown.Option("c", "Item C"), + ] +) + + +def btn2_click(e): + dd.options.append(dropdown.Option("d", "Item D")) + page.update() + + +def btn3_click(e): + dd.options[1].text = "Item Blah Blah Blah" + page.update() + + +btn2 = Button("Add new item!", on_click=btn2_click) +btn3 = Button("Change second item", on_click=btn3_click) + +page.add(dd, btn2, btn3) + +input() diff --git a/sdk/python/examples/greeter.py b/sdk/python/examples/greeter.py new file mode 100644 index 0000000000..2887f539c9 --- /dev/null +++ b/sdk/python/examples/greeter.py @@ -0,0 +1,16 @@ +import flet +from flet import Textbox, Button, Text + + +def main(page): + def btn_click(e): + name = txt_name.value + page.clean() + page.add(Text(f"Hello, {name}!")) + + txt_name = Textbox("Your name") + + page.add(txt_name, Button("Say hello!", on_click=btn_click)) + + +flet.app(target=main) diff --git a/sdk/python/examples/grid.py b/sdk/python/examples/grid.py new file mode 100644 index 0000000000..897028c376 --- /dev/null +++ b/sdk/python/examples/grid.py @@ -0,0 +1,203 @@ +import flet +from flet import ( + Button, + Checkbox, + Column, + Dropdown, + Grid, + Stack, + Text, + Textbox, + Toolbar, + dropdown, + toolbar, +) + + +class Person: + def __init__( + self, + first_name: str, + last_name: str, + age: int = None, + employee: bool = False, + color: str = None, + ): + self.first_name = first_name + self.last_name = last_name + self.age = age + self.employee = employee + self.color = color + + +def main(page): + page.title = "Grid example" + page.update() + + # Basic grid + page.add( + Text("Basic grid", size="large"), + Stack( + width="50%", + controls=[ + Grid( + columns=[ + Column(name="First name", field_name="first_name"), + Column(name="Last name", field_name="last_name"), + Column(name="Age", field_name="age"), + ], + items=[ + Person(first_name="John", last_name="Smith", age=30), + Person(first_name="Samantha", last_name="Fox", age=43), + Person(first_name="Alice", last_name="Brown", age=25), + ], + ) + ], + ), + ) + + # Sortable grid + page.add( + Text("Sortable grid with resizable columns and selectable rows", size="large"), + Grid( + selection_mode="single", + preserve_selection=True, + columns=[ + Column( + resizable=True, + sortable="string", + name="First name", + field_name="first_name", + ), + Column( + resizable=True, + sortable="string", + sorted="asc", + name="Last name", + field_name="last_name", + ), + Column(resizable=True, sortable="number", name="Age", field_name="age"), + ], + items=[ + Person(first_name="John", last_name="Smith", age=30), + Person(first_name="Samantha", last_name="Fox", age=43), + Person(first_name="Alice", last_name="Brown", age=25), + ], + ), + ) + + # Compact grid + page.add( + Text("Compact grid with no header and multiple selection", size="large"), + Grid( + compact=True, + header_visible=False, + selection_mode="multiple", + preserve_selection=True, + columns=[ + Column(max_width=100, field_name="first_name"), + Column(max_width=100, field_name="last_name"), + Column(max_width=100, field_name="age"), + ], + items=[ + Person(first_name="John", last_name="Smith", age=30), + Person(first_name="Samantha", last_name="Fox", age=43), + Person(first_name="Alice", last_name="Brown", age=25), + ], + ), + ) + + # Dynamic grid + grid = None + + def delete_records(e): + for r in grid.selected_items: + grid.items.remove(r) + page.update() + + delete_records = toolbar.Item( + text="Delete records", icon="Delete", disabled=True, on_click=delete_records + ) + grid_toolbar = Toolbar(items=[delete_records]) + + def grid_items_selected(e): + delete_records.disabled = len(e.control.selected_items) == 0 + delete_records.update() + + grid = Grid( + selection_mode="multiple", + compact=True, + header_visible=True, + columns=[ + Column( + name="First name", template_controls=[Textbox(value="{first_name}")] + ), + Column(name="Last name", template_controls=[Textbox(value="{last_name}")]), + Column(name="Age", template_controls=[Text(value="{age}")]), + Column( + name="Favorite color", + template_controls=[ + Dropdown( + value="{color}", + options=[ + dropdown.Option("red", "Red"), + dropdown.Option("green", "Green"), + dropdown.Option("blue", "Blue"), + ], + ) + ], + ), + Column( + name="Is employee", template_controls=[Checkbox(value_field="employee")] + ), + ], + items=[ + Person( + first_name="John", + last_name="Smith", + age=30, + employee=False, + color="blue", + ), + Person( + first_name="Jack", + last_name="Brown", + age=43, + employee=True, + color="green", + ), + Person(first_name="Alice", last_name="Fox", age=25, employee=False), + ], + margin=0, + on_select=grid_items_selected, + ) + + first_name = Textbox("First name") + last_name = Textbox("Last name") + age = Textbox("Age") + + def add_record(e): + grid.items.append( + Person( + first_name=first_name.value, + last_name=last_name.value, + age=age.value, + employee=True, + ) + ) + first_name.value = "" + last_name.value = "" + age.value = "" + page.update() + + page.add( + Text("Dynamic grid with template columns", size="large"), + grid_toolbar, + grid, + Text("Add new employee record", size="medium"), + Stack(horizontal=True, controls=[first_name, last_name, age]), + Button("Add record", on_click=add_record), + ) + + +flet.app("python-grid", target=main, share=False) diff --git a/sdk/python/examples/hello-app.py b/sdk/python/examples/hello-app.py new file mode 100644 index 0000000000..4b1af69e81 --- /dev/null +++ b/sdk/python/examples/hello-app.py @@ -0,0 +1,10 @@ +import flet +from flet import Text + + +def main(page): + print(page.user_auth_provider, page.user_name, page.user_email) + page.add(Text(f"Hello to session {page.session_id}!")) + + +flet.app(target=main, permissions="") diff --git a/sdk/python/examples/hello.py b/sdk/python/examples/hello.py new file mode 100644 index 0000000000..e73d676cfc --- /dev/null +++ b/sdk/python/examples/hello.py @@ -0,0 +1,6 @@ +import flet +from flet import Text + +page = flet.page("hello-world") +page.title = "Hello, world!" +page.add(Text("Hello, world!")) diff --git a/sdk/python/examples/min-page.py b/sdk/python/examples/min-page.py new file mode 100644 index 0000000000..57269de9c1 --- /dev/null +++ b/sdk/python/examples/min-page.py @@ -0,0 +1,41 @@ +from time import sleep + +import flet +from flet import Button, Stack, Text, Textbox + +page = flet.page(port=8560) + +txt1 = Text("Text A") +# page.add(txt1) + + +st1 = Stack( + controls=[ + Text("text 1"), + Stack( + controls=[ + Text("text 3"), + Stack( + controls=[ + Text("text 5"), + txt1, + Text("text 6"), + ] + ), + Text("text 4"), + ] + ), + Text("text 2"), + ] +) +page.add(st1) + +sleep(3) + +txt1.value = "Hello!" +page.update() + +sleep(3) + +txt1.value = "Bye!" +page.update() diff --git a/sdk/python/examples/minimal-page.py b/sdk/python/examples/minimal-page.py new file mode 100644 index 0000000000..2e3a0473e2 --- /dev/null +++ b/sdk/python/examples/minimal-page.py @@ -0,0 +1,54 @@ +from time import sleep + +import flet +from flet import Button, Stack, Text, Textbox + +page = flet.page() + +txt1 = Text("Text A") +txt2 = Text("Text B") +tb1 = Textbox(label="Your name", value="John") +btn1 = Button("Click me!", on_click=lambda e: print("I'm clicked!", tb1.value)) + + +def btn2_click(e): + tb1.value = tb1.value + "A" + tb1.label = tb1.label + "B" + page.update() + + +btn2 = Button("Append", on_click=btn2_click) + +st1 = Stack( + horizontal=False, + controls=[txt1, txt2], +) +page.add(st1, btn1, btn2) + +sleep(3) + +for i in range(1, 10): + # txt1.value = f"Hello, world - {i}" + st1.controls.append(Text(f"Hello, world - {i}")) + if len(st1.controls) > 5: + st1.controls.pop(0) + page.update() + # sleep(1) + +st1.controls.append(tb1) +page.update() + +sleep(5) + +# st1.bgcolor = "red" +btn1.text = "Boo!" +page.update() + +sleep(2) + +# update text +st1.controls[1].value = "Line 2" +st1.controls[3].value = "Line 4" +page.update() + +input("Press ENTER to exit...") diff --git a/sdk/python/examples/nav.py b/sdk/python/examples/nav.py new file mode 100644 index 0000000000..0f24b5ef14 --- /dev/null +++ b/sdk/python/examples/nav.py @@ -0,0 +1,196 @@ +import flet +from flet import Message, Nav, Stack, Text, nav + + +def navs(page): + + nav1 = None + + def menu_item_expanded(e): + page.controls.insert( + 0, Message(value=f'Menu item "{e.data}" was expanded', dismiss=True) + ) + page.update() + + def menu_item_collapsed(e): + page.controls.insert( + 0, Message(value=f'Menu item "{e.data}" was collapsed', dismiss=True) + ) + page.update() + + def menu_item_changed(e): + page.controls.insert( + 0, Message(value=f'Menu item was changed to "{nav1.value}"', dismiss=True) + ) + page.update() + + nav1 = Nav( + on_collapse=menu_item_collapsed, + on_expand=menu_item_expanded, + on_change=menu_item_changed, + items=[ + nav.Item( + expanded=False, + text="Actions", + items=[ + nav.Item( + expanded=True, + text="New", + items=[ + nav.Item(key="email", text="Email message", icon="Mail"), + nav.Item( + key="calendar", + text="Calendar event", + icon="Calendar", + icon_color="salmon", + ), + ], + ), + nav.Item( + text="Share", + items=[ + nav.Item( + disabled=True, + key="share", + text="Share to Facebook", + icon="Share", + ), + nav.Item(key="twitter", text="Share to Twitter"), + ], + ), + nav.Item( + text="Links", + items=[ + nav.Item( + text="Flet website", + icon="NavigateExternalInline", + url="https://flet.dev", + new_window=True, + ), + nav.Item( + text="Google website", + icon="NavigateExternalInline", + url="https://google.com", + new_window=True, + ), + ], + ), + ], + ), + nav.Item( + expanded=True, + text="Settings", + items=[ + nav.Item( + expanded=True, + text="New", + items=[ + nav.Item(key="email", text="Email message", icon="Mail"), + nav.Item( + key="calendar", + text="Calendar event", + icon="Calendar", + icon_color="salmon", + ), + ], + ), + nav.Item( + text="Share", + items=[ + nav.Item( + disabled=True, + key="share", + text="Share to Facebook", + icon="Share", + ), + nav.Item(key="twitter", text="Share to Twitter"), + ], + ), + nav.Item( + text="Links", + items=[ + nav.Item( + text="Flet website", + icon="NavigateExternalInline", + url="https://flet.dev", + new_window=True, + ), + nav.Item( + text="Google website", + icon="NavigateExternalInline", + url="https://google.com", + new_window=True, + ), + ], + ), + ], + ), + ], + ) + + nav2 = Nav( + items=[ + nav.Item( + items=[ + nav.Item( + expanded=True, + text="New", + items=[ + nav.Item(key="email", text="Email message", icon="Mail"), + nav.Item( + key="calendar", text="Calendar event", icon="Calendar" + ), + nav.Item( + text="More options", + items=[ + nav.Item( + key="option", + text="Web component", + icon="WebComponents", + ) + ], + ), + ], + ), + nav.Item( + expanded=True, + text="Share", + items=[ + nav.Item( + key="facebook", text="Share on Facebook", icon="Share" + ), + nav.Item( + key="twitter", text="Share to Twitter", icon="Share" + ), + ], + ), + ] + ) + ] + ) + + return Stack( + gap=30, + controls=[ + Stack( + controls=[ + Text( + "Nav with groups and Expand, Collapse and Change events", + size="xLarge", + ), + nav1, + ] + ), + Stack(controls=[Text("Nav without groups", size="xLarge"), nav2]), + ], + ) + + +def main(page): + + page.title = "Nav control samples" + page.update() + page.add(navs(page)) + + +flet.app("python-nav", target=main) diff --git a/sdk/python/examples/panel.py b/sdk/python/examples/panel.py new file mode 100644 index 0000000000..c3a2692f61 --- /dev/null +++ b/sdk/python/examples/panel.py @@ -0,0 +1,33 @@ +import flet +from flet import Button, Checkbox, Panel, Text + +with flet.page("panel-custom") as page: + + def button_clicked(e): + + p.light_dismiss = light_dismiss.value + p.auto_dismiss = auto_dismiss.value + p.blocking = blocking.value + values.value = ( + f"Panel properties are: {p.light_dismiss}, {p.auto_dismiss}, {p.blocking}." + ) + p.open = True + page.update() + + values = Text() + light_dismiss = Checkbox(label="Light dismiss", value=False) + auto_dismiss = Checkbox(label="Auto-dismiss", value=True) + blocking = Checkbox(label="Blocking", value=True) + b = Button(text="Open panel", on_click=button_clicked) + page.add(light_dismiss, auto_dismiss, blocking, b, values) + + t = Text("Content goes here") + + p = Panel( + title="Panel with dismiss options", + controls=[t], + ) + + page.add(p) + + input() diff --git a/sdk/python/examples/persona.py b/sdk/python/examples/persona.py new file mode 100644 index 0000000000..7057783da1 --- /dev/null +++ b/sdk/python/examples/persona.py @@ -0,0 +1,16 @@ +import flet +from flet import Persona + +page = flet.page("persona-test") +page.add(Persona("Jack Reacher", secondary_text="Designed", size=8)) +page.add(Persona("John Smith", secondary_text="Student", size=24)) +page.add(Persona("Marry Poppins", size=32, presence="busy", hide_details=True)) +page.add( + Persona( + "Feodor", + size=32, + image_url="https://avatars.githubusercontent.com/u/5041459?s=88&v=4", + presence="online", + ) +) +page.add(Persona("Alice Brown", size=72, secondary_text="Wonderer")) diff --git a/sdk/python/examples/popen.py b/sdk/python/examples/popen.py new file mode 100644 index 0000000000..bb63696748 --- /dev/null +++ b/sdk/python/examples/popen.py @@ -0,0 +1,44 @@ +import logging +import os +import socket +import subprocess +from re import sub +from time import sleep + +logging.basicConfig(level=logging.INFO) + + +def get_free_tcp_port(): + sock = socket.socket() + sock.bind(("", 0)) + return sock.getsockname()[1] + + +print("Free TCP port", get_free_tcp_port()) + +my_env = {**os.environ, "FLET_LOG_TO_FILE": "true"} + +port = 8570 +flet_path = "flet.exe" +args = [flet_path, "server", "--attached", "--port", str(port)] + +log_level = logging.getLogger().getEffectiveLevel() +if log_level == logging.CRITICAL: + log_level = logging.FATAL + +if log_level != logging.NOTSET: + log_level_name = logging.getLevelName(log_level).lower() + args.extend(["--log-level", log_level_name]) + +subprocess.Popen( + args, + env=my_env, + # creationflags=subprocess.CREATE_NEW_PROCESS_GROUP, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, +) +print("started") + +sleep(10) + +print("finished!") diff --git a/sdk/python/examples/searchbox.py b/sdk/python/examples/searchbox.py new file mode 100644 index 0000000000..2a67b38b47 --- /dev/null +++ b/sdk/python/examples/searchbox.py @@ -0,0 +1,102 @@ +import flet +from flet import SearchBox, Stack, Text + + +def searchboxes(): + return Stack( + gap=20, + controls=[ + Stack( + horizontal=True, + gap=25, + controls=[ + Stack( + controls=[ + Text("Default searchbox", size="xLarge"), + SearchBox(), + ] + ), + Stack( + controls=[ + Text("Underlined SearchBox", size="xLarge"), + SearchBox( + underlined=True, placeholder="Search files and folders" + ), + ] + ), + ], + ), + Stack( + horizontal=True, + gap=25, + controls=[ + Stack( + controls=[ + Text("Disabled SearchBox", size="xLarge"), + SearchBox(disabled=True, placeholder="Search something..."), + SearchBox( + underlined=True, + disabled=True, + placeholder="Search something...", + ), + ] + ), + Stack( + controls=[ + Text("SearchBox with custom icon", size="xLarge"), + SearchBox( + placeholder="Filter something by", + icon="Filter", + icon_color="red", + ), + ] + ), + ], + ), + Text("SearchBox with Search, Clear and Escape events", size="xLarge"), + searchbox_with_search_clear_escape(), + Text("SearchBox with Change event", size="xLarge"), + searchbox_with_change(), + ], + ) + + +def searchbox_with_search_clear_escape(): + def enter_clicked(e): + messages.controls.append(Text(f"You have searched for {sb.value}.")) + sb.value = "" + stack.update() + + def clear_or_esc_clicked(e): + messages.controls.append(Text("You have cleared the box.")) + stack.update() + + sb = SearchBox( + placeholder="Search something and click Enter, X or Esc", + on_search=enter_clicked, + on_clear=clear_or_esc_clicked, + ) + messages = Stack() + stack = Stack(controls=[sb, messages]) + return stack + + +def searchbox_with_change(): + def searchbox_changed(e): + t.value = f"You have searched for {sb.value}." + stack.update() + + sb = SearchBox(placeholder="Search something...", on_change=searchbox_changed) + t = Text() + stack = Stack(controls=[sb, t]) + return stack + + +def main(page): + + page.title = "Searchbox control samples" + page.update() + page.add(searchboxes()) + + +flet.app("python-searchbox", target=main) diff --git a/sdk/python/examples/split.py b/sdk/python/examples/split.py new file mode 100644 index 0000000000..5a777e660d --- /dev/null +++ b/sdk/python/examples/split.py @@ -0,0 +1,67 @@ +import logging + +import flet +from flet import SplitStack, Stack, Text +from flet.button import Button + +logging.basicConfig(level=logging.DEBUG) + + +def split_resize(e): + for c in e.control.controls: + print("size", c.width if e.control.horizontal else c.height) + + +page = flet.page("split1") +page.title = "Split test" +page.horizontal_align = "stretch" +page.vertical_fill = True +st = SplitStack( + height="100%", + horizontal=True, + # gutter_color="#eee", + gutter_size=10, + on_resize=split_resize, + controls=[ + Stack(width="200", min_width="200", height="100%", controls=[Text("Column A")]), + Stack(height="100%", controls=[Text("Column B")]), + Stack( + height="100%", + width="30%", + controls=[ + SplitStack( + height="100%", + gutter_color="yellow", + gutter_hover_color="orange", + gutter_drag_color="blue", + on_resize=split_resize, + controls=[ + Stack( + width="100%", + bgcolor="lightGreen", + controls=[Text("Row A")], + ), + Stack( + width="100%", + height="200", + max_height="400", + bgcolor="lightGreen", + controls=[Text("Row B")], + ), + ], + ) + ], + ), + ], +) + + +def btn_click(e): + st.height = "90%" + st.update() + + +btn = Button("Click me!", on_click=btn_click) +page.add(btn, st) + +input() diff --git a/sdk/python/examples/stack.py b/sdk/python/examples/stack.py new file mode 100644 index 0000000000..e5ebfff0da --- /dev/null +++ b/sdk/python/examples/stack.py @@ -0,0 +1,51 @@ +import logging + +import flet +from flet import Slider, Stack, Text + +logging.basicConfig(level=logging.DEBUG) + +with flet.page("horizontal-stack-wrapping") as page: + + bg_color = "#ddddee" + page.horizontal_align = "stretch" + + def items(count): + return [ + Text( + value=i + 1, + align="center", + vertical_align="center", + width=30, + height=30, + bgcolor="BlueMagenta10", + color="white", + padding=5, + ) + for i in range(count) + ] + + def wrap_slider_change(e): + print("wrap_slider_change", e) + width = int(e.control.value) + wrap_stack.width = f"{width}%" + wrap_stack.update() + + wrap_slider = Slider( + "Change the stack width to see how child items wrap onto multiple rows:", + min=0, + max=100, + step=1, + value=100, + show_value=True, + value_format="{value}%", + on_change=wrap_slider_change, + ) + + wrap_stack = Stack( + horizontal=True, wrap=True, bgcolor=bg_color, gap=20, controls=items(10) + ) + + page.add(wrap_slider, wrap_stack) + + input() diff --git a/sdk/python/examples/text.py b/sdk/python/examples/text.py new file mode 100644 index 0000000000..5c7bc247d9 --- /dev/null +++ b/sdk/python/examples/text.py @@ -0,0 +1,31 @@ +import flet +from flet import Stack, Text + +page = flet.page("text") +page.add( + Stack( + horizontal=True, + controls=[ + Text( + "This-is-a-very-long-text", + width=50, + height=50, + border_style="double", + border_width=1, + # vertical_align="center", + block=True, + nowrap=True, + ), + Text( + "This-is-a-very-long-text", + width=50, + height=50, + border_style="double", + border_width=1, + vertical_align="center", + block=True, + nowrap=True, + ), + ], + ) +) diff --git a/sdk/python/examples/todo.py b/sdk/python/examples/todo.py new file mode 100644 index 0000000000..80fe67f31b --- /dev/null +++ b/sdk/python/examples/todo.py @@ -0,0 +1,149 @@ +import flet +from flet import Text, Stack, Textbox, Button, Checkbox, Tabs, Tab + + +class Task: + def __init__(self, app, name): + self.app = app + self.display_task = Checkbox( + value=False, label=name, on_change=self.status_changed + ) + self.edit_name = Textbox(width="100%") + self.display_view = Stack( + horizontal=True, + horizontal_align="space-between", + vertical_align="center", + controls=[ + self.display_task, + Stack( + horizontal=True, + gap="0", + controls=[ + Button( + icon="Edit", title="Edit todo", on_click=self.edit_clicked + ), + Button( + icon="Delete", + title="Delete todo", + on_click=self.delete_clicked, + ), + ], + ), + ], + ) + self.edit_view = Stack( + visible=False, + horizontal=True, + horizontal_align="space-between", + vertical_align="center", + controls=[self.edit_name, Button(text="Save", on_click=self.save_clicked)], + ) + self.view = Stack(controls=[self.display_view, self.edit_view]) + + def edit_clicked(self, e): + self.edit_name.value = self.display_task.label + self.display_view.visible = False + self.edit_view.visible = True + self.view.update() + + def save_clicked(self, e): + self.display_task.label = self.edit_name.value + self.display_view.visible = True + self.edit_view.visible = False + self.view.update() + + def delete_clicked(self, e): + self.app.delete_task(self) + + def status_changed(self, e): + self.app.update() + + +class TodoApp: + def __init__(self): + self.tasks = [] + self.new_task = Textbox(placeholder="Whats needs to be done?", width="100%") + self.tasks_view = Stack() + self.filter = Tabs( + value="all", + on_change=self.tabs_changed, + tabs=[Tab(text="all"), Tab(text="active"), Tab(text="completed")], + ) + self.items_left = Text("0 items left") + self.view = Stack( + width="70%", + controls=[ + Text(value="Todos", size="large", align="center"), + Stack( + horizontal=True, + on_submit=self.add_clicked, + controls=[ + self.new_task, + Button(primary=True, text="Add", on_click=self.add_clicked), + ], + ), + Stack( + gap=25, + controls=[ + self.filter, + self.tasks_view, + Stack( + horizontal=True, + horizontal_align="space-between", + vertical_align="center", + controls=[ + self.items_left, + Button( + text="Clear completed", on_click=self.clear_clicked + ), + ], + ), + ], + ), + ], + ) + + def update(self): + status = self.filter.value + count = 0 + for task in self.tasks: + task.view.visible = ( + status == "all" + or (status == "active" and task.display_task.value == False) + or (status == "completed" and task.display_task.value) + ) + if task.display_task.value == False: + count += 1 + self.items_left.value = f"{count} active item(s) left" + self.view.update() + + def add_clicked(self, e): + task = Task(self, self.new_task.value) + self.tasks.append(task) + self.tasks_view.controls.append(task.view) + self.new_task.value = "" + self.update() + + def delete_task(self, task): + self.tasks.remove(task) + self.tasks_view.controls.remove(task.view) + self.update() + + def tabs_changed(self, e): + self.update() + + def clear_clicked(self, e): + for task in self.tasks[:]: + if task.display_task.value == True: + self.delete_task(task) + + +def main(page): + page.title = "ToDo App" + page.horizontal_align = "center" + page.update() + app = TodoApp() + page.add(app.view) + + +flet.app("todo-app", target=main) diff --git a/sdk/python/flet/__init__.py b/sdk/python/flet/__init__.py new file mode 100644 index 0000000000..ae3cc73944 --- /dev/null +++ b/sdk/python/flet/__init__.py @@ -0,0 +1,43 @@ +from flet.barchart import BarChart +from flet.button import Button +from flet.callout import Callout +from flet.checkbox import Checkbox +from flet.choicegroup import ChoiceGroup +from flet.column import Column +from flet.combobox import ComboBox +from flet.control import Control +from flet.datepicker import DatePicker +from flet.dialog import Dialog +from flet.dropdown import Dropdown +from flet.expanded import Expanded +from flet.flet import * +from flet.form import Form +from flet.grid import Grid +from flet.html import Html +from flet.icon import Icon +from flet.iframe import IFrame +from flet.image import Image +from flet.linechart import LineChart +from flet.link import Link +from flet.message import Message, MessageButton +from flet.nav import Nav +from flet.page import Page +from flet.panel import Panel +from flet.persona import Persona +from flet.piechart import PieChart +from flet.progress import Progress +from flet.reconnecting_websocket import * +from flet.ref import Ref +from flet.row import Row +from flet.searchbox import SearchBox +from flet.slider import Slider +from flet.spinbutton import SpinButton +from flet.spinner import Spinner +from flet.splitstack import SplitStack +from flet.stack import Stack +from flet.tabs import Tab, Tabs +from flet.text import Text +from flet.textbox import Textbox +from flet.toggle import Toggle +from flet.toolbar import Toolbar +from flet.verticalbarchart import VerticalBarChart diff --git a/sdk/python/flet/barchart.py b/sdk/python/flet/barchart.py new file mode 100644 index 0000000000..c10797c0d2 --- /dev/null +++ b/sdk/python/flet/barchart.py @@ -0,0 +1,187 @@ +from typing import Optional, Union + +from beartype import beartype + +from flet.control import Control + +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal + + +DataMode = Literal["default", "fraction", "percentage", None] + + +class BarChart(Control): + def __init__( + self, + id=None, + ref=None, + tooltips=None, + data_mode: DataMode = None, + points=None, + width=None, + height=None, + padding=None, + margin=None, + visible=None, + disabled=None, + ): + + Control.__init__( + self, + id=id, + ref=ref, + width=width, + height=height, + padding=padding, + margin=margin, + visible=visible, + disabled=disabled, + ) + + self.__data = Data(points=points) + self.tooltips = tooltips + self.data_mode = data_mode + + def _get_control_name(self): + return "barchart" + + # points + @property + def points(self): + return self.__data.points + + @points.setter + def points(self, value): + self.__data.points = value + + # tooltips + @property + def tooltips(self): + return self._get_attr("tooltips", data_type="bool", def_value=False) + + @tooltips.setter + @beartype + def tooltips(self, value: Optional[bool]): + self._set_attr("tooltips", value) + + # data_mode + @property + def data_mode(self): + return self._get_attr("dataMode") + + @data_mode.setter + @beartype + def data_mode(self, value: DataMode): + self._set_attr("dataMode", value) + + def _get_children(self): + return [self.__data] + + +class Data(Control): + def __init__(self, id=None, ref=None, points=None): + Control.__init__(self, id=id, ref=ref) + + self.__points = [] + if points != None: + for point in points: + self.__points.append(point) + + # points + @property + def points(self): + return self.__points + + @points.setter + def points(self, value): + self.__points = value + + def _get_control_name(self): + return "data" + + def _get_children(self): + return self.__points + + +class Point(Control): + def __init__( + self, + id=None, + ref=None, + x=None, + y=None, + legend=None, + color=None, + x_tooltip=None, + y_tooltip=None, + ): + Control.__init__(self, id=id, ref=ref) + + self.x = x + self.y = y + self.legend = legend + self.color = color + self.x_tooltip = x_tooltip + self.y_tooltip = y_tooltip + + def _get_control_name(self): + return "p" + + # x + @property + def x(self): + return self._get_attr("x") + + @x.setter + @beartype + def x(self, value: Union[None, int, float]): + self._set_attr("x", value) + + # y + @property + def y(self): + return self._get_attr("y") + + @y.setter + @beartype + def y(self, value: Union[None, int, float]): + self._set_attr("y", value) + + # legend + @property + def legend(self): + return self._get_attr("legend") + + @legend.setter + def legend(self, value): + self._set_attr("legend", value) + + # color + @property + def color(self): + return self._get_attr("color") + + @color.setter + def color(self, value): + self._set_attr("color", value) + + # x_tooltip + @property + def x_tooltip(self): + return self._get_attr("xTooltip") + + @x_tooltip.setter + def x_tooltip(self, value): + self._set_attr("xTooltip", value) + + # y_tooltip + @property + def y_tooltip(self): + return self._get_attr("yTooltip") + + @y_tooltip.setter + def y_tooltip(self, value): + self._set_attr("yTooltip", value) diff --git a/sdk/python/flet/button.py b/sdk/python/flet/button.py new file mode 100644 index 0000000000..ea502cf02c --- /dev/null +++ b/sdk/python/flet/button.py @@ -0,0 +1,398 @@ +from typing import Optional + +from beartype import beartype + +from flet.control import Control + + +class Button(Control): + def __init__( + self, + text=None, + id=None, + ref=None, + primary=None, + compound=None, + action=None, + toolbar=None, + split=None, + secondary_text=None, + url=None, + new_window=None, + title=None, + icon=None, + icon_color=None, + focused=None, + data=None, + on_click=None, + on_focus=None, + on_blur=None, + menu_items=None, + width=None, + height=None, + padding=None, + margin=None, + visible=None, + disabled=None, + ): + Control.__init__( + self, + id=id, + ref=ref, + width=width, + height=height, + padding=padding, + margin=margin, + visible=visible, + disabled=disabled, + data=data, + ) + + self.primary = primary + self.compound = compound + self.action = action + self.toolbar = toolbar + self.split = split + self.text = text + self.secondary_text = secondary_text + self.url = url + self.new_window = new_window + self.title = title + self.icon = icon + self.icon_color = icon_color + self.focused = focused + self.on_click = on_click + self.on_focus = on_focus + self.on_blur = on_blur + self.__menu_items = [] + if menu_items != None: + for item in menu_items: + self.__menu_items.append(item) + + def _get_control_name(self): + return "button" + + # menu_items + @property + def menu_items(self): + return self.__menu_items + + @menu_items.setter + def menu_items(self, value): + self.__menu_items = value + + # on_click + @property + def on_click(self): + return self._get_event_handler("click") + + @on_click.setter + def on_click(self, handler): + self._add_event_handler("click", handler) + + # primary + @property + def primary(self): + return self._get_attr("primary", data_type="bool", def_value=False) + + @primary.setter + @beartype + def primary(self, value: Optional[bool]): + self._set_attr("primary", value) + + # compound + @property + def compound(self): + return self._get_attr("compound", data_type="bool", def_value=False) + + @compound.setter + @beartype + def compound(self, value: Optional[bool]): + self._set_attr("compound", value) + + # action + @property + def action(self): + return self._get_attr("action", data_type="bool", def_value=False) + + @action.setter + @beartype + def action(self, value: Optional[bool]): + self._set_attr("action", value) + + # toolbar + @property + def toolbar(self): + return self._get_attr("toolbar", data_type="bool", def_value=False) + + @toolbar.setter + @beartype + def toolbar(self, value: Optional[bool]): + self._set_attr("toolbar", value) + + # split + @property + def split(self): + return self._get_attr("split", data_type="bool", def_value=False) + + @split.setter + @beartype + def split(self, value: Optional[bool]): + self._set_attr("split", value) + + # text + @property + def text(self): + return self._get_attr("text") + + @text.setter + def text(self, value): + self._set_attr("text", value) + + # secondary_text + @property + def secondary_text(self): + return self._get_attr("secondaryText") + + @secondary_text.setter + def secondary_text(self, value): + self._set_attr("secondaryText", value) + + # url + @property + def url(self): + return self._get_attr("url") + + @url.setter + def url(self, value): + self._set_attr("url", value) + + # new_window + @property + def new_window(self): + return self._get_attr("newWindow", data_type="bool", def_value=False) + + @new_window.setter + @beartype + def new_window(self, value: Optional[bool]): + self._set_attr("newWindow", value) + + # title + @property + def title(self): + return self._get_attr("title") + + @title.setter + def title(self, value): + self._set_attr("title", value) + + # icon + @property + def icon(self): + return self._get_attr("icon") + + @icon.setter + def icon(self, value): + self._set_attr("icon", value) + + # icon_color + @property + def icon_color(self): + return self._get_attr("iconColor") + + @icon_color.setter + def icon_color(self, value): + self._set_attr("iconColor", value) + + def _get_children(self): + return self.__menu_items + + # focused + @property + def focused(self): + return self._get_attr("focused", data_type="bool", def_value=False) + + @focused.setter + @beartype + def focused(self, value: Optional[bool]): + self._set_attr("focused", value) + + # on_focus + @property + def on_focus(self): + return self._get_event_handler("focus") + + @on_focus.setter + def on_focus(self, handler): + self._add_event_handler("focus", handler) + + # on_blur + @property + def on_blur(self): + return self._get_event_handler("blur") + + @on_blur.setter + def on_blur(self, handler): + self._add_event_handler("blur", handler) + + +class MenuItem(Control): + def __init__( + self, + text=None, + id=None, + ref=None, + secondary_text=None, + url=None, + new_window=None, + icon=None, + icon_color=None, + icon_only=None, + split=None, + divider=None, + on_click=None, + sub_menu_items=None, + width=None, + height=None, + padding=None, + margin=None, + visible=None, + disabled=None, + data=None, + ): + Control.__init__( + self, + id=id, + ref=ref, + width=width, + height=height, + padding=padding, + margin=margin, + visible=visible, + disabled=disabled, + data=data, + ) + + self.text = text + self.secondary_text = secondary_text + self.url = url + self.new_window = new_window + self.icon = icon + self.icon_color = icon_color + self.icon_only = icon_only + self.split = split + self.divider = divider + self.on_click = on_click + self.__sub_menu_items = [] + if sub_menu_items != None: + for item in sub_menu_items: + self.__sub_menu_items.append(item) + + def _get_control_name(self): + return "item" + + # on_click + @property + def on_click(self): + return self._get_event_handler("click") + + @on_click.setter + def on_click(self, handler): + self._add_event_handler("click", handler) + + # sub_menu_items + @property + def sub_menu_items(self): + return self.__sub_menu_items + + @sub_menu_items.setter + def sub_menu_items(self, value): + self.__sub_menu_items = value + + # text + @property + def text(self): + return self._get_attr("text") + + @text.setter + def text(self, value): + self._set_attr("text", value) + + # secondary_text + @property + def secondary_text(self): + return self._get_attr("secondaryText") + + @secondary_text.setter + def secondary_text(self, value): + self._set_attr("secondaryText", value) + + # url + @property + def url(self): + return self._get_attr("url") + + @url.setter + def url(self, value): + self._set_attr("url", value) + + # new_window + @property + def new_window(self): + return self._get_attr("newWindow", data_type="bool", def_value=False) + + @new_window.setter + @beartype + def new_window(self, value: Optional[bool]): + self._set_attr("newWindow", value) + + # icon + @property + def icon(self): + return self._get_attr("icon") + + @icon.setter + def icon(self, value): + self._set_attr("icon", value) + + # icon_color + @property + def icon_color(self): + return self._get_attr("iconColor") + + @icon_color.setter + def icon_color(self, value): + self._set_attr("iconColor", value) + + # icon_only + @property + def icon_only(self): + return self._get_attr("iconOnly", data_type="bool", def_value=False) + + @icon_only.setter + @beartype + def icon_only(self, value: Optional[bool]): + self._set_attr("iconOnly", value) + + # split + @property + def split(self): + return self._get_attr("split", data_type="bool", def_value=False) + + @split.setter + @beartype + def split(self, value: Optional[bool]): + self._set_attr("split", value) + + # divider + @property + def divider(self): + return self._get_attr("divider", data_type="bool", def_value=False) + + @divider.setter + @beartype + def divider(self, value: Optional[bool]): + self._set_attr("divider", value) + + def _get_children(self): + return self.__sub_menu_items diff --git a/sdk/python/flet/callout.py b/sdk/python/flet/callout.py new file mode 100644 index 0000000000..a0151fd656 --- /dev/null +++ b/sdk/python/flet/callout.py @@ -0,0 +1,182 @@ +from typing import Optional + +from beartype import beartype + +from flet.control import Control + +try: + from typing import Literal +except: + from typing_extensions import Literal + + +Position = Literal[ + None, + "topLeft", + "topCenter", + "topRight", + "topAuto", + "bottomLeft", + "bottomCenter", + "bottomRight", + "bottomAuto", + "leftTop", + "leftCenter", + "leftBottom", + "rightTop", + "rightCenter", + "rightBottom", +] + + +class Callout(Control): + def __init__( + self, + id=None, + ref=None, + target=None, + position: Position = None, + gap=None, + beak=None, + beak_width=None, + page_padding=None, + focus=None, + cover=None, + visible=None, + controls=None, + on_dismiss=None, + width=None, + height=None, + padding=None, + margin=None, + disabled=None, + ): + + Control.__init__( + self, + id=id, + ref=ref, + width=width, + height=height, + padding=padding, + margin=margin, + visible=visible, + disabled=disabled, + ) + + self.target = target + self.position = position + self.gap = gap + self.beak = beak + self.beak_width = beak_width + self.page_padding = page_padding + self.focus = focus + self.cover = cover + self.on_dismiss = on_dismiss + self.__controls = [] + if controls != None: + for control in controls: + self.__controls.append(control) + + def _get_control_name(self): + return "callout" + + # controls + @property + def controls(self): + return self.__controls + + @controls.setter + def controls(self, value): + self.__controls = value + + # on_dismiss + @property + def on_dismiss(self): + return self._get_event_handler("dismiss") + + @on_dismiss.setter + def on_dismiss(self, handler): + self._add_event_handler("dismiss", handler) + + # target + @property + def target(self): + return self._get_attr("target") + + @target.setter + def target(self, value): + self._set_attr("target", value) + + # position + @property + def position(self): + return self._get_attr("position") + + @position.setter + @beartype + def position(self, value: Position): + self._set_attr("position", value) + + # gap + @property + def gap(self): + return self._get_attr("gap") + + @gap.setter + @beartype + def gap(self, value: Optional[int]): + self._set_attr("gap", value) + + # beak + @property + def beak(self): + return self._get_attr("beak", data_type="bool", def_value=True) + + @beak.setter + @beartype + def beak(self, value: Optional[bool]): + self._set_attr("beak", value) + + # beak_width + @property + def beak_width(self): + return self._get_attr("beakWidth") + + @beak_width.setter + @beartype + def beak_width(self, value: Optional[int]): + self._set_attr("beakWidth", value) + + # page_padding + @property + def page_padding(self): + return self._get_attr("pagePadding") + + @page_padding.setter + @beartype + def page_padding(self, value: Optional[int]): + self._set_attr("pagePadding", value) + + # focus + @property + def focus(self): + return self._get_attr("focus", data_type="bool", def_value=False) + + @focus.setter + @beartype + def focus(self, value: Optional[bool]): + self._set_attr("focus", value) + + # cover + @property + def cover(self): + return self._get_attr("cover", data_type="bool", def_value=False) + + @cover.setter + @beartype + def cover(self, value: Optional[bool]): + self._set_attr("cover", value) + + def _get_children(self): + return self.__controls diff --git a/sdk/python/flet/checkbox.py b/sdk/python/flet/checkbox.py new file mode 100644 index 0000000000..31dc99e586 --- /dev/null +++ b/sdk/python/flet/checkbox.py @@ -0,0 +1,113 @@ +from typing import Optional + +from beartype import beartype + +from flet.control import Control + +try: + from typing import Literal +except: + from typing_extensions import Literal + + +BoxSide = Literal[None, "start", "end"] + + +class Checkbox(Control): + def __init__( + self, + label=None, + id=None, + ref=None, + value=None, + value_field=None, + box_side: BoxSide = None, + focused=None, + data=None, + width=None, + height=None, + padding=None, + margin=None, + on_change=None, + visible=None, + disabled=None, + ): + Control.__init__( + self, + id=id, + ref=ref, + width=width, + height=height, + padding=padding, + margin=margin, + visible=visible, + disabled=disabled, + data=data, + ) + self.value = value + self.value_field = value_field + self.label = label + self.box_side = box_side + self.focused = focused + self.on_change = on_change + + def _get_control_name(self): + return "checkbox" + + # on_change + @property + def on_change(self): + return self._get_event_handler("change") + + @on_change.setter + def on_change(self, handler): + self._add_event_handler("change", handler) + + # value + @property + def value(self): + return self._get_attr("value", data_type="bool", def_value=False) + + @value.setter + @beartype + def value(self, value: Optional[bool]): + self._set_attr("value", value) + + # value_field + @property + def value_field(self): + return self._get_attr("value") + + @value_field.setter + def value_field(self, value): + if value != None: + self._set_attr("value", f"{{{value}}}") + + # label + @property + def label(self): + return self._get_attr("label") + + @label.setter + def label(self, value): + self._set_attr("label", value) + + # box_side + @property + def box_side(self): + return self._get_attr("boxSide") + + @box_side.setter + @beartype + def box_side(self, value: BoxSide): + self._set_attr("boxSide", value) + + # focused + @property + def focused(self): + return self._get_attr("focused", data_type="bool", def_value=False) + + @focused.setter + @beartype + def focused(self, value: Optional[bool]): + self._set_attr("focused", value) diff --git a/sdk/python/flet/choicegroup.py b/sdk/python/flet/choicegroup.py new file mode 100644 index 0000000000..7246806f24 --- /dev/null +++ b/sdk/python/flet/choicegroup.py @@ -0,0 +1,169 @@ +from typing import Optional + +from beartype import beartype + +from flet.control import Control + + +class ChoiceGroup(Control): + def __init__( + self, + label=None, + id=None, + ref=None, + value=None, + data=None, + options=None, + width=None, + height=None, + padding=None, + margin=None, + focused=None, + on_change=None, + on_focus=None, + on_blur=None, + visible=None, + disabled=None, + ): + Control.__init__( + self, + id=id, + ref=ref, + width=width, + height=height, + padding=padding, + margin=margin, + visible=visible, + disabled=disabled, + data=data, + ) + self.value = value + self.label = label + self.focused = focused + self.on_change = on_change + self.on_focus = on_focus + self.on_blur = on_blur + self.__options = [] + if options != None: + for option in options: + self.__options.append(option) + + def _get_control_name(self): + return "choicegroup" + + # options + @property + def options(self): + return self.__options + + @options.setter + def options(self, value): + self.__options = value + + # on_change + @property + def on_change(self): + return self._get_event_handler("change") + + @on_change.setter + def on_change(self, handler): + self._add_event_handler("change", handler) + + # value + @property + def value(self): + return self._get_attr("value") + + @value.setter + def value(self, value): + self._set_attr("value", value) + + # label + @property + def label(self): + return self._get_attr("label") + + @label.setter + def label(self, value): + self._set_attr("label", value) + + def _get_children(self): + return self.__options + + # focused + @property + def focused(self): + return self._get_attr("focused", data_type="bool", def_value=False) + + @focused.setter + @beartype + def focused(self, value: Optional[bool]): + self._set_attr("focused", value) + + # on_focus + @property + def on_focus(self): + return self._get_event_handler("focus") + + @on_focus.setter + def on_focus(self, handler): + self._add_event_handler("focus", handler) + + # on_blur + @property + def on_blur(self): + return self._get_event_handler("blur") + + @on_blur.setter + def on_blur(self, handler): + self._add_event_handler("blur", handler) + + +class Option(Control): + def __init__(self, key=None, text=None, icon=None, icon_color=None, ref=None): + Control.__init__(self, ref=ref) + assert key != None or text != None, "key or text must be specified" + + self.key = key + self.text = text + self.icon = icon + self.icon_color = icon_color + + def _get_control_name(self): + return "option" + + # key + @property + def key(self): + return self._get_attr("key") + + @key.setter + def key(self, value): + self._set_attr("key", value) + + # text + @property + def text(self): + return self._get_attr("text") + + @text.setter + def text(self, value): + self._set_attr("text", value) + + # icon + @property + def icon(self): + return self._get_attr("icon") + + @icon.setter + def icon(self, value): + self._set_attr("icon", value) + + # icon_color + @property + def icon_color(self): + return self._get_attr("iconColor") + + @icon_color.setter + def icon_color(self, value): + self._set_attr("iconColor", value) diff --git a/sdk/python/flet/column.py b/sdk/python/flet/column.py new file mode 100644 index 0000000000..a2d1cfcffe --- /dev/null +++ b/sdk/python/flet/column.py @@ -0,0 +1,49 @@ +from typing import Optional + +from beartype import beartype + +from flet.control import Control + + +class Column(Control): + def __init__( + self, + controls=None, + id=None, + ref=None, + visible=None, + disabled=None, + data=None, + ): + Control.__init__( + self, + id=id, + ref=ref, + visible=visible, + disabled=disabled, + data=data, + ) + + self.__controls = [] + if controls != None: + for control in controls: + self.__controls.append(control) + + def _get_control_name(self): + return "column" + + def clean(self): + Control.clean(self) + self.__controls.clear() + + # controls + @property + def controls(self): + return self.__controls + + @controls.setter + def controls(self, value): + self.__controls = value + + def _get_children(self): + return self.__controls diff --git a/sdk/python/flet/combobox.py b/sdk/python/flet/combobox.py new file mode 100644 index 0000000000..6f0acac490 --- /dev/null +++ b/sdk/python/flet/combobox.py @@ -0,0 +1,230 @@ +from beartype.typing import List, Optional, Union + +from beartype import beartype + +from flet.control import Control + +try: + from typing import Literal +except: + from typing_extensions import Literal + + +ItemType = Literal[None, "normal", "divider", "header", "selectAll", "select_all"] +ComboBoxValue = Union[None, str, List[str]] + + +class ComboBox(Control): + def __init__( + self, + label=None, + id=None, + ref=None, + value: ComboBoxValue = None, + placeholder=None, + error_message=None, + on_change=None, + on_focus=None, + on_blur=None, + options=None, + width=None, + height=None, + padding=None, + margin=None, + visible=None, + disabled=None, + focused=None, + multi_select=None, + allow_free_form=None, + auto_complete=None, + data=None, + ): + Control.__init__( + self, + id=id, + ref=ref, + width=width, + height=height, + padding=padding, + margin=margin, + visible=visible, + disabled=disabled, + data=data, + ) + self.label = label + self.value = value + self.placeholder = placeholder + self.error_message = error_message + self.focused = focused + self.multi_select = multi_select + self.allow_free_form = allow_free_form + self.auto_complete = auto_complete + self.on_change = on_change + self.on_focus = on_focus + self.on_blur = on_blur + self.__options = [] + if options != None: + for option in options: + self.__options.append(option) + + def _get_control_name(self): + return "combobox" + + # options + @property + def options(self): + return self.__options + + @options.setter + def options(self, value): + self.__options = value + + # on_change + @property + def on_change(self): + return self._get_event_handler("change") + + @on_change.setter + def on_change(self, handler): + self._add_event_handler("change", handler) + + # label + @property + def label(self): + return self._get_attr("label") + + @label.setter + def label(self, value): + self._set_attr("label", value) + + # value + @property + def value(self): + return self._get_value_or_list_attr("value", ",") + + @value.setter + @beartype + def value(self, value: ComboBoxValue): + self._set_value_or_list_attr("value", value, ",") + + # placeholder + @property + def placeholder(self): + return self._get_attr("placeholder") + + @placeholder.setter + def placeholder(self, value): + self._set_attr("placeholder", value) + + # error_message + @property + def error_message(self): + return self._get_attr("errorMessage") + + @error_message.setter + def error_message(self, value): + self._set_attr("errorMessage", value) + + def _get_children(self): + return self.__options + + # focused + @property + def focused(self): + return self._get_attr("focused", data_type="bool", def_value=False) + + @focused.setter + @beartype + def focused(self, value: Optional[bool]): + self._set_attr("focused", value) + + # multi_select + @property + def multi_select(self): + return self._get_attr("multiselect", data_type="bool", def_value=False) + + @multi_select.setter + @beartype + def multi_select(self, value: Optional[bool]): + self._set_attr("multiselect", value) + + # allow_free_form + @property + def allow_free_form(self): + return self._get_attr("allowfreeform", data_type="bool", def_value=False) + + @allow_free_form.setter + @beartype + def allow_free_form(self, value: Optional[bool]): + self._set_attr("allowfreeform", value) + + # auto_complete + @property + def auto_complete(self): + return self._get_attr("autocomplete", data_type="bool", def_value=True) + + @auto_complete.setter + @beartype + def auto_complete(self, value: Optional[bool]): + self._set_attr("autocomplete", value) + + # on_focus + @property + def on_focus(self): + return self._get_event_handler("focus") + + @on_focus.setter + def on_focus(self, handler): + self._add_event_handler("focus", handler) + + # on_blur + @property + def on_blur(self): + return self._get_event_handler("blur") + + @on_blur.setter + def on_blur(self, handler): + self._add_event_handler("blur", handler) + + +class Option(Control): + def __init__( + self, key=None, text=None, item_type: ItemType = None, disabled=None, ref=None + ): + Control.__init__(self, ref=ref, disabled=disabled) + assert key != None or text != None, "key or text must be specified" + self.key = key + self.text = text + self.item_type = item_type + self.disabled = disabled + + def _get_control_name(self): + return "option" + + # key + @property + def key(self): + return self._get_attr("key") + + @key.setter + def key(self, value): + self._set_attr("key", value) + + # text + @property + def text(self): + return self._get_attr("text") + + @text.setter + def text(self, value): + self._set_attr("text", value) + + # item_type + @property + def item_type(self): + return self._get_attr("itemtype") + + @item_type.setter + @beartype + def item_type(self, value: ItemType): + self._set_attr("itemtype", value) diff --git a/sdk/python/flet/connection.py b/sdk/python/flet/connection.py new file mode 100644 index 0000000000..f1c7d7d30a --- /dev/null +++ b/sdk/python/flet/connection.py @@ -0,0 +1,124 @@ +import json +import logging +import threading +import uuid + +from flet.protocol import * +from flet.reconnecting_websocket import ReconnectingWebSocket + + +class Connection: + def __init__(self, ws: ReconnectingWebSocket): + self._ws = ws + self._ws.on_message = self._on_message + self._ws_callbacks = {} + self._on_event = None + self._on_session_created = None + self.host_client_id = None + self.page_name = None + self.page_url = None + self.browser_opened = False + self.sessions = {} + + @property + def on_event(self): + return self._on_event + + @on_event.setter + def on_event(self, handler): + self._on_event = handler + + @property + def on_session_created(self): + return self._on_session_created + + @on_session_created.setter + def on_session_created(self, handler): + self._on_session_created = handler + + def _on_message(self, data): + logging.debug(f"_on_message: {data}") + msg_dict = json.loads(data) + msg = Message(**msg_dict) + if msg.id != "": + # callback + evt = self._ws_callbacks[msg.id][0] + self._ws_callbacks[msg.id] = (None, msg.payload) + evt.set() + elif msg.action == Actions.PAGE_EVENT_TO_HOST: + if self._on_event != None: + th = threading.Thread( + target=self._on_event, + args=( + self, + PageEventPayload(**msg.payload), + ), + daemon=True, + ) + th.start() + # self._on_event(self, PageEventPayload(**msg.payload)) + elif msg.action == Actions.SESSION_CREATED: + if self._on_session_created != None: + th = threading.Thread( + target=self._on_session_created, + args=( + self, + PageSessionCreatedPayload(**msg.payload), + ), + daemon=True, + ) + th.start() + else: + # it's something else + print(msg.payload) + + def register_host_client( + self, + host_client_id: str, + page_name: str, + is_app: bool, + update: bool, + auth_token: str, + permissions: str, + ): + payload = RegisterHostClientRequestPayload( + host_client_id, page_name, is_app, update, auth_token, permissions + ) + response = self._send_message_with_result(Actions.REGISTER_HOST_CLIENT, payload) + return RegisterHostClientResponsePayload(**response) + + def send_command(self, page_name: str, session_id: str, command: Command): + payload = PageCommandRequestPayload(page_name, session_id, command) + response = self._send_message_with_result( + Actions.PAGE_COMMAND_FROM_HOST, payload + ) + result = PageCommandResponsePayload(**response) + if result.error != "": + raise Exception(result.error) + return result + + def send_commands(self, page_name: str, session_id: str, commands: List[Command]): + payload = PageCommandsBatchRequestPayload(page_name, session_id, commands) + response = self._send_message_with_result( + Actions.PAGE_COMMANDS_BATCH_FROM_HOST, payload + ) + result = PageCommandsBatchResponsePayload(**response) + if result.error != "": + raise Exception(result.error) + return result + + def _send_message_with_result(self, action_name, payload): + msg_id = uuid.uuid4().hex + msg = Message(msg_id, action_name, payload) + j = json.dumps(msg, default=vars) + logging.debug(f"_send_message_with_result: {j}") + evt = threading.Event() + self._ws_callbacks[msg_id] = (evt, None) + self._ws.send(j) + evt.wait() + return self._ws_callbacks.pop(msg_id)[1] + + def close(self): + logging.debug("Closing connection...") + if self._ws != None: + self._ws.close() diff --git a/sdk/python/flet/constants.py b/sdk/python/flet/constants.py new file mode 100644 index 0000000000..df119b68a3 --- /dev/null +++ b/sdk/python/flet/constants.py @@ -0,0 +1,4 @@ +INDEX_PAGE = "p/index" +HOSTED_SERVICE_URL = "https://app.flet.dev" +CONNECT_TIMEOUT_SECONDS = 30 +ZERO_SESSION = "0" diff --git a/sdk/python/flet/control.py b/sdk/python/flet/control.py new file mode 100644 index 0000000000..65f65f693a --- /dev/null +++ b/sdk/python/flet/control.py @@ -0,0 +1,412 @@ +import datetime as dt +import threading +from difflib import SequenceMatcher +from beartype.typing import List, Optional, Union + +from beartype import beartype + +from flet.protocol import Command +from flet.ref import Ref + +try: + from typing import Literal +except: + from typing_extensions import Literal + + +BorderStyles = Literal[ + "none", + "hidden", + "dotted", + "dashed", + "solid", + "double", + "groove", + "ridge", + "inset", + "outset", +] + +BorderStyle = Union[None, BorderStyles, List[BorderStyles]] +BorderWidth = Union[None, str, int, float, List[str], List[int], List[float]] +BorderColor = Union[None, str, List[str]] +BorderRadius = Union[None, str, int, float, List[str], List[int], List[float]] + +TextSize = Literal[ + None, + "tiny", + "xSmall", + "small", + "smallPlus", + "medium", + "mediumPlus", + "large", + "xLarge", + "xxLarge", + "superLarge", + "mega", +] + +TextAlign = Literal[None, "left", "right", "center", "justify"] + + +class Control: + def __init__( + self, + id=None, + ref: Ref = None, + width=None, + height=None, + padding=None, + margin=None, + visible=None, + disabled=None, + data=None, + ): + self.__page = None + self.__attrs = {} + self.__previous_children = [] + self.id = id + self.__uid = None + if id == "page": + self.__uid = "page" + self.width = width + self.height = height + self.padding = padding + self.margin = margin + self.visible = visible + self.disabled = disabled + self.data = data + self.__event_handlers = {} + self._lock = threading.Lock() + if ref: + ref.current = self + + def _assign(self, variable): + variable = self + + def _get_children(self): + return [] + + def _get_control_name(self): + raise Exception("_getControlName must be overridden in inherited class") + + def _add_event_handler(self, event_name, handler): + self.__event_handlers[event_name] = handler + + def _get_event_handler(self, event_name): + return self.__event_handlers.get(event_name) + + def _get_attr(self, name, def_value=None, data_type="string"): + name = name.lower() + if not name in self.__attrs: + return def_value + + s_val = self.__attrs[name][0] + if data_type == "bool" and s_val != None and isinstance(s_val, str): + return s_val.lower() == "true" + elif data_type == "float" and s_val != None and isinstance(s_val, str): + return float(s_val) + else: + return s_val + + def _set_attr(self, name, value, dirty=True): + self._set_attr_internal(name, value, dirty) + + def _get_value_or_list_attr(self, name, delimiter): + v = self._get_attr(name) + if v and delimiter in v: + return [x.strip() for x in v.split(delimiter)] + return v + + def _set_value_or_list_attr(self, name, value, delimiter): + if isinstance(value, List): + value = delimiter.join([str(x) for x in value]) + self._set_attr(name, value) + + def _set_attr_internal(self, name, value, dirty=True): + name = name.lower() + orig_val = self.__attrs.get(name) + + if orig_val == None and value == None: + return + + if value == None: + value = "" + + if orig_val == None or orig_val[0] != value: + self.__attrs[name] = (value, dirty) + + # event_handlers + @property + def event_handlers(self): + return self.__event_handlers + + # _previous_children + @property + def _previous_children(self): + return self.__previous_children + + # page + @property + def page(self): + return self.__page + + @page.setter + def page(self, page): + self.__page = page + + # id + @property + def id(self): + return self._get_attr("id") + + # uid + @property + def uid(self): + return self.__uid + + @id.setter + def id(self, value): + self._set_attr("id", value) + + # width + @property + def width(self): + return self._get_attr("width") + + @width.setter + def width(self, value): + self._set_attr("width", value) + + # height + @property + def height(self): + return self._get_attr("height") + + @height.setter + def height(self, value): + self._set_attr("height", value) + + # padding + @property + def padding(self): + return self._get_attr("padding") + + @padding.setter + def padding(self, value): + self._set_attr("padding", value) + + # margin + @property + def margin(self): + return self._get_attr("margin") + + @margin.setter + def margin(self, value): + self._set_attr("margin", value) + + # visible + @property + def visible(self): + return self._get_attr("visible", data_type="bool", def_value=True) + + @visible.setter + @beartype + def visible(self, value: Optional[bool]): + self._set_attr("visible", value) + + # disabled + @property + def disabled(self): + return self._get_attr("disabled", data_type="bool", def_value=False) + + @disabled.setter + @beartype + def disabled(self, value: Optional[bool]): + self._set_attr("disabled", value) + + # data + @property + def data(self): + return self._get_attr("data") + + @data.setter + def data(self, value): + self._set_attr("data", value) + + # public methods + def update(self): + if not self.__page: + raise Exception("Control must be added to the page first.") + self.__page.update(self) + + def clean(self): + with self._lock: + self._previous_children.clear() + for child in self._get_children(): + self._remove_control_recursively(self.__page.index, child) + return self.__page._send_command("clean", [self.uid]) + + def build_update_commands(self, index, added_controls, commands): + update_cmd = self._get_cmd_attrs(update=True) + + if len(update_cmd.attrs) > 0: + update_cmd.name = "set" + commands.append(update_cmd) + + # go through children + previous_children = self.__previous_children + current_children = self._get_children() + + hashes = {} + previous_ints = [] + current_ints = [] + + for ctrl in previous_children: + hashes[hash(ctrl)] = ctrl + previous_ints.append(hash(ctrl)) + + for ctrl in current_children: + hashes[hash(ctrl)] = ctrl + current_ints.append(hash(ctrl)) + + # print("previous_ints:", previous_ints) + # print("current_ints:", current_ints) + + sm = SequenceMatcher(None, previous_ints, current_ints) + + n = 0 + for tag, a1, a2, b1, b2 in sm.get_opcodes(): + if tag == "delete": + # deleted controls + ids = [] + for h in previous_ints[a1:a2]: + ctrl = hashes[h] + self._remove_control_recursively(index, ctrl) + ids.append(ctrl.__uid) + commands.append(Command(0, "remove", ids, None, None, None)) + elif tag == "equal": + # unchanged control + for h in previous_ints[a1:a2]: + ctrl = hashes[h] + ctrl.build_update_commands(index, added_controls, commands) + n += 1 + elif tag == "replace": + ids = [] + for h in previous_ints[a1:a2]: + # delete + ctrl = hashes[h] + self._remove_control_recursively(index, ctrl) + ids.append(ctrl.__uid) + commands.append(Command(0, "remove", ids, None, None, None)) + for h in current_ints[b1:b2]: + # add + ctrl = hashes[h] + innerCmds = ctrl.get_cmd_str( + index=index, added_controls=added_controls + ) + commands.append( + Command( + 0, + "add", + None, + {"to": self.__uid, "at": str(n)}, + None, + innerCmds, + ) + ) + n += 1 + elif tag == "insert": + # add + for h in current_ints[b1:b2]: + ctrl = hashes[h] + innerCmds = ctrl.get_cmd_str( + index=index, added_controls=added_controls + ) + commands.append( + Command( + 0, + "add", + None, + {"to": self.__uid, "at": str(n)}, + None, + innerCmds, + ) + ) + n += 1 + + self.__previous_children.clear() + self.__previous_children.extend(current_children) + + def _remove_control_recursively(self, index, control): + for child in control._get_children(): + self._remove_control_recursively(index, child) + + if control.__uid in index: + del index[control.__uid] + + # private methods + def get_cmd_str(self, indent=0, index=None, added_controls=None): + + # remove control from index + if self.__uid and index != None and self.__uid in index: + del index[self.__uid] + + commands = [] + + # main command + command = self._get_cmd_attrs(False) + command.indent = indent + command.values.append(self._get_control_name()) + commands.append(command) + + if added_controls != None: + added_controls.append(self) + + # controls + children = self._get_children() + for control in children: + childCmd = control.get_cmd_str( + indent=indent + 2, index=index, added_controls=added_controls + ) + commands.extend(childCmd) + + self.__previous_children.clear() + self.__previous_children.extend(children) + + return commands + + def _get_cmd_attrs(self, update=False): + command = Command(0, None, [], {}, [], []) + + if update and not self.__uid: + return command + + for attrName in sorted(self.__attrs): + attrName = attrName.lower() + dirty = self.__attrs[attrName][1] + + if (update and not dirty) or attrName == "id": + continue + + val = self.__attrs[attrName][0] + sval = "" + if val == None: + continue + elif isinstance(val, bool): + sval = str(val).lower() + elif isinstance(val, dt.datetime) or isinstance(val, dt.date): + sval = val.isoformat() + else: + sval = str(val) + command.attrs[attrName] = sval + self.__attrs[attrName] = (val, False) + + id = self.__attrs.get("id") + if not update and id != None: + command.attrs["id"] = id + elif update and len(command.attrs) > 0: + command.values.append(self.__uid) + + return command diff --git a/sdk/python/flet/control_event.py b/sdk/python/flet/control_event.py new file mode 100644 index 0000000000..7f56ac8808 --- /dev/null +++ b/sdk/python/flet/control_event.py @@ -0,0 +1,9 @@ +from flet.event import Event + + +class ControlEvent(Event): + def __init__(self, target, name, data, control, page): + Event.__init__(self, target=target, name=name, data=data) + + self.control = control + self.page = page diff --git a/sdk/python/flet/datepicker.py b/sdk/python/flet/datepicker.py new file mode 100644 index 0000000000..9fcc97f9b3 --- /dev/null +++ b/sdk/python/flet/datepicker.py @@ -0,0 +1,157 @@ +from datetime import date, datetime +from typing import Optional + +from beartype import beartype + +from flet.control import Control + + +class DatePicker(Control): + def __init__( + self, + label=None, + id=None, + ref=None, + value=None, + placeholder=None, + required=None, + allow_text_input=None, + underlined=None, + borderless=None, + focused=None, + on_change=None, + on_focus=None, + on_blur=None, + width=None, + visible=None, + disabled=None, + ): + Control.__init__( + self, id=id, ref=ref, width=width, visible=visible, disabled=disabled + ) + self.label = label + self.value = value + self.placeholder = placeholder + self.allow_text_input = allow_text_input + self.underlined = underlined + self.borderless = borderless + self.required = required + self.focused = focused + self.on_change = on_change + self.on_focus = on_focus + self.on_blur = on_blur + + def _get_control_name(self): + return "datepicker" + + def _set_attr(self, name, value, dirty=True): + d = value + if d == "": + d = None + elif name == "value" and d != None and not isinstance(d, date): + d = datetime.fromisoformat(value.replace("Z", "+00:00")) + self._set_attr_internal(name, d, dirty) + + # label + @property + def label(self): + return self._get_attr("label") + + @label.setter + def label(self, value): + self._set_attr("label", value) + + # value + @property + def value(self): + return self._get_attr("value") + + @value.setter + def value(self, value): + self._set_attr("value", value) + + # placeholder + @property + def placeholder(self): + return self._get_attr("placeholder") + + @placeholder.setter + def placeholder(self, value): + self._set_attr("placeholder", value) + + # allow_text_input + @property + def allow_text_input(self): + return self._get_attr("allowTextInput", data_type="bool", def_value=False) + + @allow_text_input.setter + @beartype + def allow_text_input(self, value: Optional[bool]): + self._set_attr("allowTextInput", value) + + # underlined + @property + def underlined(self): + return self._get_attr("underlined", data_type="bool", def_value=False) + + @underlined.setter + @beartype + def underlined(self, value: Optional[bool]): + self._set_attr("underlined", value) + + # borderless + @property + def borderless(self): + return self._get_attr("borderless", data_type="bool", def_value=False) + + @borderless.setter + @beartype + def borderless(self, value: Optional[bool]): + self._set_attr("borderless", value) + + # required + @property + def required(self): + return self._get_attr("required", data_type="bool", def_value=False) + + @required.setter + @beartype + def required(self, value: Optional[bool]): + self._set_attr("required", value) + + # focused + @property + def focused(self): + return self._get_attr("focused", data_type="bool", def_value=False) + + @focused.setter + @beartype + def focused(self, value: Optional[bool]): + self._set_attr("focused", value) + + # on_change + @property + def on_change(self): + return self._get_event_handler("change") + + @on_change.setter + def on_change(self, handler): + self._add_event_handler("change", handler) + + # on_focus + @property + def on_focus(self): + return self._get_event_handler("focus") + + @on_focus.setter + def on_focus(self, handler): + self._add_event_handler("focus", handler) + + # on_blur + @property + def on_blur(self): + return self._get_event_handler("blur") + + @on_blur.setter + def on_blur(self, handler): + self._add_event_handler("blur", handler) diff --git a/sdk/python/flet/dialog.py b/sdk/python/flet/dialog.py new file mode 100644 index 0000000000..ed090287ec --- /dev/null +++ b/sdk/python/flet/dialog.py @@ -0,0 +1,203 @@ +from typing import Optional + +from beartype import beartype + +from flet.control import Control + +try: + from typing import Literal +except: + from typing_extensions import Literal + + +DialogType = Literal[None, "normal", "largeHeader", "close"] + + +class Dialog(Control): + def __init__( + self, + id=None, + ref=None, + open=None, + title=None, + sub_text=None, + type: DialogType = None, + auto_dismiss=None, + width=None, + max_width=None, + height=None, + fixed_top=None, + blocking=None, + data=None, + controls=None, + footer=None, + on_dismiss=None, + padding=None, + margin=None, + visible=None, + disabled=None, + ): + + Control.__init__( + self, + id=id, + ref=ref, + width=width, + height=height, + padding=padding, + margin=margin, + visible=visible, + disabled=disabled, + data=data, + ) + + self.open = open + self.title = title + self.sub_text = sub_text + self.type = type + self.auto_dismiss = auto_dismiss + self.max_width = max_width + self.fixed_top = fixed_top + self.blocking = blocking + self.on_dismiss = on_dismiss + self.__footer = Footer(controls=footer) + self.__controls = [] + if controls != None: + for control in controls: + self.__controls.append(control) + + def _get_control_name(self): + return "dialog" + + # controls + @property + def controls(self): + return self.__controls + + @controls.setter + def controls(self, value): + self.__controls = value + + # footer + @property + def footer(self): + return self.__footer + + # on_dismiss + @property + def on_dismiss(self): + return self._get_event_handler("dismiss") + + @on_dismiss.setter + def on_dismiss(self, handler): + self._add_event_handler("dismiss", handler) + + # open + @property + def open(self): + return self._get_attr("open", data_type="bool", def_value=False) + + @open.setter + @beartype + def open(self, value: Optional[bool]): + self._set_attr("open", value) + + # title + @property + def title(self): + return self._get_attr("title") + + @title.setter + def title(self, value): + self._set_attr("title", value) + + # sub_text + @property + def sub_text(self): + return self._get_attr("subText") + + @sub_text.setter + def sub_text(self, value): + self._set_attr("subText", value) + + # type + @property + def type(self): + return self._get_attr("type") + + @type.setter + @beartype + def type(self, value: DialogType): + self._set_attr("type", value) + + # auto_dismiss + @property + def auto_dismiss(self): + return self._get_attr("autoDismiss", data_type="bool", def_value=True) + + @auto_dismiss.setter + @beartype + def auto_dismiss(self, value: Optional[bool]): + self._set_attr("autoDismiss", value) + + # max_width + @property + def max_width(self): + return self._get_attr("maxWidth") + + @max_width.setter + def max_width(self, value): + self._set_attr("maxWidth", value) + + # fixed_top + @property + def fixed_top(self): + return self._get_attr("fixedTop", data_type="bool", def_value=False) + + @fixed_top.setter + @beartype + def fixed_top(self, value: Optional[bool]): + self._set_attr("fixedTop", value) + + # blocking + @property + def blocking(self): + return self._get_attr("blocking", data_type="bool", def_value=False) + + @blocking.setter + @beartype + def blocking(self, value: Optional[bool]): + self._set_attr("blocking", value) + + def _get_children(self): + result = [] + if self.__controls and len(self.__controls) > 0: + for control in self.__controls: + result.append(control) + result.append(self.__footer) + return result + + +class Footer(Control): + def __init__(self, id=None, ref=None, controls=None): + Control.__init__(self, id=id, ref=ref) + + self.__controls = [] + if controls != None: + for control in controls: + self.__controls.append(control) + + # controls + @property + def controls(self): + return self.__controls + + @controls.setter + def controls(self, value): + self.__controls = value + + def _get_control_name(self): + return "footer" + + def _get_children(self): + return self.__controls diff --git a/sdk/python/flet/dropdown.py b/sdk/python/flet/dropdown.py new file mode 100644 index 0000000000..09ecd65fe9 --- /dev/null +++ b/sdk/python/flet/dropdown.py @@ -0,0 +1,192 @@ +from typing import Optional + +from beartype import beartype + +from flet.control import Control + +try: + from typing import Literal +except: + from typing_extensions import Literal + + +ItemType = Literal[None, "normal", "divider", "header"] + + +class Dropdown(Control): + def __init__( + self, + label=None, + id=None, + ref=None, + value=None, + placeholder=None, + error_message=None, + on_change=None, + on_focus=None, + on_blur=None, + options=None, + width=None, + height=None, + padding=None, + margin=None, + visible=None, + disabled=None, + focused=None, + data=None, + ): + Control.__init__( + self, + id=id, + ref=ref, + width=width, + height=height, + padding=padding, + margin=margin, + visible=visible, + disabled=disabled, + data=data, + ) + self.label = label + self.value = value + self.placeholder = placeholder + self.error_message = error_message + self.focused = focused + self.on_change = on_change + self.on_focus = on_focus + self.on_blur = on_blur + self.__options = [] + if options != None: + for option in options: + self.__options.append(option) + + def _get_control_name(self): + return "dropdown" + + # options + @property + def options(self): + return self.__options + + @options.setter + def options(self, value): + self.__options = value + + # on_change + @property + def on_change(self): + return self._get_event_handler("change") + + @on_change.setter + def on_change(self, handler): + self._add_event_handler("change", handler) + + # label + @property + def label(self): + return self._get_attr("label") + + @label.setter + def label(self, value): + self._set_attr("label", value) + + # value + @property + def value(self): + return self._get_attr("value") + + @value.setter + def value(self, value): + self._set_attr("value", value) + + # placeholder + @property + def placeholder(self): + return self._get_attr("placeholder") + + @placeholder.setter + def placeholder(self, value): + self._set_attr("placeholder", value) + + # error_message + @property + def error_message(self): + return self._get_attr("errorMessage") + + @error_message.setter + def error_message(self, value): + self._set_attr("errorMessage", value) + + def _get_children(self): + return self.__options + + # focused + @property + def focused(self): + return self._get_attr("focused", data_type="bool", def_value=False) + + @focused.setter + @beartype + def focused(self, value: Optional[bool]): + self._set_attr("focused", value) + + # on_focus + @property + def on_focus(self): + return self._get_event_handler("focus") + + @on_focus.setter + def on_focus(self, handler): + self._add_event_handler("focus", handler) + + # on_blur + @property + def on_blur(self): + return self._get_event_handler("blur") + + @on_blur.setter + def on_blur(self, handler): + self._add_event_handler("blur", handler) + + +class Option(Control): + def __init__( + self, key=None, text=None, item_type: ItemType = None, disabled=None, ref=None + ): + Control.__init__(self, ref=ref, disabled=disabled) + assert key != None or text != None, "key or text must be specified" + self.key = key + self.text = text + self.item_type = item_type + self.disabled = disabled + + def _get_control_name(self): + return "option" + + # key + @property + def key(self): + return self._get_attr("key") + + @key.setter + def key(self, value): + self._set_attr("key", value) + + # text + @property + def text(self): + return self._get_attr("text") + + @text.setter + def text(self, value): + self._set_attr("text", value) + + # item_type + @property + def item_type(self): + return self._get_attr("itemtype") + + @item_type.setter + @beartype + def item_type(self, value: ItemType): + self._set_attr("itemtype", value) diff --git a/sdk/python/flet/event.py b/sdk/python/flet/event.py new file mode 100644 index 0000000000..5bbcc256b1 --- /dev/null +++ b/sdk/python/flet/event.py @@ -0,0 +1,5 @@ +class Event: + def __init__(self, target, name, data): + self.target = target + self.name = name + self.data = data diff --git a/sdk/python/flet/expanded.py b/sdk/python/flet/expanded.py new file mode 100644 index 0000000000..d3b677bb4e --- /dev/null +++ b/sdk/python/flet/expanded.py @@ -0,0 +1,42 @@ +from typing import Optional + +from beartype import beartype + +from flet.control import Control + + +class Expanded(Control): + def __init__( + self, + control=None, + id=None, + ref=None, + visible=None, + disabled=None, + data=None, + ): + Control.__init__( + self, + id=id, + ref=ref, + visible=visible, + disabled=disabled, + data=data, + ) + + self.control = control + + def _get_control_name(self): + return "expanded" + + # control + @property + def control(self): + return self.__control + + @control.setter + def control(self, value): + self.__control = value + + def _get_children(self): + return [] if not self.__control else [self.__control] diff --git a/sdk/python/flet/flet.py b/sdk/python/flet/flet.py new file mode 100644 index 0000000000..ba35faa1ba --- /dev/null +++ b/sdk/python/flet/flet.py @@ -0,0 +1,301 @@ +import json +import logging +import os +import signal +import socket +import tarfile +import tempfile +import threading +import traceback +import urllib.request +import zipfile +from pathlib import Path +from time import sleep + +from flet import constants +from flet.connection import Connection +from flet.event import Event +from flet.page import Page +from flet.reconnecting_websocket import ReconnectingWebSocket +from flet.utils import * + + +def page( + name="", + port=0, + share=False, + update=False, + server=None, + token=None, + permissions=None, + no_window=False, +): + conn = _connect_internal( + name, port, False, update, share, server, token, permissions, no_window + ) + print("Page URL:", conn.page_url) + page = Page(conn, constants.ZERO_SESSION) + conn.sessions[constants.ZERO_SESSION] = page + return page + + +def app( + name="", + port=0, + share=False, + server=None, + token=None, + target=None, + permissions=None, + no_window=False, +): + + if target == None: + raise Exception("target argument is not specified") + + conn = _connect_internal( + name, port, True, False, share, server, token, permissions, no_window, target + ) + print("App URL:", conn.page_url) + + terminate = threading.Event() + + def exit_gracefully(signum, frame): + logging.debug("Gracefully terminating Flet app...") + terminate.set() + + signal.signal(signal.SIGINT, exit_gracefully) + signal.signal(signal.SIGTERM, exit_gracefully) + + try: + print("Connected to Flet app and handling user sessions...") + + if is_windows(): + input() + else: + terminate.wait() + except (Exception) as e: + pass + + conn.close() + + +def _connect_internal( + page_name=None, + port=0, + is_app=False, + update=False, + share=False, + server=None, + token=None, + permissions=None, + no_window=False, + session_handler=None, +): + if share and server == None: + server = constants.HOSTED_SERVICE_URL + elif server == None: + # local mode + env_port = os.getenv("FLET_SERVER_PORT") + if env_port != None and env_port != "": + port = env_port + + # page with a custom port starts detached process + attached = False if not is_app and port != 0 else True + + port = _start_flet_server(port, attached) + server = f"http://localhost:{port}" + + connected = threading.Event() + + def on_event(conn, e): + if e.sessionID in conn.sessions: + conn.sessions[e.sessionID].on_event( + Event(e.eventTarget, e.eventName, e.eventData) + ) + if e.eventTarget == "page" and e.eventName == "close": + print("Session closed:", e.sessionID) + del conn.sessions[e.sessionID] + + def on_session_created(conn, session_data): + page = Page(conn, session_data.sessionID) + conn.sessions[session_data.sessionID] = page + print("Session started:", session_data.sessionID) + try: + session_handler(page) + except Exception as e: + print( + f"Unhandled error processing page session {page.session_id}:", + traceback.format_exc(), + ) + page.error(f"There was an error while processing your request: {e}") + + ws_url = _get_ws_url(server) + ws = ReconnectingWebSocket(ws_url) + conn = Connection(ws) + conn.on_event = on_event + + if session_handler != None: + conn.on_session_created = on_session_created + + def _on_ws_connect(): + if conn.page_name == None: + conn.page_name = page_name + result = conn.register_host_client( + conn.host_client_id, conn.page_name, is_app, update, token, permissions + ) + conn.host_client_id = result.hostClientID + conn.page_name = result.pageName + conn.page_url = server.rstrip("/") + if conn.page_name != constants.INDEX_PAGE: + conn.page_url += f"/{conn.page_name}" + + if not no_window and not conn.browser_opened: + open_in_browser(conn.page_url) + conn.browser_opened = True + connected.set() + + def _on_ws_failed_connect(): + logging.info(f"Failed to connect: {ws_url}") + # if is_localhost_url(ws_url): + # _start_flet_server() + + ws.on_connect = _on_ws_connect + ws.on_failed_connect = _on_ws_failed_connect + ws.connect() + for n in range(0, constants.CONNECT_TIMEOUT_SECONDS): + if not connected.is_set(): + sleep(1) + if not connected.is_set(): + ws.close() + raise Exception( + f"Could not connected to Flet server in {constants.CONNECT_TIMEOUT_SECONDS} seconds." + ) + + return conn + + +def _start_flet_server(port, attached): + + if port == 0: + port = _get_free_tcp_port() + + logging.info(f"Starting local Flet Server on port {port}...") + logging.info(f"Attached process: {attached}") + + flet_exe = "flet.exe" if is_windows() else "flet" + + # check if flet.exe exists in "bin" directory (user mode) + p = Path(__file__).parent.joinpath("bin", flet_exe) + if p.exists(): + flet_path = str(p) + logging.info(f"Flet Server found in: {flet_path}") + else: + # check if flet.exe is in PATH (flet developer mode) + flet_path = which(flet_exe) + if not flet_path: + # download flet from GitHub (python module developer mode) + flet_path = _download_flet() + else: + logging.info(f"Flet Server found in PATH") + + flet_env = {**os.environ, "FLET_LOG_TO_FILE": "true"} + + args = [flet_path, "server", "--port", str(port)] + + if attached: + args.append("--attached") + + log_level = logging.getLogger().getEffectiveLevel() + if log_level == logging.CRITICAL: + log_level = logging.FATAL + + if log_level != logging.NOTSET: + log_level_name = logging.getLevelName(log_level).lower() + args.extend(["--log-level", log_level_name]) + + subprocess.Popen( + args, + env=flet_env, + # creationflags=subprocess.CREATE_NEW_PROCESS_GROUP, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + + return port + + +def _get_ws_url(server: str): + url = server.rstrip("/") + if server.startswith("https://"): + url = url.replace("https://", "wss://") + elif server.startswith("http://"): + url = url.replace("http://", "ws://") + else: + url = "ws://" + url + return url + "/ws" + + +def _download_flet(): + flet_exe = "flet.exe" if is_windows() else "flet" + flet_bin = Path.home().joinpath(".flet", "bin") + flet_bin.mkdir(parents=True, exist_ok=True) + + flet_version = _get_latest_flet_release() + + if flet_version == None: + raise Exception("There are no Flet releases yet.") + + installed_ver = None + flet_path = flet_bin.joinpath(flet_exe) + if flet_path.exists(): + # check installed version + installed_ver = subprocess.check_output([str(flet_path), "--version"]).decode( + "utf-8" + ) + logging.info(f"Flet v{flet_version} is already installed in {flet_path}") + + if not installed_ver or installed_ver != flet_version: + print(f"Downloading Flet v{flet_version} to {flet_path}") + + ext = "zip" if is_windows() else "tar.gz" + file_name = f"flet-{flet_version}-{get_platform()}-{get_arch()}.{ext}" + flet_url = f"https://github.com/flet/flet/releases/download/v{flet_version}/{file_name}" + + temp_arch = Path(tempfile.gettempdir()).joinpath(file_name) + try: + urllib.request.urlretrieve(flet_url, temp_arch) + if is_windows(): + with zipfile.ZipFile(temp_arch, "r") as zip_arch: + zip_arch.extractall(flet_bin) + else: + with tarfile.open(temp_arch, "r:gz") as tar_arch: + tar_arch.extractall(flet_bin) + finally: + os.remove(temp_arch) + return str(flet_path) + + +def _get_latest_flet_release(): + releases = json.loads( + urllib.request.urlopen( + f"https://api.github.com/repos/flet-dev/flet/releases?per_page=5" + ) + .read() + .decode() + ) + if len(releases) > 0: + return releases[0]["tag_name"].lstrip("v") + else: + return None + + +def _get_free_tcp_port(): + sock = socket.socket() + sock.bind(("", 0)) + return sock.getsockname()[1] + + +# Fix: https://bugs.python.org/issue35935 +# if _is_windows(): +# signal.signal(signal.SIGINT, signal.SIG_DFL) diff --git a/sdk/python/flet/form.py b/sdk/python/flet/form.py new file mode 100644 index 0000000000..852d256ab4 --- /dev/null +++ b/sdk/python/flet/form.py @@ -0,0 +1,531 @@ +import copy +import dataclasses +import datetime +import time +from dataclasses import is_dataclass +from functools import partial +from typing import Any +from typing import Union + +from beartype.typing import List +from flet import choicegroup +from flet import combobox +from flet import dropdown +from flet.button import Button +from flet.checkbox import Checkbox +from flet.choicegroup import ChoiceGroup +from flet.combobox import ComboBox +from flet.control import Control +from flet.control_event import ControlEvent +from flet.datepicker import DatePicker +from flet.dropdown import Dropdown +from flet.message import Message +from flet.panel import Panel +from flet.spinbutton import SpinButton +from flet.stack import Stack +from flet.text import Text +from flet.textbox import Textbox +from flet.toggle import Toggle + +__all__ = ["Form"] + + +class Form(Stack): + + _step_for_floats = 0.1 + + _float_button = partial(SpinButton, step=_step_for_floats) + # _date_picker_with_edit = partial(DatePicker, allow_text_input=True) + + _standard_library_types = { + "str": Textbox, + "int": SpinButton, + "float": _float_button, + "Decimal": Textbox, + "bool": Checkbox, + "datetime": Textbox, + "date": Textbox, + "time": Textbox, + } + + _pydantic_types = { + "ConstrainedIntValue": SpinButton, + "NegativeIntValue": SpinButton, + "PositiveIntValue": SpinButton, + "StrictIntValue": SpinButton, + "ConstrainedFloatValue": _float_button, + "NegativeFloatValue": _float_button, + "PositiveFloatValue": _float_button, + "StrictFloatValue": _float_button, + "ConstrainedDecimalValue": _float_button, + "StrictBoolValue": Checkbox, + "EmailStrValue": Textbox, + "PastDateValue": Textbox, + "FutureDateValue": Textbox, + # 'SecretStr': , not supported by flet yet + } + + default_data_to_control_mapping = _standard_library_types + default_data_to_control_mapping.update(_pydantic_types) + + # Alignments when not "top" + _label_alignment_by_control_type = { + DatePicker: "center", + SpinButton: "center", + Textbox: "center", + } + + def __init__( + self, + value: Any, + title: str = None, + on_submit: callable = None, + submit_button: Button = None, + field_validation_default_error_message: str = "Check this value", + form_validation_error_message: str = "Not all fields have valid values", + autosave: bool = False, + label_above: bool = False, + label_alignment: str = "left", + label_width: Union[int, str] = "30%", + control_width: Union[int, str] = "100%", + control_style: str = "normal", + control_kwargs: dict = None, + control_mapping: dict = None, + toggle_for_bool: bool = False, + padding: int = 20, + gap: int = 10, + width="min(600px, 90%)", + threshold_for_dropdown=3, + **kwargs, + ): + super().__init__(**kwargs) + self.title = title + self.field_validation_default_error_message = field_validation_default_error_message + self.form_validation_error_message = form_validation_error_message + self.autosave = autosave + self.label_above = label_above + self.label_alignment = label_alignment + self.label_width = label_width + self.control_width = control_width + self.control_style = control_style + self.control_kwargs = control_kwargs or {} + self.threshold_for_dropdown = threshold_for_dropdown + + self.padding = padding + self.gap = gap + self.width = width + + self.data_to_control_mapping = self.default_data_to_control_mapping.copy() + self.data_to_control_mapping.update(control_mapping or {}) + + if toggle_for_bool: + self.data_to_control_mapping["bool"] = Toggle + self.data_to_control_mapping["StrictBoolValue"] = Toggle + + if type(value) is type: + self._model = value + try: + self.value = self._model() + except Exception as error: + raise ValueError("Unable to instantiate form data with default values", error) + else: + self._model = type(value) + self.value = value + + self.working_copy = self.autosave and self.value or copy.deepcopy(self.value) + + self._fields = {} + self._messages = {} + self._pydantic_fields = {} + + self.on_submit = getattr(submit_button, "on_click", on_submit) + + self.submit_button = submit_button or Button(text="OK", primary=True, icon="CheckMark") + self.submit_button.on_click = self._submit + + self._form_not_valid_message = Message(value=self.form_validation_error_message, type="error", visible=False) + + self._create_controls() + + def _create_controls(self): + title_controls = [Text(value=self.title, bold=True, size="xLarge")] if self.title else [] + input_controls = self._create_controls_for_annotations(self.working_copy, self._model, self.label_above) + button_controls = [ + Stack(horizontal=True, horizontal_align="end", controls=[self._form_not_valid_message, self.submit_button]) + ] + self.controls = title_controls + input_controls + button_controls + + def _create_controls_for_annotations(self, obj, cls, label_above, path: tuple = tuple()) -> List[Control]: + return [ + self._create_control(attribute, attribute_type, getattr(obj, attribute), label_above, path) + for attribute, attribute_type in cls.__annotations__.items() + ] + + def _create_control( + self, + attribute: str, + attribute_type: Any, + value: Any, + label_above: bool, + path: tuple + ) -> Control: + + # For unions, we consider only the first type annotation + origin = getattr(attribute_type, "__origin__", None) + if origin and origin == Union: + attribute_type = attribute_type.__args__[0] + + control_data = ControlData( + attribute=attribute, + attribute_type=attribute_type, + value=value, + label_text=attribute.replace("_", " ").capitalize(), + placeholder="", + error_message=self.field_validation_default_error_message, + kwargs=self.control_kwargs.get(attribute, {}), + ) + + control_data = self._apply_dataclass_overrides(control_data, path) + control_data = self._apply_pydantic_overrides(control_data, path) + + # handle_change_func = partial(self._handle_field_submit_event, path + (attribute,)) + + is_list = False + + if origin == list and len(attribute_type.__args__) == 1: + actual_type = attribute_type.__args__[0] + control_data.attribute_type = actual_type + if type(actual_type).__name__ == "EnumMeta": + control = self._create_choice_control(control_data, multiple=True) + else: + control = self._create_list_control(control_data) + is_list = True + elif type(attribute_type).__name__ == "EnumMeta": + control = self._create_choice_control(control_data) + elif self._is_complex_object(attribute_type): + control = self._create_complex_control(control_data, path) + else: + control = self._create_basic_control(control_data) + + if self.control_style == "line": + try: + control.underlined = True + control.borderless = True + except AttributeError: + pass + + self._fields[path + (attribute,)] = control + + controls = [control] + + if not self._is_complex_object(attribute_type): + message = Message(value=control_data.error_message, type="error", visible=False) + self._messages[path + (attribute,)] = message + controls.append(message) + + control_stack = Stack( + controls=controls, + width=self.control_width, + vertical_align="center", + ) + + if hasattr(control, "label"): + control.label = None + + label_text = Text( + value=control_data.label_text, + width="100%", + bold=True, + align=self.label_alignment, + vertical_align=self._label_alignment_by_control_type.get(type(control), "top"), + ) + + label_stack = Stack(horizontal=True, controls=[label_text]) + if not label_above: + label_stack.width = self.label_width + + if is_list: + label_stack.controls.append(Button(icon="Add", on_click=control.list_add)) + + attribute_stack = Stack( + horizontal_align="end", + controls=[ + label_stack, + control_stack, + ], + ) + if label_above: + attribute_stack.gap = 0 + + if not label_above: + attribute_stack.horizontal = True + + return attribute_stack + + def _is_complex_object(self, object_type: type): + return is_dataclass(object_type) or hasattr(object_type, "__fields__") + + def _apply_dataclass_overrides(self, control_data, path): + custom_kwargs = {} + if hasattr(self._model, "__dataclass_fields__"): + dataclass_field = self.value.__dataclass_fields__.get(control_data.attribute) + if dataclass_field: + metadata = dataclass_field.metadata + if metadata: + custom_kwargs = metadata.get('flet', {}) + + if custom_kwargs: + control_data.kwargs.update(custom_kwargs) + + return control_data + + def _apply_pydantic_overrides(self, control_data, path): + pydantic_field = ( + hasattr(self._model, "__fields__") and self.value.__fields__.get(control_data.attribute) or None + ) + + if pydantic_field: + self._pydantic_fields[path + (control_data.attribute,)] = pydantic_field + + label_text = pydantic_field.field_info.title + if label_text: + control_data.label_text = label_text + + placeholder = pydantic_field.field_info.description + if placeholder: + control_data.placeholder = placeholder + control_data.error_message = placeholder + + extra = pydantic_field.field_info.extra + if extra: + control_data.kwargs.update(extra.get('flet', {})) + + return control_data + + def _create_basic_control(self, control_data): + control_type = self.data_to_control_mapping.get(control_data.attribute_type.__name__, Textbox) + control = control_type(value=control_data.value, **control_data.kwargs) + if control_type in (DatePicker, Dropdown, Textbox): + control.placeholder = control_data.placeholder + return control + + def _create_choice_control(self, control_data, multiple=False): + enum_type = control_data.attribute_type + + if multiple: + return ComboBox( + multi_select=True, + options=[combobox.Option(key=option.value, text=option.value.title()) for option in enum_type], + value=[enum_type(value).value for value in control_data.value], + ) + + if len(enum_type) > self.threshold_for_dropdown: + control_type = Dropdown + option_type = dropdown.Option + else: + control_type = ChoiceGroup + option_type = choicegroup.Option + + value = enum_type(control_data.value).value + + return control_type( + options=[option_type(key=option.value, text=option.value.title()) for option in enum_type], + value=value, + ) + + def _create_complex_control(self, control_data, path): + return Stack( + width="100%", + controls=self._create_controls_for_annotations( + control_data.value, control_data.attribute_type, label_above=True, path=path + (control_data.attribute,) + ), + ) + + def _create_list_control(self, control_data: "ControlData") -> "ListControl": + if self._is_complex_object(control_data.attribute_type): + return ListControl( + value=control_data.value, + attribute_type=control_data.attribute_type, + form=self, + simple=False, + panel_width=self.width, + ) + else: + return ListControl( + value=control_data.value, + attribute_type=control_data.attribute_type, + form=self, + ) + + def _handle_field_submit_event(self, attribute, event): + self._validate_value(attribute) + + def _validate_value(self, attribute: str) -> bool: + is_valid = True + control = self._fields[attribute] + message = self._messages[attribute] + message.value = self.field_validation_default_error_message + + if type(control) is Stack: + return True + elif type(control) is DatePicker and type(control.value) is datetime.datetime: + datetime_tuple = control.value.timetuple() + if datetime_tuple[3:6] == (0, 0, 0): + control.value = datetime.date(datetime_tuple[:3]) + + pydantic_field = self._pydantic_fields.get(attribute) + if pydantic_field: + description = pydantic_field.field_info.description + if description: + message.value = description + value, error = pydantic_field.validate( + control.value, + self.working_copy.dict(), + loc=attribute, + cls=self._model, + ) + if error: + is_valid = False + message.value = str(error.exc).capitalize() + else: + # Validation can change the value, update control + if type(value) is datetime.date: + value = value.isoformat() + control.value = value + + if is_valid: + obj = self.working_copy + for attribute_name in attribute[:-1]: + obj = getattr(obj, attribute_name) + try: + setattr(obj, attribute[-1], control.value) + except ValueError: + is_valid = False + + self._messages[attribute].visible = not is_valid + self.page.update() + return is_valid + + def _submit(self, e): + if not all(self._validate_value(attribute) for attribute in self._fields): + self.submit_button.primary = False + self.submit_button.icon = "Cancel" + self.page.update() + time.sleep(5) + self.submit_button.primary = True + self.submit_button.icon = "CheckMark" + self.page.update() + else: + if not self.autosave: + self.value.__dict__.update(self.working_copy.__dict__) + if self.on_submit: + custom_event = ControlEvent(self.submit_button, "submit", None, self, self.page) + self.on_submit(custom_event) + + +class ListControl(Stack): + + def __init__(self, value, attribute_type, form, simple=True, panel_width=None, gap=0, **kwargs): + super().__init__(**kwargs) + self.form = form + self.simple = simple + self.gap = gap + self.value = value + self.attribute_type = attribute_type + self.panel_width = panel_width + self.panel = None + self.panel_holder = Stack() + self.update() + + def update(self): + if self.simple: + self.controls = [ + Stack( + gap=2, + horizontal=True, + controls=[ + self.get_value_control(item, index), + Button(height="100%", icon="Delete", on_click=partial(self.list_delete, index)), + ], + ) + for index, item in enumerate(self.value) + ] + else: + self.controls = [ + Stack( + gap=0, + horizontal=True, + # border_top="1px solid lightgray", + controls=[ + Button(width="100%", text=str(item), action=True, on_click=partial(self.list_selection, item)), + Button(height="100%", icon="Delete", on_click=partial(self.list_delete, index)), + Button(height="100%", icon="ChevronRight", on_click=partial(self.list_selection, item)), + ], + ) + for index, item in enumerate(self.value) + ] + [self.panel_holder] + + def get_value_control(self, item: Any, index: int) -> Control: + control_data = ControlData( + attribute="", + attribute_type=self.attribute_type, + value=item, + label_text="", + placeholder="", + error_message="", + kwargs={}, + ) + control = self.form._create_basic_control(control_data) + control.width = "100%" + control.on_change = partial(self.list_change, index) + return control + + def list_change(self, index, event): + self.value[index] = event.control.value + + def list_selection(self, item, event): + subform = Form(value=item, on_submit=self._handle_subform_submit_event) + self.panel = Panel( + open=True, + type='custom', + auto_dismiss=False, + light_dismiss=True, + title=type(item).__name__.capitalize(), + controls=[subform], + on_dismiss=self._handle_subform_dismiss_event + ) + if self.panel_width: + self.panel.width = self.panel_width + self.panel_holder.controls.append(self.panel) + self.panel_holder.update() + + def list_delete(self, index, event): + del self.value[index] + self.update() + self.page.update() + + def list_add(self, event): + self.value.append(self.attribute_type()) + self.update() + self.page.update() + self.list_selection(self.value[-1], event) + + def _handle_subform_submit_event(self, event): + self.update() + self.page.update() + self._handle_subform_dismiss_event(event) + + def _handle_subform_dismiss_event(self, event): + self.panel_holder.controls.pop() + self.panel_holder.update() + + +@dataclasses.dataclass +class ControlData: + attribute: str + attribute_type: Any + value: Any + label_text: str + placeholder: str + error_message: str + kwargs: dict diff --git a/sdk/python/flet/grid.py b/sdk/python/flet/grid.py new file mode 100644 index 0000000000..34da56ed91 --- /dev/null +++ b/sdk/python/flet/grid.py @@ -0,0 +1,418 @@ +from __future__ import annotations + +from typing import Optional + +from beartype import beartype + +from flet.control import Control + +try: + from typing import Literal +except: + from typing_extensions import Literal + + +SelectionMode = Literal[None, "single", "multiple"] +Sortable = Literal[None, "string", "number", False] +Sorted = Literal[None, False, "asc", "desc"] + + +class Grid(Control): + def __init__( + self, + id=None, + ref=None, + selection_mode: SelectionMode = None, + compact=None, + header_visible=None, + shimmer_lines=None, + preserve_selection=None, + columns=None, + items=None, + on_select=None, + onitem_invoke=None, + width=None, + height=None, + padding=None, + margin=None, + visible=None, + disabled=None, + ): + Control.__init__( + self, + id=id, + ref=ref, + width=width, + height=height, + padding=padding, + margin=margin, + visible=visible, + disabled=disabled, + ) + + self.selection_mode = selection_mode + self.compact = compact + self.header_visible = header_visible + self.shimmer_lines = shimmer_lines + self.preserve_selection = preserve_selection + self._on_select_handler = None + self.on_select = on_select + self.onitem_invoke = onitem_invoke + self._columns = Columns(columns=columns) + self._items = Items(items=items) + self._selected_items = [] + + def _get_control_name(self): + return "grid" + + # columns + @property + def columns(self): + return self._columns.columns + + @columns.setter + def columns(self, value): + self._columns.columns = value + + # items + @property + def items(self): + return self._items.items + + @items.setter + def items(self, value): + self._items.items = value + + # on_select + @property + def on_select(self): + return self._on_select_handler + + @on_select.setter + def on_select(self, handler): + self._on_select_handler = handler + self._add_event_handler("select", self._on_select_internal) + + # selected_items + @property + def selected_items(self): + return self._selected_items + + @selected_items.setter + def selected_items(self, value): + self._selected_items = value + indices = [ + str(idx) + for selected_item in value + for idx, item in enumerate(self._items.items) + if item == selected_item + ] + self._set_attr("selectedindices", " ".join(indices)) + + # onitem_invoke + @property + def onitem_invoke(self): + return self._get_event_handler("itemInvoke") + + @onitem_invoke.setter + def onitem_invoke(self, handler): + self._add_event_handler("itemInvoke", handler) + + # selection_mode + @property + def selection_mode(self): + return self._get_attr("selection") + + @selection_mode.setter + @beartype + def selection_mode(self, value: SelectionMode): + self._set_attr("selection", value) + + # compact + @property + def compact(self): + return self._get_attr("compact", data_type="bool", def_value=False) + + @compact.setter + @beartype + def compact(self, value: Optional[bool]): + self._set_attr("compact", value) + + # header_visible + @property + def header_visible(self): + return self._get_attr("headerVisible", data_type="bool", def_value=True) + + @header_visible.setter + @beartype + def header_visible(self, value: Optional[bool]): + self._set_attr("headerVisible", value) + + # preserve_selection + @property + def preserve_selection(self): + return self._get_attr("preserveSelection", data_type="bool", def_value=False) + + @preserve_selection.setter + @beartype + def preserve_selection(self, value: Optional[bool]): + self._set_attr("preserveSelection", value) + + # shimmer_lines + @property + def shimmer_lines(self): + return self._get_attr("shimmerLines") + + @shimmer_lines.setter + @beartype + def shimmer_lines(self, value: Optional[int]): + self._set_attr("shimmerLines", value) + + def _on_select_internal(self, e): + self._selected_items = [self.page.get_control(id).obj for id in e.data.split()] + + if self._on_select_handler != None: + self._on_select_handler(e) + + def _get_children(self): + return [self._columns, self._items] + + +class Columns(Control): + def __init__(self, id=None, ref=None, columns=None): + Control.__init__(self, id=id, ref=ref) + + self.columns = columns + + def _get_control_name(self): + return "columns" + + def _get_children(self): + return self.columns + + +class Items(Control): + def __init__(self, id=None, ref=None, items=None): + Control.__init__(self, id=id, ref=ref) + + self.__map = {} + self.__items = [] + self.items = items + + # items + @property + def items(self): + return self.__items + + @items.setter + @beartype + def items(self, value: Optional[list]): + self.__items = value or [] + + def _get_control_name(self): + return "items" + + def _get_children(self): + items = [] + for obj in self.__items: + key = obj + if isinstance(obj, dict): + key = tuple(obj.items()) + item = self.__map.setdefault(key, Item(obj)) + item._fetch_attrs() + items.append(item) + + del_objs = [key for key, item in self.__map.items() if item not in items] + for key in del_objs: + del self.__map[key] + + return items + + +class Column(Control): + def __init__( + self, + id=None, + ref=None, + name=None, + icon=None, + icon_only=None, + field_name=None, + sortable: Sortable = None, + sort_field=None, + sorted: Sorted = None, + resizable=None, + min_width=None, + max_width=None, + on_click=None, + template_controls=None, + ): + Control.__init__(self, id=id, ref=ref) + + self.name = name + self.icon = icon + self.icon_only = icon_only + self.field_name = field_name + self.sortable = sortable + self.sort_field = sort_field + self.sorted = sorted + self.resizable = resizable + self.min_width = min_width + self.max_width = max_width + self.on_click = on_click + + self.template_controls = template_controls or [] + + def _get_control_name(self): + return "column" + + # name + @property + def name(self): + return self._get_attr("name") + + @name.setter + def name(self, value): + self._set_attr("name", value) + + # icon + @property + def icon(self): + return self._get_attr("icon") + + @icon.setter + def icon(self, value): + self._set_attr("icon", value) + + # icon_only + @property + def icon_only(self): + return self._get_attr("iconOnly", data_type="bool", def_value=False) + + @icon_only.setter + @beartype + def icon_only(self, value: Optional[bool]): + self._set_attr("iconOnly", value) + + # field_name + @property + def field_name(self): + return self._get_attr("fieldName") + + @field_name.setter + def field_name(self, value): + self._set_attr("fieldName", value) + + # sortable + @property + def sortable(self): + return self._get_attr("sortable") + + @sortable.setter + @beartype + def sortable(self, value: Sortable): + self._set_attr("sortable", value) + + # sort_field + @property + def sort_field(self): + return self._get_attr("sortField") + + @sort_field.setter + def sort_field(self, value): + self._set_attr("sortField", value) + + # sorted + @property + def sorted(self): + return self._get_attr("sorted") + + @sorted.setter + @beartype + def sorted(self, value: Sorted): + self._set_attr("sorted", value) + + # resizable + @property + def resizable(self): + return self._get_attr("resizable", data_type="bool", def_value=False) + + @resizable.setter + @beartype + def resizable(self, value: Optional[bool]): + self._set_attr("resizable", value) + + # min_width + @property + def min_width(self): + return self._get_attr("minWidth") + + @min_width.setter + @beartype + def min_width(self, value: Optional[int]): + self._set_attr("minWidth", value) + + # max_width + @property + def max_width(self): + return self._get_attr("maxWidth") + + @max_width.setter + @beartype + def max_width( + self, value: Optional[int] + ): # could these not be floats? Union[None, int, float] + self._set_attr("maxWidth", value) + + # on_click + @property + def on_click(self): + return self._get_attr("on_click") + + @on_click.setter + def on_click(self, value): # beartype currently has an issue with typing.Callable + self._set_attr("on_click", value) + + def _get_children(self): + return self.template_controls + + +class Item(Control): + def __init__(self, obj): + Control.__init__(self) + assert obj, "obj cannot be empty" + self.obj = obj + + def _set_attr(self, name, value, dirty=True): + + if value is None: + return + + orig_val = self._get_attr(name) + if orig_val is not None: + if isinstance(orig_val, bool): + value = str(value).lower() == "true" + elif isinstance(orig_val, float): + value = float(str(value)) + + self._set_attr_internal(name, value, dirty=False) + if isinstance(self.obj, dict): + self.obj[name] = value + else: + setattr(self.obj, name, value) + + def _fetch_attrs(self): + # reflection + obj = self.obj if isinstance(self.obj, dict) else vars(self.obj) + + for name, val in obj.items(): + data_type = ( + type(val).__name__ if isinstance(val, (bool, float)) else "string" + ) + orig_val = self._get_attr(name, data_type=data_type) + + if val != orig_val: + self._set_attr_internal(name, val, dirty=True) + + def _get_control_name(self): + return "item" diff --git a/sdk/python/flet/html.py b/sdk/python/flet/html.py new file mode 100644 index 0000000000..c10f201db1 --- /dev/null +++ b/sdk/python/flet/html.py @@ -0,0 +1,21 @@ +from flet.control import Control + + +class Html(Control): + def __init__(self, value=None, id=None, ref=None, visible=None): + + Control.__init__(self, id=id, ref=ref, visible=visible) + + self.value = value + + def _get_control_name(self): + return "html" + + # value + @property + def value(self): + return self._get_attr("value") + + @value.setter + def value(self, value): + self._set_attr("value", value) diff --git a/sdk/python/flet/icon.py b/sdk/python/flet/icon.py new file mode 100644 index 0000000000..5a0b90333b --- /dev/null +++ b/sdk/python/flet/icon.py @@ -0,0 +1,64 @@ +from flet.control import Control + + +class Icon(Control): + def __init__( + self, + name=None, + id=None, + ref=None, + color=None, + size=None, + width=None, + height=None, + padding=None, + margin=None, + visible=None, + disabled=None, + ): + + Control.__init__( + self, + id=id, + ref=ref, + width=width, + height=height, + padding=padding, + margin=margin, + visible=visible, + disabled=disabled, + ) + + self.name = name + self.color = color + self.size = size + + def _get_control_name(self): + return "icon" + + # name + @property + def name(self): + return self._get_attr("name") + + @name.setter + def name(self, value): + self._set_attr("name", value) + + # color + @property + def color(self): + return self._get_attr("color") + + @color.setter + def color(self, value): + self._set_attr("color", value) + + # size + @property + def size(self): + return self._get_attr("size") + + @size.setter + def size(self, value): + self._set_attr("size", value) diff --git a/sdk/python/flet/iframe.py b/sdk/python/flet/iframe.py new file mode 100644 index 0000000000..f1cb0e67ca --- /dev/null +++ b/sdk/python/flet/iframe.py @@ -0,0 +1,106 @@ +from beartype import beartype +from flet.control import BorderColor +from flet.control import BorderRadius +from flet.control import BorderStyle +from flet.control import BorderWidth +from flet.control import Control + + +class IFrame(Control): + def __init__( + self, + id=None, + ref=None, + src=None, + border_style: BorderStyle = None, + border_width: BorderWidth = None, + border_color: BorderColor = None, + border_radius: BorderRadius = None, + title=None, + width=None, + height=None, + padding=None, + margin=None, + visible=None, + disabled=None, + ): + + Control.__init__( + self, + id=id, + ref=ref, + width=width, + height=height, + padding=padding, + margin=margin, + visible=visible, + disabled=disabled, + ) + + self.src = src + self.border_style = border_style + self.border_width = border_width + self.border_color = border_color + self.border_radius = border_radius + self.title = title + + def _get_control_name(self): + return "iframe" + + # src + @property + def src(self): + return self._get_attr("src") + + @src.setter + def src(self, value): + self._set_attr("src", value) + + # border_style + @property + def border_style(self): + return self._get_value_or_list_attr("borderStyle", " ") + + @border_style.setter + @beartype + def border_style(self, value: BorderStyle): + self._set_value_or_list_attr("borderStyle", value, " ") + + # border_width + @property + def border_width(self): + return self._get_value_or_list_attr("borderWidth", " ") + + @border_width.setter + @beartype + def border_width(self, value: BorderWidth): + self._set_value_or_list_attr("borderWidth", value, " ") + + # border_color + @property + def border_color(self): + return self._get_value_or_list_attr("borderColor", " ") + + @border_color.setter + @beartype + def border_color(self, value: BorderColor): + self._set_value_or_list_attr("borderColor", value, " ") + + # border_radius + @property + def border_radius(self): + return self._get_value_or_list_attr("borderRadius", " ") + + @border_radius.setter + @beartype + def border_radius(self, value: BorderRadius): + self._set_value_or_list_attr("borderRadius", value, " ") + + # title + @property + def title(self): + return self._get_attr("title") + + @title.setter + def title(self, value): + self._set_attr("title", value) diff --git a/sdk/python/flet/image.py b/sdk/python/flet/image.py new file mode 100644 index 0000000000..69ddda417a --- /dev/null +++ b/sdk/python/flet/image.py @@ -0,0 +1,153 @@ +from typing import Optional + +from beartype import beartype +from flet.control import BorderColor +from flet.control import BorderRadius +from flet.control import BorderStyle +from flet.control import BorderWidth +from flet.control import Control + +try: + from typing import Literal +except: + from typing_extensions import Literal + + +Fit = Literal[ + None, "none", "contain", "cover", "center", "centerContain", "centerCover" +] + + +class Image(Control): + def __init__( + self, + src=None, + id=None, + ref=None, + alt=None, + title=None, + maximize_frame=None, + fit: Fit = None, + border_style: BorderStyle = None, + border_width: BorderWidth = None, + border_color: BorderColor = None, + border_radius: BorderRadius = None, + width=None, + height=None, + padding=None, + margin=None, + visible=None, + disabled=None, + ): + + Control.__init__( + self, + id=id, + ref=ref, + width=width, + height=height, + padding=padding, + margin=margin, + visible=visible, + disabled=disabled, + ) + + self.src = src + self.alt = alt + self.title = title + self.border_style = border_style + self.border_width = border_width + self.border_color = border_color + self.border_radius = border_radius + self.fit = fit + self.maximize_frame = maximize_frame + + def _get_control_name(self): + return "image" + + # src + @property + def src(self): + return self._get_attr("src") + + @src.setter + def src(self, value): + self._set_attr("src", value) + + # alt + @property + def alt(self): + return self._get_attr("alt") + + @alt.setter + def alt(self, value): + self._set_attr("alt", value) + + # title + @property + def title(self): + return self._get_attr("title") + + @title.setter + def title(self, value): + self._set_attr("title", value) + + # maximize_frame + @property + def maximize_frame(self): + return self._get_attr("maximizeFrame", data_type="bool", def_value=False) + + @maximize_frame.setter + @beartype + def maximize_frame(self, value: Optional[bool]): + self._set_attr("maximizeFrame", value) + + # fit + @property + def fit(self): + return self._get_attr("fit") + + @fit.setter + @beartype + def fit(self, value: Fit): + self._set_attr("fit", value) + + # border_style + @property + def border_style(self): + return self._get_value_or_list_attr("borderStyle", " ") + + @border_style.setter + @beartype + def border_style(self, value: BorderStyle): + self._set_value_or_list_attr("borderStyle", value, " ") + + # border_width + @property + def border_width(self): + return self._get_value_or_list_attr("borderWidth", " ") + + @border_width.setter + @beartype + def border_width(self, value: BorderWidth): + self._set_value_or_list_attr("borderWidth", value, " ") + + # border_color + @property + def border_color(self): + return self._get_value_or_list_attr("borderColor", " ") + + @border_color.setter + @beartype + def border_color(self, value: BorderColor): + self._set_value_or_list_attr("borderColor", value, " ") + + # border_radius + @property + def border_radius(self): + return self._get_value_or_list_attr("borderRadius", " ") + + @border_radius.setter + @beartype + def border_radius(self, value: BorderRadius): + self._set_value_or_list_attr("borderRadius", value, " ") diff --git a/sdk/python/flet/linechart.py b/sdk/python/flet/linechart.py new file mode 100644 index 0000000000..2bb3a2272f --- /dev/null +++ b/sdk/python/flet/linechart.py @@ -0,0 +1,281 @@ +from typing import Optional, Union + +from beartype import beartype + +from flet.control import Control + +try: + from typing import Literal +except: + from typing_extensions import Literal + + +XType = Literal[None, "number", "date"] + + +class LineChart(Control): + def __init__( + self, + id=None, + ref=None, + legend=None, + tooltips=None, + stroke_width=None, + y_min=None, + y_max=None, + y_ticks=None, + y_format=None, + x_type: XType = None, + lines=None, + width=None, + height=None, + padding=None, + margin=None, + visible=None, + disabled=None, + ): + + Control.__init__( + self, + id=id, + ref=ref, + width=width, + height=height, + padding=padding, + margin=margin, + visible=visible, + disabled=disabled, + ) + + self.__lines = [] + if lines != None: + for line in lines: + self.__lines.append(line) + + self.legend = legend + self.tooltips = tooltips + self.stroke_width = stroke_width + self.y_min = y_min + self.y_max = y_max + self.y_ticks = y_ticks + self.y_format = y_format + self.x_type = x_type + + def _get_control_name(self): + return "linechart" + + # lines + @property + def lines(self): + return self.__lines + + @lines.setter + def lines(self, value): + self.__lines = value + + # legend + @property + def legend(self): + return self._get_attr("legend", data_type="bool", def_value=False) + + @legend.setter + @beartype + def legend(self, value: Optional[bool]): + self._set_attr("legend", value) + + # tooltips + @property + def tooltips(self): + return self._get_attr("tooltips", data_type="bool", def_value=False) + + @tooltips.setter + @beartype + def tooltips(self, value: Optional[bool]): + self._set_attr("tooltips", value) + + # stroke_width + @property + def stroke_width(self): + return self._get_attr("strokeWidth") + + @stroke_width.setter + @beartype + def stroke_width(self, value: Optional[int]): + self._set_attr("strokeWidth", value) + + # y_min + @property + def y_min(self): + return self._get_attr("yMin") + + @y_min.setter + @beartype + def y_min(self, value: Union[None, int, float]): + self._set_attr("yMin", value) + + # y_max + @property + def y_max(self): + return self._get_attr("yMax") + + @y_max.setter + @beartype + def y_max(self, value: Union[None, int, float]): + self._set_attr("yMax", value) + + # y_ticks + @property + def y_ticks(self): + return self._get_attr("yTicks") + + @y_ticks.setter + @beartype + def y_ticks(self, value: Optional[int]): + self._set_attr("yTicks", value) + + # y_format + @property + def y_format(self): + return self._get_attr("yFormat") + + @y_format.setter + def y_format(self, value): + self._set_attr("yFormat", value) + + # x_type + @property + def x_type(self): + return self._get_attr("xType") + + @x_type.setter + @beartype + def x_type(self, value: XType): + self._set_attr("xType", value) + + def _get_children(self): + return self.__lines + + +class Data(Control): + def __init__(self, id=None, ref=None, color=None, legend=None, points=None): + Control.__init__(self, id=id, ref=ref) + + self.color = color + self.legend = legend + self.__points = [] + if points != None: + for point in points: + self.__points.append(point) + + # color + @property + def color(self): + return self._get_attr("color") + + @color.setter + def color(self, value): + self._set_attr("color", value) + + # legend + @property + def legend(self): + return self._get_attr("legend") + + @legend.setter + def legend(self, value): + self._set_attr("legend", value) + + # points + @property + def points(self): + return self.__points + + @points.setter + def points(self, value): + self.__points = value + + def _get_control_name(self): + return "data" + + def _get_children(self): + return self.__points + + +class Point(Control): + def __init__( + self, + id=None, + ref=None, + x=None, + y=None, + tick=None, + legend=None, + x_tooltip=None, + y_tooltip=None, + ): + Control.__init__(self, id=id, ref=ref) + + self.x = x + self.y = y + self.tick = tick + self.legend = legend + self.x_tooltip = x_tooltip + self.y_tooltip = y_tooltip + + def _get_control_name(self): + return "p" + + # x + @property + def x(self): + return self._get_attr("x") + + @x.setter + def x(self, value): + self._set_attr("x", value) + + # y + @property + def y(self): + return self._get_attr("y") + + @y.setter + @beartype + def y(self, value: Union[None, int, float]): + self._set_attr("y", value) + + # tick + @property + def tick(self): + return self._get_attr("tick") + + @tick.setter + def tick(self, value): + self._set_attr("tick", value) + + # legend + @property + def legend(self): + return self._get_attr("legend") + + @legend.setter + def legend(self, value): + self._set_attr("legend", value) + + # x_tooltip + @property + def x_tooltip(self): + return self._get_attr("xTooltip") + + @x_tooltip.setter + def x_tooltip(self, value): + self._set_attr("xTooltip", value) + + # y_tooltip + @property + def y_tooltip(self): + return self._get_attr("yTooltip") + + @y_tooltip.setter + def y_tooltip(self, value): + self._set_attr("yTooltip", value) diff --git a/sdk/python/flet/link.py b/sdk/python/flet/link.py new file mode 100644 index 0000000000..9a808379f9 --- /dev/null +++ b/sdk/python/flet/link.py @@ -0,0 +1,169 @@ +from typing import Optional + +from beartype import beartype + +from flet.control import Control, TextAlign + + +class Link(Control): + def __init__( + self, + url=None, + id=None, + ref=None, + value=None, + new_window=None, + title=None, + size=None, + bold=None, + italic=None, + pre=None, + align: TextAlign = None, + on_click=None, + controls=None, + width=None, + height=None, + padding=None, + margin=None, + visible=None, + disabled=None, + data=None, + ): + + Control.__init__( + self, + id=id, + ref=ref, + width=width, + height=height, + padding=padding, + margin=margin, + visible=visible, + disabled=disabled, + data=data, + ) + + self.value = value + self.url = url + self.new_window = new_window + self.title = title + self.size = size + self.bold = bold + self.italic = italic + self.pre = pre + self.align = align + self.on_click = on_click + self.__controls = [] + if controls != None: + for control in controls: + self.__controls.append(control) + + def _get_control_name(self): + return "link" + + # controls + @property + def controls(self): + return self.__controls + + @controls.setter + def controls(self, value): + self.__controls = value + + # on_click + @property + def on_click(self): + return self._get_event_handler("click") + + @on_click.setter + def on_click(self, handler): + self._add_event_handler("click", handler) + + # value + @property + def value(self): + return self._get_attr("value") + + @value.setter + def value(self, value): + self._set_attr("value", value) + + # url + @property + def url(self): + return self._get_attr("url") + + @url.setter + def url(self, value): + self._set_attr("url", value) + + # new_window + @property + def new_window(self): + return self._get_attr("newWindow", data_type="bool", def_value=False) + + @new_window.setter + @beartype + def new_window(self, value: Optional[bool]): + self._set_attr("newWindow", value) + + # title + @property + def title(self): + return self._get_attr("title") + + @title.setter + def title(self, value): + self._set_attr("title", value) + + # size + @property + def size(self): + return self._get_attr("size") + + @size.setter + def size(self, value): + self._set_attr("size", value) + + # bold + @property + def bold(self): + return self._get_attr("bold", data_type="bool", def_value=False) + + @bold.setter + @beartype + def bold(self, value: Optional[bool]): + self._set_attr("bold", value) + + # italic + @property + def italic(self): + return self._get_attr("italic", data_type="bool", def_value=False) + + @italic.setter + @beartype + def italic(self, value: Optional[bool]): + self._set_attr("italic", value) + + # pre + @property + def pre(self): + return self._get_attr("pre", data_type="bool", def_value=False) + + @pre.setter + @beartype + def pre(self, value: Optional[bool]): + self._set_attr("pre", value) + + # align + @property + def align(self): + return self._get_attr("align") + + @align.setter + @beartype + def align(self, value: TextAlign): + self._set_attr("align", value) + + def _get_children(self): + return self.__controls diff --git a/sdk/python/flet/message.py b/sdk/python/flet/message.py new file mode 100644 index 0000000000..696d5335dc --- /dev/null +++ b/sdk/python/flet/message.py @@ -0,0 +1,162 @@ +from typing import Optional + +from beartype import beartype + +from flet.control import Control + +try: + from typing import Literal +except: + from typing_extensions import Literal + + +MessageType = Literal[ + None, "info", "error", "blocked", "severeWarning", "success", "warning" +] + + +class Message(Control): + def __init__( + self, + value=None, + type: MessageType = None, + id=None, + ref=None, + multiline=None, + truncated=None, + dismiss=None, + data=None, + on_dismiss=None, + buttons=None, + width=None, + height=None, + padding=None, + margin=None, + visible=None, + disabled=None, + ): + + Control.__init__( + self, + id=id, + ref=ref, + width=width, + height=height, + padding=padding, + margin=margin, + visible=visible, + disabled=disabled, + data=data, + ) + + self.type = type + self.value = value + self.multiline = multiline + self.truncated = truncated + self.dismiss = dismiss + self.on_dismiss = on_dismiss + self.__buttons = [] + if buttons != None: + for button in buttons: + self.__buttons.append(button) + + def _get_control_name(self): + return "message" + + # buttons + @property + def buttons(self): + return self.__buttons + + @buttons.setter + def buttons(self, value): + self.__buttons = value + + # on_dismiss + @property + def on_dismiss(self): + return self._get_event_handler("dismiss") + + @on_dismiss.setter + def on_dismiss(self, handler): + self._add_event_handler("dismiss", handler) + + # value + @property + def value(self): + return self._get_attr("value") + + @value.setter + def value(self, value): + self._set_attr("value", value) + + # type + @property + def type(self): + return self._get_attr("type") + + @type.setter + @beartype + def type(self, value: MessageType): + self._set_attr("type", value) + + # multiline + @property + def multiline(self): + return self._get_attr("multiline", data_type="bool", def_value=False) + + @multiline.setter + @beartype + def multiline(self, value: Optional[bool]): + self._set_attr("multiline", value) + + # truncated + @property + def truncated(self): + return self._get_attr("truncated", data_type="bool", def_value=False) + + @truncated.setter + @beartype + def truncated(self, value: Optional[bool]): + self._set_attr("truncated", value) + + # dismiss + @property + def dismiss(self): + return self._get_attr("dismiss", data_type="bool", def_value=False) + + @dismiss.setter + @beartype + def dismiss(self, value: Optional[bool]): + self._set_attr("dismiss", value) + + def _get_children(self): + return self.__buttons + + +class MessageButton(Control): + def __init__(self, text, action=None): + Control.__init__(self) + self.text = text + self.action = action + + def _get_control_name(self): + return "button" + + # text + @property + def text(self): + return self._get_attr("text") + + @text.setter + def text(self, value): + self._set_attr("text", value) + + # action + @property + def action(self): + return self._get_attr("action") + + @action.setter + def action(self, value): + self._set_attr("action", value) diff --git a/sdk/python/flet/nav.py b/sdk/python/flet/nav.py new file mode 100644 index 0000000000..c2bbaef7af --- /dev/null +++ b/sdk/python/flet/nav.py @@ -0,0 +1,211 @@ +from typing import Optional + +from beartype import beartype + +from flet.control import Control + + +class Nav(Control): + def __init__( + self, + id=None, + ref=None, + value=None, + items=None, + on_change=None, + on_expand=None, + on_collapse=None, + width=None, + height=None, + padding=None, + margin=None, + visible=None, + disabled=None, + ): + + Control.__init__( + self, + id=id, + ref=ref, + width=width, + height=height, + padding=padding, + margin=margin, + visible=visible, + disabled=disabled, + ) + + self.value = value + self.on_change = on_change + self.on_expand = on_expand + self.on_collapse = on_collapse + self.__items = [] + if items != None: + for item in items: + self.__items.append(item) + + def _get_control_name(self): + return "nav" + + # items + @property + def items(self): + return self.__items + + @items.setter + def items(self, value): + self.__items = value + + # on_change + @property + def on_change(self): + return self._get_event_handler("change") + + @on_change.setter + def on_change(self, handler): + self._add_event_handler("change", handler) + + # on_expand + @property + def on_expand(self): + return self._get_event_handler("expand") + + @on_expand.setter + def on_expand(self, handler): + self._add_event_handler("expand", handler) + + # on_collapse + @property + def on_collapse(self): + return self._get_event_handler("collapse") + + @on_collapse.setter + def on_collapse(self, handler): + self._add_event_handler("collapse", handler) + + # value + @property + def value(self): + return self._get_attr("value") + + @value.setter + def value(self, value): + self._set_attr("value", value) + + def _get_children(self): + return self.__items + + +class Item(Control): + def __init__( + self, + id=None, + ref=None, + key=None, + text=None, + icon=None, + icon_color=None, + url=None, + items=None, + new_window=None, + expanded=None, + visible=None, + disabled=None, + data=None, + ): + Control.__init__( + self, id=id, ref=ref, visible=visible, disabled=disabled, data=data + ) + # key and text are optional for group item but key or text are required for level 2 and deeper items + # assert key != None or text != None, "key or text must be specified" + self.key = key + self.text = text + self.icon = icon + self.icon_color = icon_color + self.url = url + self.new_window = new_window + self.expanded = expanded + self.__items = [] + if items != None: + for item in items: + self.__items.append(item) + + def _get_control_name(self): + return "item" + + # items + @property + def items(self): + return self.__items + + @items.setter + def items(self, value): + self.__items = value + + # key + @property + def key(self): + return self._get_attr("key") + + @key.setter + def key(self, value): + self._set_attr("key", value) + + # text + @property + def text(self): + return self._get_attr("text") + + @text.setter + def text(self, value): + self._set_attr("text", value) + + # icon + @property + def icon(self): + return self._get_attr("icon") + + @icon.setter + def icon(self, value): + self._set_attr("icon", value) + + # icon_color + @property + def icon_color(self): + return self._get_attr("iconColor") + + @icon_color.setter + def icon_color(self, value): + self._set_attr("iconColor", value) + + # url + @property + def url(self): + return self._get_attr("url") + + @url.setter + def url(self, value): + self._set_attr("url", value) + + # new_window + @property + def new_window(self): + return self._get_attr("newWindow", data_type="bool", def_value=False) + + @new_window.setter + @beartype + def new_window(self, value: Optional[bool]): + self._set_attr("newWindow", value) + + # expanded + @property + def expanded(self): + return self._get_attr("expanded", data_type="bool", def_value=False) + + @expanded.setter + @beartype + def expanded(self, value: Optional[bool]): + self._set_attr("expanded", value) + + def _get_children(self): + return self.__items diff --git a/sdk/python/flet/page.py b/sdk/python/flet/page.py new file mode 100644 index 0000000000..830d0861ee --- /dev/null +++ b/sdk/python/flet/page.py @@ -0,0 +1,512 @@ +import json +import logging +import threading + +from beartype import beartype +from beartype.typing import List +from beartype.typing import Optional +from flet import constants +from flet.connection import Connection +from flet.control import Control +from flet.control_event import ControlEvent +from flet.protocol import Command + +try: + from typing import Literal +except: + from typing_extensions import Literal + + +Align = Literal[ + None, + "start", + "end", + "center", + "space-between", + "space-around", + "space-evenly", + "baseline", + "stretch", +] + +THEME = Literal[None, "light", "dark"] + + +class Page(Control): + def __init__(self, conn: Connection, session_id): + Control.__init__(self, id="page") + + self._conn = conn + self._session_id = session_id + self._controls = [] # page controls + self._index = {} # index with all page controls + self._index[self.id] = self + self._last_event = None + self._event_available = threading.Event() + self._fetch_page_details() + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self.close() + + def get_control(self, id): + return self._index.get(id) + + def _get_children(self): + return self._controls + + def _fetch_page_details(self): + values = self._conn.send_commands( + self._conn.page_name, + self._session_id, + [ + Command(0, "get", ["page", "hash"], None, None, None), + Command(0, "get", ["page", "winwidth"], None, None, None), + Command(0, "get", ["page", "winheight"], None, None, None), + Command(0, "get", ["page", "userauthprovider"], None, None, None), + Command(0, "get", ["page", "userid"], None, None, None), + Command(0, "get", ["page", "userlogin"], None, None, None), + Command(0, "get", ["page", "username"], None, None, None), + Command(0, "get", ["page", "useremail"], None, None, None), + Command(0, "get", ["page", "userclientip"], None, None, None), + ], + ).results + self._set_attr("hash", values[0], False) + self._set_attr("winwidth", values[1], False) + self._set_attr("winheight", values[2], False) + self._set_attr("userauthprovider", values[3], False) + self._set_attr("userid", values[4], False) + self._set_attr("userlogin", values[5], False) + self._set_attr("username", values[6], False) + self._set_attr("useremail", values[7], False) + self._set_attr("userclientip", values[8], False) + + def update(self, *controls): + with self._lock: + if len(controls) == 0: + return self.__update(self) + else: + return self.__update(*controls) + + def __update(self, *controls): + added_controls = [] + commands = [] + + # build commands + for control in controls: + control.build_update_commands(self._index, added_controls, commands) + + if len(commands) == 0: + return + + # execute commands + results = self._conn.send_commands( + self._conn.page_name, self._session_id, commands + ).results + + if len(results) > 0: + n = 0 + for line in results: + for id in line.split(" "): + added_controls[n]._Control__uid = id + added_controls[n].page = self + + # add to index + self._index[id] = added_controls[n] + n += 1 + + def add(self, *controls): + with self._lock: + self._controls.extend(controls) + return self.__update(self) + + def insert(self, at, *controls): + with self._lock: + n = at + for control in controls: + self._controls.insert(n, control) + n += 1 + return self.__update(self) + + def remove(self, *controls): + with self._lock: + for control in controls: + self._controls.remove(control) + return self.__update(self) + + def remove_at(self, index): + with self._lock: + self._controls.pop(index) + return self.__update(self) + + def clean(self): + with self._lock: + self._previous_children.clear() + for child in self._get_children(): + self._remove_control_recursively(self._index, child) + self._controls.clear() + return self._send_command("clean", [self.uid]) + + def error(self, message=""): + with self._lock: + self._send_command("error", [message]) + + def on_event(self, e): + logging.info(f"page.on_event: {e.target} {e.name} {e.data}") + + with self._lock: + if e.target == "page" and e.name == "change": + for props in json.loads(e.data): + id = props["i"] + if id in self._index: + for name in props: + if name != "i": + self._index[id]._set_attr( + name, props[name], dirty=False + ) + + elif e.target in self._index: + self._last_event = ControlEvent( + e.target, e.name, e.data, self._index[e.target], self + ) + handler = self._index[e.target].event_handlers.get(e.name) + if handler: + t = threading.Thread( + target=handler, args=(self._last_event,), daemon=True + ) + t.start() + self._event_available.set() + + def wait_event(self): + self._event_available.clear() + self._event_available.wait() + return self._last_event + + def show_signin(self, auth_providers="*", auth_groups=False, allow_dismiss=False): + with self._lock: + self.signin = auth_providers + self.signin_groups = auth_groups + self.signin_allow_dismiss = allow_dismiss + self.__update(self) + + while True: + e = self.wait_event() + if e.control == self and e.name.lower() == "signin": + return True + elif e.control == self and e.name.lower() == "dismisssignin": + return False + + def signout(self): + return self._send_command("signout", None) + + def can_access(self, users_and_groups): + return ( + self._send_command("canAccess", [users_and_groups]).result.lower() == "true" + ) + + def close(self): + if self._session_id == constants.ZERO_SESSION: + self._conn.close() + + def _send_command(self, name: str, values: List[str]): + return self._conn.send_command( + self._conn.page_name, + self._session_id, + Command(0, name, values, None, None, None), + ) + + # url + @property + def url(self): + return self._conn.page_url + + # name + @property + def name(self): + return self._conn.page_name + + # connection + @property + def connection(self): + return self._conn + + # index + @property + def index(self): + return self._index + + # session_id + @property + def session_id(self): + return self._session_id + + # controls + @property + def controls(self): + return self._controls + + @controls.setter + def controls(self, value): + self._controls = value + + # title + @property + def title(self): + return self._get_attr("title") + + @title.setter + def title(self, value): + self._set_attr("title", value) + + # vertical_fill + @property + def vertical_fill(self): + return self._get_attr("verticalFill", data_type="bool", def_value=False) + + @vertical_fill.setter + @beartype + def vertical_fill(self, value: Optional[bool]): + self._set_attr("verticalFill", value) + + # horizontal_align + @property + def horizontal_align(self): + return self._get_attr("horizontalAlign") + + @horizontal_align.setter + @beartype + def horizontal_align(self, value: Align): + self._set_attr("horizontalAlign", value) + + # vertical_align + @property + def vertical_align(self): + return self._get_attr("verticalAlign") + + @vertical_align.setter + @beartype + def vertical_align(self, value: Align): + self._set_attr("verticalAlign", value) + + # gap + @property + def gap(self): + return self._get_attr("gap") + + @gap.setter + @beartype + def gap(self, value: Optional[int]): + self._set_attr("gap", value) + + # padding + @property + def padding(self): + return self._get_attr("padding") + + @padding.setter + def padding(self, value): + self._set_attr("padding", value) + + # bgcolor + @property + def bgcolor(self): + return self._get_attr("bgcolor") + + @bgcolor.setter + def bgcolor(self, value): + self._set_attr("bgcolor", value) + + # theme + @property + def theme(self): + return self._get_attr("theme") + + @theme.setter + @beartype + def theme(self, value: THEME): + self._set_attr("theme", value) + + # theme_primary_color + @property + def theme_primary_color(self): + return self._get_attr("themePrimaryColor") + + @theme_primary_color.setter + def theme_primary_color(self, value): + self._set_attr("themePrimaryColor", value) + + # theme_text_color + @property + def theme_text_color(self): + return self._get_attr("themeTextColor") + + @theme_text_color.setter + def theme_text_color(self, value): + self._set_attr("themeTextColor", value) + + # theme_background_color + @property + def theme_background_color(self): + return self._get_attr("themeBackgroundColor") + + @theme_background_color.setter + def theme_background_color(self, value): + self._set_attr("themeBackgroundColor", value) + + # hash + @property + def hash(self): + return self._get_attr("hash") + + @hash.setter + def hash(self, value): + self._set_attr("hash", value) + + # win_width + @property + def win_width(self): + w = self._get_attr("winwidth") + if w != None and w != "": + return int(w) + return 0 + + # win_height + @property + def win_height(self): + h = self._get_attr("winheight") + if h != None and h != "": + return int(h) + return 0 + + # signin + @property + def signin(self): + return self._get_attr("signin") + + @signin.setter + def signin(self, value): + self._set_attr("signin", value) + + # signin_allow_dismiss + @property + def signin_allow_dismiss(self): + return self._get_attr("signinAllowDismiss", data_type="bool", def_value=False) + + @signin_allow_dismiss.setter + @beartype + def signin_allow_dismiss(self, value: Optional[bool]): + self._set_attr("signinAllowDismiss", value) + + # signin_groups + @property + def signin_groups(self): + return self._get_attr("signinGroups", data_type="bool", def_value=False) + + @signin_groups.setter + @beartype + def signin_groups(self, value: Optional[bool]): + self._set_attr("signinGroups", value) + + # user_auth_provider + @property + def user_auth_provider(self): + return self._get_attr("userauthprovider") + + # user_id + @property + def user_id(self): + return self._get_attr("userId") + + # user_login + @property + def user_login(self): + return self._get_attr("userLogin") + + # user_name + @property + def user_name(self): + return self._get_attr("userName") + + # user_email + @property + def user_email(self): + return self._get_attr("userEmail") + + # user_client_ip + @property + def user_client_ip(self): + return self._get_attr("userClientIP") + + # on_signin + @property + def on_signin(self): + return self._get_event_handler("signin") + + @on_signin.setter + def on_signin(self, handler): + self._add_event_handler("signin", handler) + + # on_dismiss_signin + @property + def on_dismiss_signin(self): + return self._get_event_handler("dismissSignin") + + @on_dismiss_signin.setter + def on_dismiss_signin(self, handler): + self._add_event_handler("dismissSignin", handler) + + # on_signout + @property + def on_signout(self): + return self._get_event_handler("signout") + + @on_signout.setter + def on_signout(self, handler): + self._add_event_handler("signout", handler) + + # on_close + @property + def on_close(self): + return self._get_event_handler("close") + + @on_close.setter + def on_close(self, handler): + self._add_event_handler("close", handler) + + # on_hash_change + @property + def on_hash_change(self): + return self._get_event_handler("hashChange") + + @on_hash_change.setter + def on_hash_change(self, handler): + self._add_event_handler("hashChange", handler) + + # on_resize + @property + def on_resize(self): + return self._get_event_handler("resize") + + @on_resize.setter + def on_resize(self, handler): + self._add_event_handler("resize", handler) + + # on_connect + @property + def on_connect(self): + return self._get_event_handler("connect") + + @on_connect.setter + def on_connect(self, handler): + self._add_event_handler("connect", handler) + + # on_disconnect + @property + def on_disconnect(self): + return self._get_event_handler("disconnect") + + @on_disconnect.setter + def on_disconnect(self, handler): + self._add_event_handler("disconnect", handler) diff --git a/sdk/python/flet/panel.py b/sdk/python/flet/panel.py new file mode 100644 index 0000000000..3e68262af9 --- /dev/null +++ b/sdk/python/flet/panel.py @@ -0,0 +1,202 @@ +from typing import Optional + +from beartype import beartype + +from flet.control import Control + +try: + from typing import Literal +except: + from typing_extensions import Literal + + +PanelType = Literal[ + None, + "small", + "smallLeft", + "medium", + "large", + "largeFixed", + "extraLarge", + "fluid", + "custom", + "customLeft", +] + + +class Panel(Control): + def __init__( + self, + id=None, + ref=None, + open=None, + title=None, + type: PanelType = None, + auto_dismiss=None, + light_dismiss=None, + width=None, + blocking=None, + data=None, + controls=None, + footer=None, + on_dismiss=None, + height=None, + padding=None, + margin=None, + visible=None, + disabled=None, + ): + + Control.__init__( + self, + id=id, + ref=ref, + width=width, + height=height, + padding=padding, + margin=margin, + visible=visible, + disabled=disabled, + data=data, + ) + + self.open = open + self.title = title + self.type = type + self.auto_dismiss = auto_dismiss + self.light_dismiss = light_dismiss + self.width = width + self.blocking = blocking + self.on_dismiss = on_dismiss + self.__footer = Footer(controls=footer) + self.__controls = [] + if controls != None: + for control in controls: + self.__controls.append(control) + + def _get_control_name(self): + return "panel" + + # controls + @property + def controls(self): + return self.__controls + + @controls.setter + def controls(self, value): + self.__controls = value + + # footer + @property + def footer(self): + return self.__footer + + # on_dismiss + @property + def on_dismiss(self): + return self._get_event_handler("dismiss") + + @on_dismiss.setter + def on_dismiss(self, handler): + self._add_event_handler("dismiss", handler) + + # open + @property + def open(self): + return self._get_attr("open", data_type="bool", def_value=False) + + @open.setter + @beartype + def open(self, value: Optional[bool]): + self._set_attr("open", value) + + # title + @property + def title(self): + return self._get_attr("title") + + @title.setter + def title(self, value): + self._set_attr("title", value) + + # type + @property + def type(self): + return self._get_attr("type") + + @type.setter + @beartype + def type(self, value: PanelType): + self._set_attr("type", value) + + # auto_dismiss + @property + def auto_dismiss(self): + return self._get_attr("autoDismiss", data_type="bool", def_value=True) + + @auto_dismiss.setter + @beartype + def auto_dismiss(self, value: Optional[bool]): + self._set_attr("autoDismiss", value) + + # light_dismiss + @property + def light_dismiss(self): + return self._get_attr("lightDismiss", data_type="bool", def_value=False) + + @light_dismiss.setter + @beartype + def light_dismiss(self, value: Optional[bool]): + self._set_attr("lightDismiss", value) + + # width + @property + def width(self): + return self._get_attr("Width") + + @width.setter + def width(self, value): + self._set_attr("Width", value) + + # blocking + @property + def blocking(self): + return self._get_attr("blocking", data_type="bool", def_value=False) + + @blocking.setter + @beartype + def blocking(self, value: Optional[bool]): + self._set_attr("blocking", value) + + def _get_children(self): + result = [] + if self.__controls and len(self.__controls) > 0: + for control in self.__controls: + result.append(control) + result.append(self.__footer) + return result + + +class Footer(Control): + def __init__(self, id=None, ref=None, controls=None): + Control.__init__(self, id=id, ref=ref) + + self.__controls = [] + if controls != None: + for control in controls: + self.__controls.append(control) + + # controls + @property + def controls(self): + return self.__controls + + @controls.setter + def controls(self, value): + self.__controls = value + + def _get_control_name(self): + return "footer" + + def _get_children(self): + return self.__controls diff --git a/sdk/python/flet/persona.py b/sdk/python/flet/persona.py new file mode 100644 index 0000000000..ab29f32c20 --- /dev/null +++ b/sdk/python/flet/persona.py @@ -0,0 +1,181 @@ +from typing import Optional + +from beartype import beartype + +from flet.control import Control + +try: + from typing import Literal +except: + from typing_extensions import Literal + + +Size = Literal[None, 8, 24, 32, 40, 48, 56, 72, 100, 120] + +Presence = Literal[None, "none", "offline", "online", "away", "blocked", "busy", "dnd"] + +InitialsColor = Literal[ + None, + "blue", + "burgundy", + "coolGray", + "cyan", + "darkBlue", + "darkGreen", + "darkRed", + "gold", + "green", + "lightBlue", + "lightGreen", + "lightPink", + "lightRed", + "magenta", + "orange", + "pink", + "purple", + "rust", + "teal", + "transparent", + "violet", + "warmGray", +] + + +class Persona(Control): + def __init__( + self, + text=None, + id=None, + ref=None, + image_url=None, + image_alt=None, + initials_color: InitialsColor = None, + initials_text_color=None, + secondary_text=None, + tertiary_text=None, + optional_text=None, + size: Size = None, + presence: Presence = None, + hide_details=None, + visible=None, + ): + + Control.__init__(self, id=id, ref=ref, visible=visible) + + self.text = text + self.image_url = image_url + self.image_alt = image_alt + self.initials_color = initials_color + self.initials_text_color = initials_text_color + self.secondary_text = secondary_text + self.tertiary_text = tertiary_text + self.optional_text = optional_text + self.size = size + self.presence = presence + self.hide_details = hide_details + + def _get_control_name(self): + return "persona" + + # text + @property + def text(self): + return self._get_attr("text") + + @text.setter + def text(self, value): + self._set_attr("text", value) + + # image_url + @property + def image_url(self): + return self._get_attr("imageurl") + + @image_url.setter + def image_url(self, value): + self._set_attr("imageurl", value) + + # image_alt + @property + def image_alt(self): + return self._get_attr("imagealt") + + @image_alt.setter + def image_alt(self, value): + self._set_attr("imagealt", value) + + # initials_color + @property + def initials_color(self): + return self._get_attr("initialscolor") + + @initials_color.setter + @beartype + def initials_color(self, value: InitialsColor): + self._set_attr("initialscolor", value) + + # initials_text_color + @property + def initials_text_color(self): + return self._get_attr("initialstextcolor") + + @initials_text_color.setter + def initials_text_color(self, value): + self._set_attr("initialstextcolor", value) + + # secondary_text + @property + def secondary_text(self): + return self._get_attr("secondarytext") + + @secondary_text.setter + def secondary_text(self, value): + self._set_attr("secondarytext", value) + + # tertiary_text + @property + def tertiary_text(self): + return self._get_attr("tertiarytext") + + @tertiary_text.setter + def tertiary_text(self, value): + self._set_attr("tertiarytext", value) + + # optional_text + @property + def optional_text(self): + return self._get_attr("optionaltext") + + @optional_text.setter + def optional_text(self, value): + self._set_attr("optionaltext", value) + + # size + @property + def size(self): + return self._get_attr("size") + + @size.setter + @beartype + def size(self, value: Size): + self._set_attr("size", value) + + # presence + @property + def presence(self): + return self._get_attr("presence") + + @presence.setter + @beartype + def presence(self, value: Presence): + self._set_attr("presence", value) + + # hide_details + @property + def hide_details(self): + return self._get_attr("hidedetails", data_type="bool", def_value=False) + + @hide_details.setter + @beartype + def hide_details(self, value: Optional[bool]): + self._set_attr("hidedetails", value) diff --git a/sdk/python/flet/piechart.py b/sdk/python/flet/piechart.py new file mode 100644 index 0000000000..999adb1a5f --- /dev/null +++ b/sdk/python/flet/piechart.py @@ -0,0 +1,173 @@ +from typing import Optional, Union + +from beartype import beartype + +from flet.control import Control + + +class PieChart(Control): + def __init__( + self, + id=None, + ref=None, + legend=None, + tooltips=None, + inner_value=None, + inner_radius=None, + points=None, + width=None, + height=None, + padding=None, + margin=None, + visible=None, + disabled=None, + ): + + Control.__init__( + self, + id=id, + ref=ref, + width=width, + height=height, + padding=padding, + margin=margin, + visible=visible, + disabled=disabled, + ) + + self.__data = Data(points=points) + self.legend = legend + self.tooltips = tooltips + self.inner_value = inner_value + self.inner_radius = inner_radius + + def _get_control_name(self): + return "piechart" + + # data + @property + def points(self): + return self.__data.points + + @points.setter + def points(self, value): + self.__data.points = value + + # legend + @property + def legend(self): + return self._get_attr("legend", data_type="bool", def_value=False) + + @legend.setter + @beartype + def legend(self, value: Optional[bool]): + self._set_attr("legend", value) + + # tooltips + @property + def tooltips(self): + return self._get_attr("tooltips", data_type="bool", def_value=False) + + @tooltips.setter + @beartype + def tooltips(self, value: Optional[bool]): + self._set_attr("tooltips", value) + + # inner_value + @property + def inner_value(self): + return self._get_attr("innerValue") + + @inner_value.setter + def inner_value(self, value): + self._set_attr("innerValue", value) + + # inner_radius + @property + def inner_radius(self): + return self._get_attr("innerRadius") + + @inner_radius.setter + @beartype + def inner_radius(self, value: Union[None, int, float]): + self._set_attr("innerRadius", value) + + def _get_children(self): + return [self.__data] + + +class Data(Control): + def __init__(self, id=None, ref=None, points=None): + Control.__init__(self, id=id, ref=ref) + + self.__points = [] + if points != None: + for point in points: + self.__points.append(point) + + # points + @property + def points(self): + return self.__points + + @points.setter + def points(self, value): + self.__points = value + + def _get_control_name(self): + return "data" + + def _get_children(self): + return self.__points + + +class Point(Control): + def __init__( + self, id=None, ref=None, value=None, legend=None, color=None, tooltip=None + ): + Control.__init__(self, id=id, ref=ref) + + self.value = value + self.legend = legend + self.color = color + self.tooltip = tooltip + + def _get_control_name(self): + return "p" + + # value + @property + def value(self): + return self._get_attr("value") + + @value.setter + @beartype + def value(self, value: Union[None, int, float]): + self._set_attr("value", value) + + # legend + @property + def legend(self): + return self._get_attr("legend") + + @legend.setter + def legend(self, value): + self._set_attr("legend", value) + + # color + @property + def color(self): + return self._get_attr("color") + + @color.setter + def color(self, value): + self._set_attr("color", value) + + # tooltip + @property + def tooltip(self): + return self._get_attr("tooltip") + + @tooltip.setter + def tooltip(self, value): + self._set_attr("tooltip", value) diff --git a/sdk/python/flet/progress.py b/sdk/python/flet/progress.py new file mode 100644 index 0000000000..d4a6c88f61 --- /dev/null +++ b/sdk/python/flet/progress.py @@ -0,0 +1,78 @@ +from typing import Optional + +from beartype import beartype + +from flet.control import Control + + +class Progress(Control): + def __init__( + self, + label=None, + id=None, + ref=None, + description=None, + value=None, + bar_height=None, + width=None, + height=None, + padding=None, + margin=None, + visible=None, + disabled=None, + ): + Control.__init__( + self, + id=id, + ref=ref, + width=width, + height=height, + padding=padding, + margin=margin, + visible=visible, + disabled=disabled, + ) + self.value = value + self.description = description + self.label = label + self.bar_height = bar_height + + def _get_control_name(self): + return "progress" + + # value + @property + def value(self): + return self._get_attr("value") + + @value.setter + @beartype + def value(self, value: Optional[int]): + self._set_attr("value", value) + + # description + @property + def description(self): + return self._get_attr("description") + + @description.setter + def description(self, value): + self._set_attr("description", value) + + # bar_height + @property + def bar_height(self): + return self._get_attr("barheight") + + @bar_height.setter + def bar_height(self, value): + self._set_attr("barheight", value) + + # label + @property + def label(self): + return self._get_attr("label") + + @label.setter + def label(self, value): + self._set_attr("label", value) diff --git a/sdk/python/flet/protocol.py b/sdk/python/flet/protocol.py new file mode 100644 index 0000000000..4f60e29118 --- /dev/null +++ b/sdk/python/flet/protocol.py @@ -0,0 +1,87 @@ +from dataclasses import dataclass, field +from typing import Dict, List +from typing import Optional + + +class Actions: + REGISTER_HOST_CLIENT = "registerHostClient" + SESSION_CREATED = "sessionCreated" + PAGE_COMMAND_FROM_HOST = "pageCommandFromHost" + PAGE_COMMANDS_BATCH_FROM_HOST = "pageCommandsBatchFromHost" + PAGE_EVENT_TO_HOST = "pageEventToHost" + + +@dataclass +class Command: + indent: int + name: Optional[str] + values: List[str] = field(default_factory=list) + attrs: Dict[str, str] = field(default_factory=dict) + lines: List[str] = field(default_factory=list) + commands: List[any] = field(default_factory=list) + + +@dataclass +class Message: + id: str + action: str + payload: any + + +@dataclass +class PageCommandRequestPayload: + pageName: str + sessionID: str + command: Command + + +@dataclass +class PageCommandResponsePayload: + result: str + error: str + + +@dataclass +class PageCommandsBatchRequestPayload: + pageName: str + sessionID: str + commands: List[Command] + + +@dataclass +class PageCommandsBatchResponsePayload: + results: List[str] + error: str + + +@dataclass +class PageEventPayload: + pageName: str + sessionID: str + eventTarget: str + eventName: str + eventData: str + + +@dataclass +class RegisterHostClientRequestPayload: + hostClientID: str + pageName: str + isApp: bool + update: bool + authToken: str + permissions: str + + +@dataclass +class RegisterHostClientResponsePayload: + hostClientID: str + pageName: str + sessionID: str + error: str + + +@dataclass +class PageSessionCreatedPayload: + pageName: str + sessionID: str diff --git a/sdk/python/flet/reconnecting_websocket.py b/sdk/python/flet/reconnecting_websocket.py new file mode 100644 index 0000000000..cc732e3d9b --- /dev/null +++ b/sdk/python/flet/reconnecting_websocket.py @@ -0,0 +1,102 @@ +import logging +import random +import threading + +import websocket + +from flet.utils import is_localhost_url + +_REMOTE_CONNECT_TIMEOUT_SEC = 5 +_LOCAL_CONNECT_TIMEOUT_SEC = 0.2 + + +class ReconnectingWebSocket: + def __init__(self, url) -> None: + self._url = url + self._on_connect_handler = None + self._on_failed_connect_handler = None + self._on_message_handler = None + self.connected = threading.Event() + self.exit = threading.Event() + self.retry = 0 + websocket.setdefaulttimeout( + _LOCAL_CONNECT_TIMEOUT_SEC + if is_localhost_url(url) + else _REMOTE_CONNECT_TIMEOUT_SEC + ) + + @property + def on_connect(self, handler): + return self._on_connect_handler + + @on_connect.setter + def on_connect(self, handler): + self._on_connect_handler = handler + + @property + def on_failed_connect(self, handler): + return self._on_failed_connect_handler + + @on_failed_connect.setter + def on_failed_connect(self, handler): + self._on_failed_connect_handler = handler + + @property + def on_message(self, handler): + return self._on_message_handler + + @on_message.setter + def on_message(self, handler): + self._on_message_handler = handler + + def _on_open(self, wsapp) -> None: + logging.info(f"Successfully connected to {self._url}") + self.connected.set() + self.retry = 0 + if self._on_connect_handler != None: + th = threading.Thread(target=self._on_connect_handler, args=(), daemon=True) + th.start() + + def _on_message(self, wsapp, data) -> None: + if self._on_message_handler != None: + self._on_message_handler(data) + + def connect(self) -> None: + self.wsapp = websocket.WebSocketApp( + self._url, on_message=self._on_message, on_open=self._on_open + ) + th = threading.Thread(target=self._connect_loop, args=(), daemon=True) + th.start() + + def send(self, message) -> None: + self.connected.wait() + self.wsapp.send(message) + + def close(self) -> None: + self.exit.set() + self.wsapp.close() + + # TODO: Can't do CTRL+C while it sleeps between re-connects + # Change to Event: https://stackoverflow.com/questions/5114292/break-interrupt-a-time-sleep-in-python + def _connect_loop(self): + while not self.exit.is_set(): + logging.info(f"Connecting Flet Server at {self._url}...") + r = self.wsapp.run_forever() + logging.debug(f"Exited run_forever()") + self.connected.clear() + if r != True: + return + + if self.retry == 0 and self._on_failed_connect_handler != None: + th = threading.Thread( + target=self._on_failed_connect_handler, args=(), daemon=True + ) + th.start() + + backoff_in_seconds = 1 + sleep = 0.1 + if not is_localhost_url(self._url): + sleep = backoff_in_seconds * 2**self.retry + random.uniform(0, 1) + logging.info(f"Reconnecting Flet Server in {sleep} seconds") + self.exit.wait(sleep) + self.retry += 1 diff --git a/sdk/python/flet/ref.py b/sdk/python/flet/ref.py new file mode 100644 index 0000000000..33df0977e6 --- /dev/null +++ b/sdk/python/flet/ref.py @@ -0,0 +1,16 @@ +from typing import Generic, TypeVar + +T = TypeVar("T") + + +class Ref(Generic[T]): + def __init__(self): + self._current: T = None + + @property + def current(self) -> T: + return self._current + + @current.setter + def current(self, value: T): + self._current = value diff --git a/sdk/python/flet/row.py b/sdk/python/flet/row.py new file mode 100644 index 0000000000..46351d72fb --- /dev/null +++ b/sdk/python/flet/row.py @@ -0,0 +1,49 @@ +from typing import Optional + +from beartype import beartype + +from flet.control import Control + + +class Row(Control): + def __init__( + self, + controls=None, + id=None, + ref=None, + visible=None, + disabled=None, + data=None, + ): + Control.__init__( + self, + id=id, + ref=ref, + visible=visible, + disabled=disabled, + data=data, + ) + + self.__controls = [] + if controls != None: + for control in controls: + self.__controls.append(control) + + def _get_control_name(self): + return "row" + + def clean(self): + Control.clean(self) + self.__controls.clear() + + # controls + @property + def controls(self): + return self.__controls + + @controls.setter + def controls(self, value): + self.__controls = value + + def _get_children(self): + return self.__controls diff --git a/sdk/python/flet/searchbox.py b/sdk/python/flet/searchbox.py new file mode 100644 index 0000000000..5aa5306247 --- /dev/null +++ b/sdk/python/flet/searchbox.py @@ -0,0 +1,164 @@ +from typing import Optional + +from beartype import beartype + +from flet.control import Control + + +class SearchBox(Control): + def __init__( + self, + id=None, + ref=None, + value=None, + placeholder=None, + underlined=None, + icon=None, + icon_color=None, + data=None, + on_search=None, + on_clear=None, + on_change=None, + on_focus=None, + on_blur=None, + width=None, + height=None, + padding=None, + margin=None, + visible=None, + disabled=None, + focused=None, + ): + + Control.__init__( + self, + id=id, + ref=ref, + width=width, + height=height, + padding=padding, + margin=margin, + visible=visible, + disabled=disabled, + data=data, + ) + + self.value = value + self.placeholder = placeholder + self.underlined = underlined + self.icon = icon + self.icon_color = icon_color + self.focused = focused + self.on_search = on_search + self.on_clear = on_clear + self.on_change = on_change + self.on_focus = on_focus + self.on_blur = on_blur + + def _get_control_name(self): + return "searchbox" + + # on_search + @property + def on_search(self): + return self._get_event_handler("search") + + @on_search.setter + def on_search(self, handler): + self._add_event_handler("search", handler) + + # on_clear + @property + def on_clear(self): + return self._get_event_handler("clear") + + @on_clear.setter + def on_clear(self, handler): + self._add_event_handler("clear", handler) + + # on_change + @property + def on_change(self): + return self._get_event_handler("change") + + @on_change.setter + def on_change(self, handler): + self._add_event_handler("change", handler) + if handler != None: + self._set_attr("onchange", True) + else: + self._set_attr("onchange", False) + + # value + @property + def value(self): + return self._get_attr("value", def_value="") + + @value.setter + def value(self, value): + self._set_attr("value", value) + + # placeholder + @property + def placeholder(self): + return self._get_attr("placeholder") + + @placeholder.setter + def placeholder(self, value): + self._set_attr("placeholder", value) + + # underlined + @property + def underlined(self): + return self._get_attr("underlined", data_type="bool", def_value=False) + + @underlined.setter + @beartype + def underlined(self, value: Optional[bool]): + self._set_attr("underlined", value) + + # icon + @property + def icon(self): + return self._get_attr("icon") + + @icon.setter + def icon(self, value): + self._set_attr("icon", value) + + # icon_color + @property + def icon_color(self): + return self._get_attr("iconColor") + + @icon_color.setter + def icon_color(self, value): + self._set_attr("iconColor", value) + + # focused + @property + def focused(self): + return self._get_attr("focused", data_type="bool", def_value=False) + + @focused.setter + @beartype + def focused(self, value: Optional[bool]): + self._set_attr("focused", value) + + # on_focus + @property + def on_focus(self): + return self._get_event_handler("focus") + + @on_focus.setter + def on_focus(self, handler): + self._add_event_handler("focus", handler) + + # on_blur + @property + def on_blur(self): + return self._get_event_handler("blur") + + @on_blur.setter + def on_blur(self, handler): + self._add_event_handler("blur", handler) diff --git a/sdk/python/flet/slider.py b/sdk/python/flet/slider.py new file mode 100644 index 0000000000..01c58cf1b9 --- /dev/null +++ b/sdk/python/flet/slider.py @@ -0,0 +1,174 @@ +from typing import Optional, Union + +from beartype import beartype + +from flet.control import Control + + +class Slider(Control): + def __init__( + self, + label=None, + id=None, + ref=None, + value=None, + min=None, + max=None, + step=None, + show_value=None, + value_format=None, + vertical=None, + focused=None, + data=None, + on_change=None, + on_focus=None, + on_blur=None, + width=None, + height=None, + padding=None, + margin=None, + visible=None, + disabled=None, + ): + Control.__init__( + self, + id=id, + ref=ref, + width=width, + height=height, + padding=padding, + margin=margin, + visible=visible, + disabled=disabled, + data=data, + ) + self.value = value + self.label = label + self.min = min + self.max = max + self.step = step + self.show_value = show_value + self.value_format = value_format + self.vertical = vertical + self.focused = focused + self.on_change = on_change + self.on_focus = on_focus + self.on_blur = on_blur + + def _get_control_name(self): + return "slider" + + # on_change + @property + def on_change(self): + return self._get_event_handler("change") + + @on_change.setter + def on_change(self, handler): + self._add_event_handler("change", handler) + + # value + @property + def value(self): + return self._get_attr("value", data_type="float") + + @value.setter + @beartype + def value(self, value: Union[None, int, float]): + self._set_attr("value", value) + + # label + @property + def label(self): + return self._get_attr("label") + + @label.setter + def label(self, value): + self._set_attr("label", value) + + # min + @property + def min(self): + return self._get_attr("min") + + @min.setter + @beartype + def min(self, value: Union[None, int, float]): + self._set_attr("min", value) + + # max + @property + def max(self): + return self._get_attr("max") + + @max.setter + @beartype + def max(self, value: Union[None, int, float]): + self._set_attr("max", value) + + # step + @property + def step(self): + return self._get_attr("step") + + @step.setter + @beartype + def step(self, value: Union[None, int, float]): + self._set_attr("step", value) + + # show_value + @property + def show_value(self): + return self._get_attr("showValue", data_type="bool", def_value=False) + + @show_value.setter + @beartype + def show_value(self, value: Optional[bool]): + self._set_attr("showValue", value) + + # value_format + @property + def value_format(self): + return self._get_attr("valueFormat") + + @value_format.setter + def value_format(self, value): + self._set_attr("valueFormat", value) + + # vertical + @property + def vertical(self): + return self._get_attr("vertical", data_type="bool", def_value=False) + + @vertical.setter + @beartype + def vertical(self, value: Optional[bool]): + self._set_attr("vertical", value) + + # focused + @property + def focused(self): + return self._get_attr("focused", data_type="bool", def_value=False) + + @focused.setter + @beartype + def focused(self, value: Optional[bool]): + self._set_attr("focused", value) + + # on_focus + @property + def on_focus(self): + return self._get_event_handler("focus") + + @on_focus.setter + def on_focus(self, handler): + self._add_event_handler("focus", handler) + + # on_blur + @property + def on_blur(self): + return self._get_event_handler("blur") + + @on_blur.setter + def on_blur(self, handler): + self._add_event_handler("blur", handler) diff --git a/sdk/python/flet/spinbutton.py b/sdk/python/flet/spinbutton.py new file mode 100644 index 0000000000..84f95742d7 --- /dev/null +++ b/sdk/python/flet/spinbutton.py @@ -0,0 +1,170 @@ +from typing import Optional, Union + +from beartype import beartype + +from flet.control import Control + +try: + from typing import Literal +except: + from typing_extensions import Literal + + +Position = Literal[None, "left", "top", "right", "bottom"] + + +class SpinButton(Control): + def __init__( + self, + label=None, + id=None, + ref=None, + value=None, + min=None, + max=None, + step=None, + icon=None, + label_position: Position = None, + focused=None, + data=None, + on_change=None, + on_focus=None, + on_blur=None, + width=None, + height=None, + padding=None, + margin=None, + visible=None, + disabled=None, + ): + Control.__init__( + self, + id=id, + ref=ref, + width=width, + height=height, + padding=padding, + margin=margin, + visible=visible, + disabled=disabled, + data=data, + ) + self.value = value + self.label = label + self.label_position = label_position + self.min = min + self.max = max + self.step = step + self.icon = icon + self.focused = focused + self.on_change = on_change + self.on_focus = on_focus + self.on_blur = on_blur + + def _get_control_name(self): + return "spinbutton" + + # on_change + @property + def on_change(self): + return self._get_event_handler("change") + + @on_change.setter + def on_change(self, handler): + self._add_event_handler("change", handler) + + # value + @property + def value(self): + return self._get_attr("value", data_type="float") + + @value.setter + @beartype + def value(self, value: Union[None, int, float]): + self._set_attr("value", value) + + # label + @property + def label(self): + return self._get_attr("label") + + @label.setter + def label(self, value): + self._set_attr("label", value) + + # label_position + @property + def label_position(self): + return self._get_attr("labelposition") + + @label_position.setter + @beartype + def label_position(self, value: Position): + self._set_attr("labelposition", value) + + # min + @property + def min(self): + return self._get_attr("min") + + @min.setter + @beartype + def min(self, value: Union[None, int, float]): + self._set_attr("min", value) + + # max + @property + def max(self): + return self._get_attr("max") + + @max.setter + @beartype + def max(self, value: Union[None, int, float]): + self._set_attr("max", value) + + # step + @property + def step(self): + return self._get_attr("step") + + @step.setter + @beartype + def step(self, value: Union[None, int, float]): + self._set_attr("step", value) + + # icon + @property + def icon(self): + return self._get_attr("icon") + + @icon.setter + def icon(self, value): + self._set_attr("icon", value) + + # focused + @property + def focused(self): + return self._get_attr("focused", data_type="bool", def_value=False) + + @focused.setter + @beartype + def focused(self, value: Optional[bool]): + self._set_attr("focused", value) + + # on_focus + @property + def on_focus(self): + return self._get_event_handler("focus") + + @on_focus.setter + def on_focus(self, handler): + self._add_event_handler("focus", handler) + + # on_blur + @property + def on_blur(self): + return self._get_event_handler("blur") + + @on_blur.setter + def on_blur(self, handler): + self._add_event_handler("blur", handler) diff --git a/sdk/python/flet/spinner.py b/sdk/python/flet/spinner.py new file mode 100644 index 0000000000..a637a9ba44 --- /dev/null +++ b/sdk/python/flet/spinner.py @@ -0,0 +1,73 @@ +from beartype import beartype + +from flet.control import Control + +try: + from typing import Literal +except: + from typing_extensions import Literal + + +Position = Literal[None, "left", "top", "right", "bottom"] + + +class Spinner(Control): + def __init__( + self, + label=None, + id=None, + ref=None, + label_position: Position = None, + size=None, + width=None, + height=None, + padding=None, + margin=None, + visible=None, + disabled=None, + ): + Control.__init__( + self, + id=id, + ref=ref, + width=width, + height=height, + padding=padding, + margin=margin, + visible=visible, + disabled=disabled, + ) + self.label = label + self.size = size + self.label_position = label_position + + def _get_control_name(self): + return "spinner" + + # label + @property + def label(self): + return self._get_attr("label") + + @label.setter + def label(self, value): + self._set_attr("label", value) + + # size + @property + def size(self): + return self._get_attr("size") + + @size.setter + def size(self, value): + self._set_attr("size", value) + + # label_position + @property + def label_position(self): + return self._get_attr("labelPosition") + + @label_position.setter + @beartype + def label_position(self, value: Position): + self._set_attr("labelPosition", value) diff --git a/sdk/python/flet/splitstack.py b/sdk/python/flet/splitstack.py new file mode 100644 index 0000000000..a660b4e90d --- /dev/null +++ b/sdk/python/flet/splitstack.py @@ -0,0 +1,127 @@ +from typing import Optional + +from beartype import beartype + +from flet.control import Control + +try: + from typing import Literal +except: + from typing_extensions import Literal + + +class SplitStack(Control): + def __init__( + self, + controls=None, + id=None, + ref=None, + horizontal=None, + gutter_size=None, + gutter_color=None, + gutter_hover_color=None, + gutter_drag_color=None, + on_resize=None, + width=None, + height=None, + visible=None, + disabled=None, + data=None, + ): + Control.__init__( + self, + id=id, + ref=ref, + width=width, + height=height, + visible=visible, + disabled=disabled, + data=data, + ) + + self.horizontal = horizontal + self.gutter_size = gutter_size + self.gutter_color = gutter_color + self.gutter_hover_color = gutter_hover_color + self.gutter_drag_color = gutter_drag_color + self.on_resize = on_resize + + self.__controls = [] + if controls != None: + for control in controls: + self.__controls.append(control) + + def _get_control_name(self): + return "splitstack" + + def clean(self): + Control.clean(self) + self.__controls.clear() + + # controls + @property + def controls(self): + return self.__controls + + @controls.setter + def controls(self, value): + self.__controls = value + + # horizontal + @property + def horizontal(self): + return self._get_attr("horizontal", data_type="bool", def_value=False) + + @horizontal.setter + @beartype + def horizontal(self, value: Optional[bool]): + self._set_attr("horizontal", value) + + # gutter_size + @property + def gutter_size(self): + return self._get_attr("guttersize") + + @gutter_size.setter + @beartype + def gutter_size(self, value: Optional[int]): + self._set_attr("guttersize", value) + + # gutter_color + @property + def gutter_color(self): + return self._get_attr("guttercolor") + + @gutter_color.setter + def gutter_color(self, value): + self._set_attr("guttercolor", value) + + # gutter_hover_color + @property + def gutter_hover_color(self): + return self._get_attr("gutterhovercolor") + + @gutter_hover_color.setter + def gutter_hover_color(self, value): + self._set_attr("gutterhovercolor", value) + + # gutter_drag_color + @property + def gutter_drag_color(self): + return self._get_attr("gutterdragcolor") + + @gutter_drag_color.setter + def gutter_drag_color(self, value): + self._set_attr("gutterdragcolor", value) + + def _get_children(self): + return self.__controls + + # on_resize + @property + def on_resize(self): + return self._get_event_handler("resize") + + @on_resize.setter + def on_resize(self, handler): + self._add_event_handler("resize", handler) diff --git a/sdk/python/flet/stack.py b/sdk/python/flet/stack.py new file mode 100644 index 0000000000..a49d8a8a31 --- /dev/null +++ b/sdk/python/flet/stack.py @@ -0,0 +1,304 @@ +from typing import Optional + +from beartype import beartype +from flet.control import BorderColor +from flet.control import BorderRadius +from flet.control import BorderStyle +from flet.control import BorderWidth +from flet.control import Control + +try: + from typing import Literal +except: + from typing_extensions import Literal + + +Align = Literal[ + None, + "start", + "end", + "center", + "space-between", + "space-around", + "space-evenly", + "baseline", + "stretch", +] + + +class Stack(Control): + def __init__( + self, + controls=None, + id=None, + ref=None, + horizontal=None, + vertical_fill=None, + horizontal_align: Align = None, + vertical_align: Align = None, + min_width=None, + max_width=None, + min_height=None, + max_height=None, + gap=None, + wrap=None, + bgcolor=None, + border_style: BorderStyle = None, + border_width: BorderWidth = None, + border_color: BorderColor = None, + border_radius: BorderRadius = None, + scroll_x=None, + scroll_y=None, + auto_scroll=None, + on_submit=None, + width=None, + height=None, + padding=None, + margin=None, + visible=None, + disabled=None, + data=None, + ): + Control.__init__( + self, + id=id, + ref=ref, + width=width, + height=height, + padding=padding, + margin=margin, + visible=visible, + disabled=disabled, + data=data, + ) + + self.horizontal = horizontal + self.vertical_fill = vertical_fill + self.horizontal_align = horizontal_align + self.vertical_align = vertical_align + self.min_width = min_width + self.max_width = max_width + self.min_height = min_height + self.max_height = max_height + self.gap = gap + self.wrap = wrap + self.bgcolor = bgcolor + self.border_style = border_style + self.border_width = border_width + self.border_color = border_color + self.border_radius = border_radius + self.scroll_x = scroll_x + self.scroll_y = scroll_y + self.auto_scroll = auto_scroll + self.on_submit = on_submit + + self.__controls = [] + if controls != None: + for control in controls: + self.__controls.append(control) + + def _get_control_name(self): + return "stack" + + def clean(self): + Control.clean(self) + self.__controls.clear() + + # controls + @property + def controls(self): + return self.__controls + + @controls.setter + def controls(self, value): + self.__controls = value + + # horizontal + @property + def horizontal(self): + return self._get_attr("horizontal", data_type="bool", def_value=False) + + @horizontal.setter + @beartype + def horizontal(self, value: Optional[bool]): + self._set_attr("horizontal", value) + + # vertical_fill + @property + def vertical_fill(self): + return self._get_attr("verticalFill", data_type="bool", def_value=False) + + @vertical_fill.setter + @beartype + def vertical_fill(self, value: Optional[bool]): + self._set_attr("verticalFill", value) + + # horizontal_align + @property + def horizontal_align(self): + return self._get_attr("horizontalAlign") + + @horizontal_align.setter + @beartype + def horizontal_align(self, value: Align): + self._set_attr("horizontalAlign", value) + + # vertical_align + @property + def vertical_align(self): + return self._get_attr("verticalAlign") + + @vertical_align.setter + @beartype + def vertical_align(self, value: Align): + self._set_attr("verticalAlign", value) + + # min_width + @property + def min_width(self): + return self._get_attr("minWidth") + + @min_width.setter + def min_width(self, value): + self._set_attr("minWidth", value) + + # max_width + @property + def max_width(self): + return self._get_attr("maxWidth") + + @max_width.setter + def max_width(self, value): + self._set_attr("maxWidth", value) + + # min_height + @property + def min_height(self): + return self._get_attr("minHeight") + + @min_height.setter + def min_height(self, value): + self._set_attr("minHeight", value) + + # max_height + @property + def max_height(self): + return self._get_attr("maxHeight") + + @max_height.setter + def max_height(self, value): + self._set_attr("maxHeight", value) + + # gap + @property + def gap(self): + return self._get_attr("gap") + + @gap.setter + def gap(self, value): + self._set_attr("gap", value) + + # wrap + @property + def wrap(self): + return self._get_attr("wrap", data_type="bool", def_value=False) + + @wrap.setter + @beartype + def wrap(self, value: Optional[bool]): + self._set_attr("wrap", value) + + # bgcolor + @property + def bgcolor(self): + return self._get_attr("bgcolor") + + @bgcolor.setter + def bgcolor(self, value): + self._set_attr("bgcolor", value) + + # border_style + @property + def border_style(self): + return self._get_value_or_list_attr("borderStyle", " ") + + @border_style.setter + @beartype + def border_style(self, value: BorderStyle): + self._set_value_or_list_attr("borderStyle", value, " ") + + # border_width + @property + def border_width(self): + return self._get_value_or_list_attr("borderWidth", " ") + + @border_width.setter + @beartype + def border_width(self, value: BorderWidth): + self._set_value_or_list_attr("borderWidth", value, " ") + + # border_color + @property + def border_color(self): + return self._get_value_or_list_attr("borderColor", " ") + + @border_color.setter + @beartype + def border_color(self, value: BorderColor): + self._set_value_or_list_attr("borderColor", value, " ") + + # border_radius + @property + def border_radius(self): + return self._get_value_or_list_attr("borderRadius", " ") + + @border_radius.setter + @beartype + def border_radius(self, value: BorderRadius): + self._set_value_or_list_attr("borderRadius", value, " ") + + # scroll_x + @property + def scroll_x(self): + return self._get_attr("scrollx", data_type="bool", def_value=False) + + @scroll_x.setter + @beartype + def scroll_x(self, value: Optional[bool]): + self._set_attr("scrollx", value) + + # scroll_y + @property + def scroll_y(self): + return self._get_attr("scrolly", data_type="bool", def_value=False) + + @scroll_y.setter + @beartype + def scroll_y(self, value: Optional[bool]): + self._set_attr("scrolly", value) + + # auto_scroll + @property + def auto_scroll(self): + return self._get_attr("autoscroll", data_type="bool", def_value=False) + + @auto_scroll.setter + @beartype + def auto_scroll(self, value: Optional[bool]): + self._set_attr("autoscroll", value) + + # on_submit + @property + def on_submit(self): + return self._get_event_handler("submit") + + @on_submit.setter + def on_submit(self, handler): + self._add_event_handler("submit", handler) + if handler != None: + self._set_attr("onsubmit", True) + else: + self._set_attr("onsubmit", None) + + def _get_children(self): + return self.__controls diff --git a/sdk/python/flet/tabs.py b/sdk/python/flet/tabs.py new file mode 100644 index 0000000000..3252938d68 --- /dev/null +++ b/sdk/python/flet/tabs.py @@ -0,0 +1,166 @@ +from typing import Optional + +from beartype import beartype + +from flet.control import Control + + +class Tabs(Control): + def __init__( + self, + tabs=None, + id=None, + ref=None, + value=None, + solid=None, + on_change=None, + width=None, + height=None, + padding=None, + margin=None, + visible=None, + disabled=None, + ): + + Control.__init__( + self, + id=id, + ref=ref, + width=width, + height=height, + padding=padding, + margin=margin, + visible=visible, + disabled=disabled, + ) + + self.solid = solid + self.on_change = on_change + self.__tabs = [] + self.tabs = tabs + if value: + self.value = value + + def _get_control_name(self): + return "tabs" + + def clean(self): + Control.clean(self) + self.__tabs.clear() + self.value = None + + # tabs + @property + def tabs(self): + return self.__tabs + + @tabs.setter + def tabs(self, value): + value = value or [] + self.__tabs = value + self.value = value and (value[0].key or value[0].text) or "" + + # on_change + @property + def on_change(self): + return self._get_event_handler("change") + + @on_change.setter + def on_change(self, handler): + self._add_event_handler("change", handler) + + # value + @property + def value(self): + return self._get_attr("value") + + @value.setter + @beartype + def value(self, value: str): + if not value: + assert ( + not self.tabs + ), "Setting an empty value is only allowed if you have no tabs" + else: + assert any( + value in keys for keys in [(tab.key, tab.text) for tab in self.tabs] + ), f"'{value}' is not a key for any tab" + self._set_attr("value", value or "") + + # solid + @property + def solid(self): + return self._get_attr("solid", data_type="bool", def_value=False) + + @solid.setter + @beartype + def solid(self, value: Optional[bool]): + self._set_attr("solid", value) + + def _get_children(self): + return self.__tabs + + +class Tab(Control): + def __init__( + self, text, controls=None, id=None, ref=None, key=None, icon=None, count=None + ): + Control.__init__(self, id=id, ref=ref) + assert key or text, "key or text must be specified" + self.key = key + self.text = text + self.icon = icon + self.count = count + self.__controls = [] + self.controls = controls + + def _get_control_name(self): + return "tab" + + # key + @property + def key(self): + return self._get_attr("key") + + @key.setter + def key(self, value): + self._set_attr("key", value) + + # text + @property + def text(self): + return self._get_attr("text") + + @text.setter + def text(self, value): + self._set_attr("text", value) + + # icon + @property + def icon(self): + return self._get_attr("icon") + + @icon.setter + def icon(self, value): + self._set_attr("icon", value) + + # controls + @property + def controls(self): + return self.__controls + + @controls.setter + def controls(self, value): + self.__controls = value or [] + + # count + @property + def count(self): + return self._get_attr("count") + + @count.setter + def count(self, value): + self._set_attr("count", value) + + def _get_children(self): + return self.__controls diff --git a/sdk/python/flet/text.py b/sdk/python/flet/text.py new file mode 100644 index 0000000000..02a57828f6 --- /dev/null +++ b/sdk/python/flet/text.py @@ -0,0 +1,237 @@ +from typing import Optional + +from beartype import beartype +from flet.control import BorderColor +from flet.control import BorderRadius +from flet.control import BorderStyle +from flet.control import BorderWidth +from flet.control import Control +from flet.control import TextAlign +from flet.control import TextSize + +try: + from typing import Literal +except: + from typing_extensions import Literal + + +VerticalAlign = Literal[None, "top", "center", "bottom"] + + +class Text(Control): + def __init__( + self, + value=None, + id=None, + ref=None, + markdown=None, + align: TextAlign = None, + vertical_align: VerticalAlign = None, + size: TextSize = None, + bold=None, + italic=None, + pre=None, + nowrap=None, + block=None, + color=None, + bgcolor=None, + border_style: BorderStyle = None, + border_width: BorderWidth = None, + border_color: BorderColor = None, + border_radius: BorderRadius = None, + width=None, + height=None, + padding=None, + margin=None, + visible=None, + disabled=None, + ): + + Control.__init__( + self, + id=id, + ref=ref, + width=width, + height=height, + padding=padding, + margin=margin, + visible=visible, + disabled=disabled, + ) + + self.value = value + self.markdown = markdown + self.align = align + self.vertical_align = vertical_align + self.size = size + self.bold = bold + self.italic = italic + self.pre = pre + self.nowrap = nowrap + self.block = block + self.color = color + self.bgcolor = bgcolor + self.border_style = border_style + self.border_width = border_width + self.border_color = border_color + self.border_radius = border_radius + + def _get_control_name(self): + return "text" + + # value + @property + def value(self): + return self._get_attr("value") + + @value.setter + def value(self, value): + self._set_attr("value", value) + + # markdown + @property + def markdown(self): + return self._get_attr("markdown", data_type="bool", def_value=False) + + @markdown.setter + @beartype + def markdown(self, value: Optional[bool]): + self._set_attr("markdown", value) + + # align + @property + def align(self): + return self._get_attr("align") + + @align.setter + @beartype + def align(self, value: TextAlign): + self._set_attr("align", value) + + # vertical_align + @property + def vertical_align(self): + return self._get_attr("verticalAlign") + + @vertical_align.setter + @beartype + def vertical_align(self, value: VerticalAlign): + self._set_attr("verticalAlign", value) + + # size + @property + def size(self): + return self._get_attr("size") + + @size.setter + @beartype + def size(self, value: TextSize): + self._set_attr("size", value) + + # bold + @property + def bold(self): + return self._get_attr("bold", data_type="bool", def_value=False) + + @bold.setter + @beartype + def bold(self, value: Optional[bool]): + self._set_attr("bold", value) + + # italic + @property + def italic(self): + return self._get_attr("italic", data_type="bool", def_value=False) + + @italic.setter + @beartype + def italic(self, value: Optional[bool]): + self._set_attr("italic", value) + + # pre + @property + def pre(self): + return self._get_attr("pre", data_type="bool", def_value=False) + + @pre.setter + @beartype + def pre(self, value: Optional[bool]): + self._set_attr("pre", value) + + # nowrap + @property + def nowrap(self): + return self._get_attr("nowrap", data_type="bool", def_value=False) + + @nowrap.setter + @beartype + def nowrap(self, value: Optional[bool]): + self._set_attr("nowrap", value) + + # block + @property + def block(self): + return self._get_attr("block", data_type="bool", def_value=False) + + @block.setter + @beartype + def block(self, value: Optional[bool]): + self._set_attr("block", value) + + # color + @property + def color(self): + return self._get_attr("color") + + @color.setter + def color(self, value): + self._set_attr("color", value) + + # bgcolor + @property + def bgcolor(self): + return self._get_attr("bgcolor") + + @bgcolor.setter + def bgcolor(self, value): + self._set_attr("bgcolor", value) + + # border_style + @property + def border_style(self): + return self._get_value_or_list_attr("borderStyle", " ") + + @border_style.setter + @beartype + def border_style(self, value: BorderStyle): + self._set_value_or_list_attr("borderStyle", value, " ") + + # border_width + @property + def border_width(self): + return self._get_value_or_list_attr("borderWidth", " ") + + @border_width.setter + @beartype + def border_width(self, value: BorderWidth): + self._set_value_or_list_attr("borderWidth", value, " ") + + # border_color + @property + def border_color(self): + return self._get_value_or_list_attr("borderColor", " ") + + @border_color.setter + @beartype + def border_color(self, value: BorderColor): + self._set_value_or_list_attr("borderColor", value, " ") + + # border_radius + @property + def border_radius(self): + return self._get_value_or_list_attr("borderRadius", " ") + + @border_radius.setter + @beartype + def border_radius(self, value: BorderRadius): + self._set_value_or_list_attr("borderRadius", value, " ") diff --git a/sdk/python/flet/textbox.py b/sdk/python/flet/textbox.py new file mode 100644 index 0000000000..9a8a9ae319 --- /dev/null +++ b/sdk/python/flet/textbox.py @@ -0,0 +1,313 @@ +from typing import Optional + +from beartype import beartype + +from flet.control import Control, TextAlign + + +class Textbox(Control): + def __init__( + self, + label=None, + id=None, + ref=None, + value=None, + placeholder=None, + error_message=None, + description=None, + icon=None, + icon_color=None, + prefix=None, + suffix=None, + multiline=None, + rows=None, + shift_enter=None, + password=None, + required=None, + read_only=None, + auto_adjust_height=None, + resizable=None, + underlined=None, + borderless=None, + focused=None, + on_change=None, + on_focus=None, + on_blur=None, + width=None, + height=None, + padding=None, + margin=None, + align: TextAlign = None, + visible=None, + disabled=None, + ): + Control.__init__( + self, + id=id, + ref=ref, + width=width, + height=height, + padding=padding, + margin=margin, + visible=visible, + disabled=disabled, + ) + self.label = label + self.value = value + self.placeholder = placeholder + self.error_message = error_message + self.description = description + self.icon = icon + self.icon_color = icon_color + self.suffix = suffix + self.prefix = prefix + self.align = align + self.multiline = multiline + self.rows = rows + self.shift_enter = shift_enter + self.read_only = read_only + self.auto_adjust_height = auto_adjust_height + self.resizable = resizable + self.underlined = underlined + self.borderless = borderless + self.password = password + self.required = required + self.focused = focused + self.on_change = on_change + self.on_focus = on_focus + self.on_blur = on_blur + + def _get_control_name(self): + return "textbox" + + # label + @property + def label(self): + return self._get_attr("label") + + @label.setter + def label(self, value): + self._set_attr("label", value) + + # value + @property + def value(self): + return self._get_attr("value", def_value="") + + @value.setter + def value(self, value): + self._set_attr("value", value) + + # placeholder + @property + def placeholder(self): + return self._get_attr("placeholder") + + @placeholder.setter + def placeholder(self, value): + self._set_attr("placeholder", value) + + # error_message + @property + def error_message(self): + return self._get_attr("errorMessage") + + @error_message.setter + def error_message(self, value): + self._set_attr("errorMessage", value) + + # description + @property + def description(self): + return self._get_attr("description") + + @description.setter + def description(self, value): + self._set_attr("description", value) + + # icon + @property + def icon(self): + return self._get_attr("icon") + + @icon.setter + def icon(self, value): + self._set_attr("icon", value) + + # icon_color + @property + def icon_color(self): + return self._get_attr("iconColor") + + @icon_color.setter + def icon_color(self, value): + self._set_attr("iconColor", value) + + # prefix + @property + def prefix(self): + return self._get_attr("prefix") + + @prefix.setter + def prefix(self, value): + self._set_attr("prefix", value) + + # suffix + @property + def suffix(self): + return self._get_attr("suffix") + + @suffix.setter + def suffix(self, value): + self._set_attr("suffix", value) + + # align + @property + def align(self): + return self._get_attr("align") + + @align.setter + @beartype + def align(self, value: TextAlign): + self._set_attr("align", value) + + # multiline + @property + def multiline(self): + return self._get_attr("multiline", data_type="bool", def_value=False) + + @multiline.setter + @beartype + def multiline(self, value: Optional[bool]): + self._set_attr("multiline", value) + + # rows + @property + def rows(self): + return self._get_attr("rows") + + @rows.setter + @beartype + def rows(self, value: Optional[int]): + self._set_attr("rows", value) + + # shift_enter + @property + def shift_enter(self): + return self._get_attr("shiftenter", data_type="bool", def_value=False) + + @shift_enter.setter + @beartype + def shift_enter(self, value: Optional[bool]): + self._set_attr("shiftenter", value) + + # read_only + @property + def read_only(self): + return self._get_attr("readOnly", data_type="bool", def_value=False) + + @read_only.setter + @beartype + def read_only(self, value: Optional[bool]): + self._set_attr("readOnly", value) + + # auto_adjust_height + @property + def auto_adjust_height(self): + return self._get_attr("autoadjustheight", data_type="bool", def_value=False) + + @auto_adjust_height.setter + @beartype + def auto_adjust_height(self, value: Optional[bool]): + self._set_attr("autoadjustheight", value) + + # resizable + @property + def resizable(self): + return self._get_attr("resizable", data_type="bool", def_value=True) + + @resizable.setter + @beartype + def resizable(self, value: Optional[bool]): + self._set_attr("resizable", value) + + # underlined + @property + def underlined(self): + return self._get_attr("underlined", data_type="bool", def_value=False) + + @underlined.setter + @beartype + def underlined(self, value: Optional[bool]): + self._set_attr("underlined", value) + + # borderless + @property + def borderless(self): + return self._get_attr("borderless", data_type="bool", def_value=False) + + @borderless.setter + @beartype + def borderless(self, value: Optional[bool]): + self._set_attr("borderless", value) + + # password + @property + def password(self): + return self._get_attr("password", data_type="bool", def_value=False) + + @password.setter + @beartype + def password(self, value: Optional[bool]): + self._set_attr("password", value) + + # required + @property + def required(self): + return self._get_attr("required", data_type="bool", def_value=False) + + @required.setter + @beartype + def required(self, value: Optional[bool]): + self._set_attr("required", value) + + # focused + @property + def focused(self): + return self._get_attr("focused", data_type="bool", def_value=False) + + @focused.setter + @beartype + def focused(self, value: Optional[bool]): + self._set_attr("focused", value) + + # on_change + @property + def on_change(self): + return self._get_event_handler("change") + + @on_change.setter + def on_change(self, handler): + self._add_event_handler("change", handler) + if handler != None: + self._set_attr("onchange", True) + else: + self._set_attr("onchange", None) + + # on_focus + @property + def on_focus(self): + return self._get_event_handler("focus") + + @on_focus.setter + def on_focus(self, handler): + self._add_event_handler("focus", handler) + + # on_blur + @property + def on_blur(self): + return self._get_event_handler("blur") + + @on_blur.setter + def on_blur(self, handler): + self._add_event_handler("blur", handler) diff --git a/sdk/python/flet/toggle.py b/sdk/python/flet/toggle.py new file mode 100644 index 0000000000..8731e80a3f --- /dev/null +++ b/sdk/python/flet/toggle.py @@ -0,0 +1,151 @@ +from typing import Optional + +from beartype import beartype + +from flet.control import Control + + +class Toggle(Control): + def __init__( + self, + label=None, + id=None, + ref=None, + value=None, + value_field=None, + inline=None, + on_text=None, + off_text=None, + focused=None, + data=None, + on_change=None, + on_focus=None, + on_blur=None, + width=None, + height=None, + padding=None, + margin=None, + visible=None, + disabled=None, + ): + + Control.__init__( + self, + id=id, + ref=ref, + width=width, + height=height, + padding=padding, + margin=margin, + visible=visible, + disabled=disabled, + data=data, + ) + + self.value = value + self.value_field = value_field + self.label = label + self.inline = inline + self.on_text = on_text + self.off_text = off_text + self.focused = focused + self.on_change = on_change + self.on_focus = on_focus + self.on_blur = on_blur + + def _get_control_name(self): + return "toggle" + + # on_change + @property + def on_change(self): + return self._get_event_handler("change") + + @on_change.setter + def on_change(self, handler): + self._add_event_handler("change", handler) + + # value + @property + def value(self): + return self._get_attr("value", data_type="bool", def_value=False) + + @value.setter + @beartype + def value(self, value: Optional[bool]): + self._set_attr("value", value) + + # value_field + @property + def value_field(self): + return self._get_attr("value") + + @value_field.setter + def value_field(self, value): + if value != None: + self._set_attr("value", f"{{{value}}}") + + # label + @property + def label(self): + return self._get_attr("label") + + @label.setter + def label(self, value): + self._set_attr("label", value) + + # inline + @property + def inline(self): + return self._get_attr("inline", data_type="bool", def_value=False) + + @inline.setter + @beartype + def inline(self, value: Optional[bool]): + self._set_attr("inline", value) + + # on_text + @property + def on_text(self): + return self._get_attr("onText") + + @on_text.setter + def on_text(self, value): + self._set_attr("onText", value) + + # off_text + @property + def off_text(self): + return self._get_attr("offText") + + @off_text.setter + def off_text(self, value): + self._set_attr("offText", value) + + # focused + @property + def focused(self): + return self._get_attr("focused", data_type="bool", def_value=False) + + @focused.setter + @beartype + def focused(self, value: Optional[bool]): + self._set_attr("focused", value) + + # on_focus + @property + def on_focus(self): + return self._get_event_handler("focus") + + @on_focus.setter + def on_focus(self, handler): + self._add_event_handler("focus", handler) + + # on_blur + @property + def on_blur(self): + return self._get_event_handler("blur") + + @on_blur.setter + def on_blur(self, handler): + self._add_event_handler("blur", handler) diff --git a/sdk/python/flet/toolbar.py b/sdk/python/flet/toolbar.py new file mode 100644 index 0000000000..08bd8d23ad --- /dev/null +++ b/sdk/python/flet/toolbar.py @@ -0,0 +1,291 @@ +from typing import Optional + +from beartype import beartype + +from flet.control import Control + + +class Toolbar(Control): + def __init__( + self, + id=None, + ref=None, + inverted=None, + items=None, + overflow=None, + far=None, + width=None, + height=None, + padding=None, + margin=None, + visible=None, + disabled=None, + ): + + Control.__init__( + self, + id=id, + ref=ref, + width=width, + height=height, + padding=padding, + margin=margin, + visible=visible, + disabled=disabled, + ) + + self.__items = [] + if items != None: + for item in items: + self.__items.append(item) + self.__overflow = Overflow(items=overflow) + self.__far = Far(items=far) + self.inverted = inverted + + def _get_control_name(self): + return "toolbar" + + # inverted + @property + def inverted(self): + return self._get_attr("inverted", data_type="bool", def_value=False) + + @inverted.setter + @beartype + def inverted(self, value: Optional[bool]): + self._set_attr("inverted", value) + + # items + @property + def items(self): + return self.__items + + @items.setter + def items(self, value): + self.__items = value + + # far + @property + def far_items(self): + return self.__far.items + + @far_items.setter + def far_items(self, value): + self.__far.items = value + + # overflow + @property + def overflow_items(self): + return self.__overflow.items + + @overflow_items.setter + def overflow_items(self, value): + self.__overflow.items = value + + def _get_children(self): + result = [] + if self.__items and len(self.__items) > 0: + for item in self.__items: + result.append(item) + result.append(self.__overflow) + result.append(self.__far) + return result + + +class Overflow(Control): + def __init__(self, id=None, ref=None, items=None): + Control.__init__(self, id=id, ref=ref) + + self.__items = [] + if items != None: + for item in items: + self.__items.append(item) + + # items + @property + def items(self): + return self.__items + + @items.setter + def items(self, value): + self.__items = value + + def _get_control_name(self): + return "overflow" + + def _get_children(self): + return self.__items + + +class Far(Control): + def __init__(self, id=None, ref=None, items=None): + Control.__init__(self, id=id, ref=ref) + + self.__items = [] + if items != None: + for item in items: + self.__items.append(item) + + # items + @property + def items(self): + return self.__items + + @items.setter + def items(self, value): + self.__items = value + + def _get_control_name(self): + return "far" + + def _get_children(self): + return self.__items + + +class Item(Control): + def __init__( + self, + id=None, + ref=None, + text=None, + secondary_text=None, + url=None, + new_window=None, + icon=None, + icon_color=None, + icon_only=None, + split=None, + divider=None, + on_click=None, + items=None, + visible=None, + disabled=None, + data=None, + ): + Control.__init__( + self, id=id, ref=ref, visible=visible, disabled=disabled, data=data + ) + + self.text = text + self.secondary_text = secondary_text + self.url = url + self.new_window = new_window + self.icon = icon + self.icon_color = icon_color + self.icon_only = icon_only + self.split = split + self.divider = divider + self.on_click = on_click + self.__items = [] + if items != None: + for item in items: + self.__items.append(item) + + def _get_control_name(self): + return "item" + + # on_click + @property + def on_click(self): + return self._get_event_handler("click") + + @on_click.setter + def on_click(self, handler): + self._add_event_handler("click", handler) + + # items + @property + def items(self): + return self.__items + + @items.setter + def items(self, value): + self.__items = value + + # text + @property + def text(self): + return self._get_attr("text") + + @text.setter + def text(self, value): + self._set_attr("text", value) + + # secondary_text + @property + def secondary_text(self): + return self._get_attr("secondaryText") + + @secondary_text.setter + def secondary_text(self, value): + self._set_attr("secondaryText", value) + + # url + @property + def url(self): + return self._get_attr("url") + + @url.setter + def url(self, value): + self._set_attr("url", value) + + # new_window + @property + def new_window(self): + return self._get_attr("newWindow", data_type="bool", def_value=False) + + @new_window.setter + @beartype + def new_window(self, value: Optional[bool]): + self._set_attr("newWindow", value) + + # icon + @property + def icon(self): + return self._get_attr("icon") + + @icon.setter + def icon(self, value): + self._set_attr("icon", value) + + # icon_color + @property + def icon_color(self): + return self._get_attr("iconColor") + + @icon_color.setter + def icon_color(self, value): + self._set_attr("iconColor", value) + + # icon_only + @property + def icon_only(self): + return self._get_attr("iconOnly", data_type="bool", def_value=False) + + @icon_only.setter + @beartype + def icon_only(self, value: Optional[bool]): + self._set_attr("iconOnly", value) + + # split + @property + def split(self): + return self._get_attr("split", data_type="bool", def_value=False) + + @split.setter + @beartype + def split(self, value: Optional[bool]): + self._set_attr("split", value) + + # divider + @property + def divider(self): + return self._get_attr("divider", data_type="bool", def_value=False) + + @divider.setter + @beartype + def divider(self, value: Optional[bool]): + self._set_attr("divider", value) + + def _get_children(self): + return self.__items diff --git a/sdk/python/flet/utils.py b/sdk/python/flet/utils.py new file mode 100644 index 0000000000..cc7203cac6 --- /dev/null +++ b/sdk/python/flet/utils.py @@ -0,0 +1,65 @@ +import platform +import subprocess + + +def is_windows(): + return platform.system() == "Windows" + + +def is_macos(): + return platform.system() == "Darwin" + + +def get_platform(): + p = platform.system() + if is_windows(): + return "windows" + elif p == "Linux": + return "linux" + elif p == "Darwin": + return "darwin" + else: + raise Exception(f"Unsupported platform: {p}") + + +def get_arch(): + a = platform.machine().lower() + if a == "x86_64" or a == "amd64": + return "amd64" + elif a == "arm64" or a == "aarch64": + return "arm64" + elif a.startswith("arm"): + return "arm" + else: + raise Exception(f"Unsupported architecture: {a}") + + +def open_in_browser(url): + if is_windows(): + subprocess.run(["explorer.exe", url]) + elif is_macos(): + subprocess.run(["open", url]) + + +# https://stackoverflow.com/questions/377017/test-if-executable-exists-in-python +def which(program): + import os + + def is_exe(fpath): + return os.path.isfile(fpath) and os.access(fpath, os.X_OK) + + fpath, _ = os.path.split(program) + if fpath: + if is_exe(program): + return program + else: + for path in os.environ["PATH"].split(os.pathsep): + exe_file = os.path.join(path, program) + if is_exe(exe_file): + return exe_file + + return None + + +def is_localhost_url(url): + return "://localhost/" in url or "://localhost:" in url diff --git a/sdk/python/flet/verticalbarchart.py b/sdk/python/flet/verticalbarchart.py new file mode 100644 index 0000000000..aa5a20d047 --- /dev/null +++ b/sdk/python/flet/verticalbarchart.py @@ -0,0 +1,269 @@ +from typing import Optional, Union + +from beartype import beartype + +from flet.control import Control + +try: + from typing import Literal +except: + from typing_extensions import Literal + + +XType = Literal[None, "string", "number"] + + +class VerticalBarChart(Control): + def __init__( + self, + id=None, + ref=None, + legend=None, + tooltips=None, + bar_width=None, + colors=None, + y_min=None, + y_max=None, + y_ticks=None, + y_format=None, + x_type: XType = None, + points=None, + width=None, + height=None, + padding=None, + margin=None, + visible=None, + disabled=None, + ): + + Control.__init__( + self, + id=id, + ref=ref, + width=width, + height=height, + padding=padding, + margin=margin, + visible=visible, + disabled=disabled, + ) + + self.__data = Data(points=points) + self.legend = legend + self.tooltips = tooltips + self.bar_width = bar_width + self.colors = colors + self.y_min = y_min + self.y_max = y_max + self.y_ticks = y_ticks + self.y_format = y_format + self.x_type = x_type + + def _get_control_name(self): + return "verticalbarchart" + + # points + @property + def points(self): + return self.__data.points + + @points.setter + def points(self, value): + self.__data.points = value + + # legend + @property + def legend(self): + return self._get_attr("legend", data_type="bool", def_value=False) + + @legend.setter + @beartype + def legend(self, value: Optional[bool]): + self._set_attr("legend", value) + + # tooltips + @property + def tooltips(self): + return self._get_attr("tooltips", data_type="bool", def_value=False) + + @tooltips.setter + @beartype + def tooltips(self, value: Optional[bool]): + self._set_attr("tooltips", value) + + # colors + @property + def colors(self): + return self._get_attr("colors") + + @colors.setter + def colors(self, value): + self._set_attr("colors", value) + + # yMin + @property + def y_min(self): + return self._get_attr("yMin") + + @y_min.setter + @beartype + def y_min(self, value: Union[None, int, float]): + self._set_attr("yMin", value) + + # yMax + @property + def y_max(self): + return self._get_attr("yMax") + + @y_max.setter + @beartype + def y_max(self, value: Union[None, int, float]): + self._set_attr("yMax", value) + + # yTicks + @property + def y_ticks(self): + return self._get_attr("yTicks") + + @y_ticks.setter + @beartype + def y_ticks(self, value: Optional[int]): + self._set_attr("yTicks", value) + + # yFormat + @property + def y_format(self): + return self._get_attr("yFormat") + + @y_format.setter + def y_format(self, value): + self._set_attr("yFormat", value) + + # xType + @property + def x_type(self): + return self._get_attr("xType") + + @x_type.setter + @beartype + def x_type(self, value: XType): + self._set_attr("xType", value) + + # bar_width + @property + def bar_width(self): + return self._get_attr("barWidth") + + @bar_width.setter + @beartype + def bar_width(self, value: Optional[int]): + self._set_attr("barWidth", value) + + def _get_children(self): + return [self.__data] + + +class Data(Control): + def __init__(self, id=None, ref=None, points=None): + Control.__init__(self, id=id, ref=ref) + + self.__points = [] + if points != None: + for point in points: + self.__points.append(point) + + # points + @property + def points(self): + return self.__points + + @points.setter + def points(self, value): + self.__points = value + + def _get_control_name(self): + return "data" + + def _get_children(self): + return self.__points + + +class Point(Control): + def __init__( + self, + id=None, + ref=None, + x=None, + y=None, + legend=None, + color=None, + x_tooltip=None, + y_tooltip=None, + ): + Control.__init__(self, id=id, ref=ref) + + self.x = x + self.y = y + self.legend = legend + self.color = color + self.x_tooltip = x_tooltip + self.y_tooltip = y_tooltip + + def _get_control_name(self): + return "p" + + # x + @property + def x(self): + return self._get_attr("x") + + @x.setter + @beartype + def x(self, value: Union[None, int, float, str]): + self._set_attr("x", value) + + # y + @property + def y(self): + return self._get_attr("y") + + @y.setter + @beartype + def y(self, value: Union[None, int, float]): + self._set_attr("y", value) + + # legend + @property + def legend(self): + return self._get_attr("legend") + + @legend.setter + def legend(self, value): + self._set_attr("legend", value) + + # color + @property + def color(self): + return self._get_attr("color") + + @color.setter + def color(self, value): + self._set_attr("color", value) + + # x_tooltip + @property + def x_tooltip(self): + return self._get_attr("xTooltip") + + @x_tooltip.setter + def x_tooltip(self, value): + self._set_attr("xTooltip", value) + + # y_tooltip + @property + def y_tooltip(self): + return self._get_attr("yTooltip") + + @y_tooltip.setter + def y_tooltip(self, value): + self._set_attr("yTooltip", value) diff --git a/sdk/python/pdm.lock b/sdk/python/pdm.lock new file mode 100644 index 0000000000..f2d0b5c060 --- /dev/null +++ b/sdk/python/pdm.lock @@ -0,0 +1,330 @@ +[[package]] +name = "atomicwrites" +version = "1.4.0" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +summary = "Atomic file writes." + +[[package]] +name = "attrs" +version = "21.4.0" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +summary = "Classes Without Boilerplate" + +[[package]] +name = "beartype" +version = "0.10.4" +requires_python = ">=3.6.0" +summary = "Unbearably fast runtime type checking in pure Python." + +[[package]] +name = "cfgv" +version = "3.3.1" +requires_python = ">=3.6.1" +summary = "Validate configuration and produce human readable error messages." + +[[package]] +name = "colorama" +version = "0.4.4" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +summary = "Cross-platform colored terminal text." + +[[package]] +name = "distlib" +version = "0.3.4" +summary = "Distribution utilities" + +[[package]] +name = "filelock" +version = "3.6.0" +requires_python = ">=3.7" +summary = "A platform independent file lock." + +[[package]] +name = "identify" +version = "2.4.12" +requires_python = ">=3.7" +summary = "File identification library for Python" + +[[package]] +name = "importlib-metadata" +version = "4.11.3" +requires_python = ">=3.7" +summary = "Read metadata from Python packages" +dependencies = [ + "typing-extensions>=3.6.4; python_version < \"3.8\"", + "zipp>=0.5", +] + +[[package]] +name = "iniconfig" +version = "1.1.1" +summary = "iniconfig: brain-dead simple config-ini parsing" + +[[package]] +name = "nodeenv" +version = "1.6.0" +summary = "Node.js virtual environment builder" + +[[package]] +name = "packaging" +version = "21.3" +requires_python = ">=3.6" +summary = "Core utilities for Python packages" +dependencies = [ + "pyparsing!=3.0.5,>=2.0.2", +] + +[[package]] +name = "platformdirs" +version = "2.5.1" +requires_python = ">=3.7" +summary = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." + +[[package]] +name = "pluggy" +version = "1.0.0" +requires_python = ">=3.6" +summary = "plugin and hook calling mechanisms for python" +dependencies = [ + "importlib-metadata>=0.12; python_version < \"3.8\"", +] + +[[package]] +name = "pre-commit" +version = "2.17.0" +requires_python = ">=3.6.1" +summary = "A framework for managing and maintaining multi-language pre-commit hooks." +dependencies = [ + "cfgv>=2.0.0", + "identify>=1.0.0", + "importlib-metadata; python_version < \"3.8\"", + "nodeenv>=0.11.1", + "pyyaml>=5.1", + "toml", + "virtualenv>=20.0.8", +] + +[[package]] +name = "py" +version = "1.11.0" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +summary = "library with cross-python path, ini-parsing, io, code, log facilities" + +[[package]] +name = "pyparsing" +version = "3.0.7" +requires_python = ">=3.6" +summary = "Python parsing module" + +[[package]] +name = "pytest" +version = "7.1.1" +requires_python = ">=3.7" +summary = "pytest: simple powerful testing with Python" +dependencies = [ + "atomicwrites>=1.0; sys_platform == \"win32\"", + "attrs>=19.2.0", + "colorama; sys_platform == \"win32\"", + "importlib-metadata>=0.12; python_version < \"3.8\"", + "iniconfig", + "packaging", + "pluggy<2.0,>=0.12", + "py>=1.8.2", + "tomli>=1.0.0", +] + +[[package]] +name = "pyyaml" +version = "6.0" +requires_python = ">=3.6" +summary = "YAML parser and emitter for Python" + +[[package]] +name = "six" +version = "1.16.0" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +summary = "Python 2 and 3 compatibility utilities" + +[[package]] +name = "toml" +version = "0.10.2" +requires_python = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +summary = "Python Library for Tom's Obvious, Minimal Language" + +[[package]] +name = "tomli" +version = "2.0.1" +requires_python = ">=3.7" +summary = "A lil' TOML parser" + +[[package]] +name = "typing-extensions" +version = "4.1.1" +requires_python = ">=3.6" +summary = "Backported and Experimental Type Hints for Python 3.6+" + +[[package]] +name = "virtualenv" +version = "20.13.4" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +summary = "Virtual Python Environment builder" +dependencies = [ + "distlib<1,>=0.3.1", + "filelock<4,>=3.2", + "importlib-metadata>=0.12; python_version < \"3.8\"", + "platformdirs<3,>=2", + "six<2,>=1.9.0", +] + +[[package]] +name = "websocket-client" +version = "1.3.1" +requires_python = ">=3.6" +summary = "WebSocket client for Python with low level API options" + +[[package]] +name = "zipp" +version = "3.7.0" +requires_python = ">=3.7" +summary = "Backport of pathlib-compatible object wrapper for zip files" + +[metadata] +lock_version = "3.1" +content_hash = "sha256:be80cde9d007abeb2c0d352d9a69fa5aefc29a1345919aebfd5685df9e3b639f" + +[metadata.files] +"atomicwrites 1.4.0" = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +"attrs 21.4.0" = [ + {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, + {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, +] +"beartype 0.10.4" = [ + {file = "beartype-0.10.4-py3-none-any.whl", hash = "sha256:1a65453bc25b39979bf5ad65fe5e73350551282956456d828fb5783468649e3e"}, + {file = "beartype-0.10.4.tar.gz", hash = "sha256:24ec69f6a7f4e6e97af403d08de270def3248518060327095d23b1c4df64bf2a"}, +] +"cfgv 3.3.1" = [ + {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, + {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, +] +"colorama 0.4.4" = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +"distlib 0.3.4" = [ + {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"}, + {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"}, +] +"filelock 3.6.0" = [ + {file = "filelock-3.6.0-py3-none-any.whl", hash = "sha256:f8314284bfffbdcfa0ff3d7992b023d4c628ced6feb957351d4c48d059f56bc0"}, + {file = "filelock-3.6.0.tar.gz", hash = "sha256:9cd540a9352e432c7246a48fe4e8712b10acb1df2ad1f30e8c070b82ae1fed85"}, +] +"identify 2.4.12" = [ + {file = "identify-2.4.12-py2.py3-none-any.whl", hash = "sha256:5f06b14366bd1facb88b00540a1de05b69b310cbc2654db3c7e07fa3a4339323"}, + {file = "identify-2.4.12.tar.gz", hash = "sha256:3f3244a559290e7d3deb9e9adc7b33594c1bc85a9dd82e0f1be519bf12a1ec17"}, +] +"importlib-metadata 4.11.3" = [ + {file = "importlib_metadata-4.11.3-py3-none-any.whl", hash = "sha256:1208431ca90a8cca1a6b8af391bb53c1a2db74e5d1cef6ddced95d4b2062edc6"}, + {file = "importlib_metadata-4.11.3.tar.gz", hash = "sha256:ea4c597ebf37142f827b8f39299579e31685c31d3a438b59f469406afd0f2539"}, +] +"iniconfig 1.1.1" = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +"nodeenv 1.6.0" = [ + {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"}, + {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, +] +"packaging 21.3" = [ + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, +] +"platformdirs 2.5.1" = [ + {file = "platformdirs-2.5.1-py3-none-any.whl", hash = "sha256:bcae7cab893c2d310a711b70b24efb93334febe65f8de776ee320b517471e227"}, + {file = "platformdirs-2.5.1.tar.gz", hash = "sha256:7535e70dfa32e84d4b34996ea99c5e432fa29a708d0f4e394bbcb2a8faa4f16d"}, +] +"pluggy 1.0.0" = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] +"pre-commit 2.17.0" = [ + {file = "pre_commit-2.17.0-py2.py3-none-any.whl", hash = "sha256:725fa7459782d7bec5ead072810e47351de01709be838c2ce1726b9591dad616"}, + {file = "pre_commit-2.17.0.tar.gz", hash = "sha256:c1a8040ff15ad3d648c70cc3e55b93e4d2d5b687320955505587fd79bbaed06a"}, +] +"py 1.11.0" = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] +"pyparsing 3.0.7" = [ + {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, + {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, +] +"pytest 7.1.1" = [ + {file = "pytest-7.1.1-py3-none-any.whl", hash = "sha256:92f723789a8fdd7180b6b06483874feca4c48a5c76968e03bb3e7f806a1869ea"}, + {file = "pytest-7.1.1.tar.gz", hash = "sha256:841132caef6b1ad17a9afde46dc4f6cfa59a05f9555aae5151f73bdf2820ca63"}, +] +"pyyaml 6.0" = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] +"six 1.16.0" = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] +"toml 0.10.2" = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +"tomli 2.0.1" = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] +"typing-extensions 4.1.1" = [ + {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, + {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, +] +"virtualenv 20.13.4" = [ + {file = "virtualenv-20.13.4-py2.py3-none-any.whl", hash = "sha256:c3e01300fb8495bc00ed70741f5271fc95fed067eb7106297be73d30879af60c"}, + {file = "virtualenv-20.13.4.tar.gz", hash = "sha256:ce8901d3bbf3b90393498187f2d56797a8a452fb2d0d7efc6fd837554d6f679c"}, +] +"websocket-client 1.3.1" = [ + {file = "websocket_client-1.3.1-py3-none-any.whl", hash = "sha256:074e2ed575e7c822fc0940d31c3ac9bb2b1142c303eafcf3e304e6ce035522e8"}, + {file = "websocket-client-1.3.1.tar.gz", hash = "sha256:6278a75065395418283f887de7c3beafb3aa68dada5cacbe4b214e8d26da499b"}, +] +"zipp 3.7.0" = [ + {file = "zipp-3.7.0-py3-none-any.whl", hash = "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"}, + {file = "zipp-3.7.0.tar.gz", hash = "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d"}, +] diff --git a/sdk/python/pyproject.toml b/sdk/python/pyproject.toml new file mode 100644 index 0000000000..d6b1b4ecff --- /dev/null +++ b/sdk/python/pyproject.toml @@ -0,0 +1,44 @@ +[project] +name = "flet" +version = "0.1.0" +description = "Flet for Python - easily build interactive multi-platform apps in Python" +readme = "README.md" +authors = [ + { name = "Appveyor Systems Inc.", email = "hello@flet.dev" }, +] +dependencies = [ + "websocket-client>=1.2.1", + "beartype>=0.9.1", + 'typing_extensions; python_version < "3.8"' +] +requires-python = ">=3.7" +license = { text = "MIT" } +classifiers = [ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] + +[project.urls] +repository = "https://github.com/flet-dev/flet" +documentation = "https://flet.dev/docs/" + +[project.optional-dependencies] + +[tool.pdm.dev-dependencies] +tests = [ + "pytest>=6.1.2", +] +dev = [ + "pre-commit>=2.17.0", +] + +[build-system] +requires = ["pdm-pep517"] +build-backend = "pdm.pep517.api" + +[tool.isort] +profile = "black" +float_to_top = true diff --git a/sdk/python/tests/__init__.py b/sdk/python/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sdk/python/tests/conftest.py b/sdk/python/tests/conftest.py new file mode 100644 index 0000000000..d58e7c52d6 --- /dev/null +++ b/sdk/python/tests/conftest.py @@ -0,0 +1,22 @@ +import flet +import pytest +from flet import Control +from flet import Text + + +@pytest.fixture +def page(): + return flet.page("test_update", no_window=True) + + +@pytest.fixture +def control_type_tree(): + def func(control: Control): + if getattr(control, "controls", None): + return {type(control): [func(child) for child in control.controls]} + elif type(control) is Text: + return control.value + else: + return type(control) + + return func \ No newline at end of file diff --git a/sdk/python/tests/test_barchart.py b/sdk/python/tests/test_barchart.py new file mode 100644 index 0000000000..c3eb8f237a --- /dev/null +++ b/sdk/python/tests/test_barchart.py @@ -0,0 +1,53 @@ +import flet +from flet import BarChart +from flet.barchart import Point +from flet.protocol import Command + + +def test_barchart_add(): + bc = BarChart( + data_mode="default", + tooltips=False, + points=[ + Point( + x=1, + y=100, + legend="legend", + color="green", + x_tooltip="x tooltip", + y_tooltip="y tooltip", + ), + Point(x=80, y=200), + Point(x=100, y=300), + ], + ) + assert isinstance(bc, flet.Control) + assert isinstance(bc, flet.BarChart) + assert bc.get_cmd_str() == [ + Command( + indent=0, + name=None, + values=["barchart"], + attrs={"datamode": "default", "tooltips": "false"}, + lines=[], + commands=[], + ), + Command(indent=2, name=None, values=["data"], attrs={}, lines=[], commands=[]), + Command( + indent=4, + name=None, + values=["p"], + attrs={ + "color": "green", + "legend": "legend", + "x": "1", + "xtooltip": "x tooltip", + "y": "100", + "ytooltip": "y tooltip", + }, + lines=[], + commands=[], + ), + Command(indent=4, name=None, values=["p"], attrs={"x": "80", "y": "200"}, lines=[], commands=[]), + Command(indent=4, name=None, values=["p"], attrs={"x": "100", "y": "300"}, lines=[], commands=[]), + ], "Test failed" diff --git a/sdk/python/tests/test_button.py b/sdk/python/tests/test_button.py new file mode 100644 index 0000000000..40135a6ffb --- /dev/null +++ b/sdk/python/tests/test_button.py @@ -0,0 +1,109 @@ +import flet +import pytest +from flet import Button, button +from flet.protocol import Command + + +def test_button_primary_must_be_bool(): + with pytest.raises(Exception): + Button(id="button1", text="My button", primary="1") + + +def test_button_add(): + b = Button(id="button1", text="My button", primary=True, data="this is data") + assert isinstance(b, flet.Control) + assert isinstance(b, flet.Button) + assert b.get_cmd_str() == [ + Command( + indent=0, + name=None, + values=["button"], + attrs={"data": "this is data", "primary": "true", "text": "My button", "id": ("button1", True)}, + lines=[], + commands=[], + ) + ], "Test failed" + + +def test_button_with_all_properties(): + b = Button( + primary=False, + compound=False, + action=False, + toolbar=True, + split=False, + text="This is text", + secondary_text="This is secondary text", + url="https://google.com", + new_window=True, + title="This is title", + icon="Mail", + icon_color="red", + data="data", + menu_items=[ + button.MenuItem( + text="Item1 text", + secondary_text="Item1 secondary text", + url="https://google.com", + new_window=False, + icon="Mail", + icon_color="blue", + icon_only=True, + split=False, + divider=False, + sub_menu_items=[ + button.MenuItem("Item1Item1"), + button.MenuItem("Item1Item2"), + ], + ), + button.MenuItem(text="Item2 text"), + ], + ) + + assert isinstance(b, flet.Control) + assert isinstance(b, flet.Button) + assert b.get_cmd_str() == [ + Command( + indent=0, + name=None, + values=["button"], + attrs={ + "action": "false", + "compound": "false", + "data": "data", + "icon": "Mail", + "iconcolor": "red", + "newwindow": "true", + "primary": "false", + "secondarytext": "This is secondary text", + "split": "false", + "text": "This is text", + "title": "This is title", + "toolbar": "true", + "url": "https://google.com", + }, + lines=[], + commands=[], + ), + Command( + indent=2, + name=None, + values=["item"], + attrs={ + "divider": "false", + "icon": "Mail", + "iconcolor": "blue", + "icononly": "true", + "newwindow": "false", + "secondarytext": "Item1 secondary text", + "split": "false", + "text": "Item1 text", + "url": "https://google.com", + }, + lines=[], + commands=[], + ), + Command(indent=4, name=None, values=["item"], attrs={"text": "Item1Item1"}, lines=[], commands=[]), + Command(indent=4, name=None, values=["item"], attrs={"text": "Item1Item2"}, lines=[], commands=[]), + Command(indent=2, name=None, values=["item"], attrs={"text": "Item2 text"}, lines=[], commands=[]), + ], "Test failed" diff --git a/sdk/python/tests/test_callout.py b/sdk/python/tests/test_callout.py new file mode 100644 index 0000000000..177238102e --- /dev/null +++ b/sdk/python/tests/test_callout.py @@ -0,0 +1,42 @@ +import flet +from flet import Callout, Text +from flet.protocol import Command + + +def test_callout_add(): + c = Callout( + target="button1", + position="leftBottom", + gap=100, + beak=True, + beak_width=10, + page_padding=10, + focus=False, + cover=True, + visible=True, + controls=[Text(value="This is callout")], + ) + + assert isinstance(c, flet.Control) + assert isinstance(c, flet.Callout) + assert c.get_cmd_str() == [ + Command( + indent=0, + name=None, + values=["callout"], + attrs={ + "beak": "true", + "beakwidth": "10", + "cover": "true", + "focus": "false", + "gap": "100", + "pagepadding": "10", + "position": "leftBottom", + "target": "button1", + "visible": "true", + }, + lines=[], + commands=[], + ), + Command(indent=2, name=None, values=["text"], attrs={"value": "This is callout"}, lines=[], commands=[]), + ], "Test failed" diff --git a/sdk/python/tests/test_checkbox.py b/sdk/python/tests/test_checkbox.py new file mode 100644 index 0000000000..b9a65289b9 --- /dev/null +++ b/sdk/python/tests/test_checkbox.py @@ -0,0 +1,19 @@ +import flet +from flet import Checkbox +from flet.protocol import Command + + +def test_checkbox_add(): + c = Checkbox(label="Do you agree?", value=True, visible=True, box_side="start", data="data1") + assert isinstance(c, flet.Control) + assert isinstance(c, flet.Checkbox) + assert c.get_cmd_str() == [ + Command( + indent=0, + name=None, + values=["checkbox"], + attrs={"boxside": "start", "data": "data1", "label": "Do you agree?", "value": "true", "visible": "true"}, + lines=[], + commands=[], + ) + ], "Test failed" diff --git a/sdk/python/tests/test_choicegroup.py b/sdk/python/tests/test_choicegroup.py new file mode 100644 index 0000000000..95ef1ec53d --- /dev/null +++ b/sdk/python/tests/test_choicegroup.py @@ -0,0 +1,69 @@ +import flet +from flet.choicegroup import Option +from flet.protocol import Command + + +def test_option(): + opt = Option("key1") + assert isinstance(opt, flet.Control) + assert isinstance(opt, Option) + + +def test_choicegroup(): + cg = flet.ChoiceGroup( + id="list1", + value="list1", + label="Your favorite color:", + options=[ + Option(key="key1", text="value1", icon="Shop", icon_color="Green"), + Option(key="key2", text="value2"), + ], + ) + + assert isinstance(cg, flet.Control) + assert isinstance(cg, flet.ChoiceGroup) + assert cg.get_cmd_str() == [ + Command( + indent=0, + name=None, + values=["choicegroup"], + attrs={"label": "Your favorite color:", "value": "list1", "id": ("list1", True)}, + lines=[], + commands=[], + ), + Command( + indent=2, + name=None, + values=["option"], + attrs={"icon": "Shop", "iconcolor": "Green", "key": "key1", "text": "value1"}, + lines=[], + commands=[], + ), + Command(indent=2, name=None, values=["option"], attrs={"key": "key2", "text": "value2"}, lines=[], commands=[]), + ], "Test failed" + + cgo = Option("key1") + assert isinstance(cgo, Option) + + +def test_choicegroup_with_just_keys(): + cg = flet.ChoiceGroup( + id="list1", + label="Your favorite color:", + options=[Option(key="key1"), Option(key="key2")], + ) + assert cg.get_cmd_str() == [ + Command( + indent=0, + name=None, + values=["choicegroup"], + attrs={"label": "Your favorite color:", "id": ("list1", True)}, + lines=[], + commands=[], + ), + Command(indent=2, name=None, values=["option"], attrs={"key": "key1"}, lines=[], commands=[]), + Command(indent=2, name=None, values=["option"], attrs={"key": "key2"}, lines=[], commands=[]), + ], "Test failed" + + cgo = Option("key1") + assert isinstance(cgo, Option) diff --git a/sdk/python/tests/test_dialog.py b/sdk/python/tests/test_dialog.py new file mode 100644 index 0000000000..28f91a7e43 --- /dev/null +++ b/sdk/python/tests/test_dialog.py @@ -0,0 +1,50 @@ +import flet +from flet import Button, Dialog, Text +from flet.protocol import Command + + +def test_dialog_add(): + d = Dialog( + open=True, + title="Hello", + sub_text="sub_text1", + type="close", + auto_dismiss=True, + width=100, + max_width=200, + height=100, + fixed_top=True, + blocking=False, + data="data1", + controls=[Text(value="Are you sure?")], + footer=[Button(text="OK"), Button(text="Cancel")], + ) + + assert isinstance(d, flet.Control) + assert isinstance(d, flet.Dialog) + assert d.get_cmd_str() == [ + Command( + indent=0, + name=None, + values=["dialog"], + attrs={ + "autodismiss": "true", + "blocking": "false", + "data": "data1", + "fixedtop": "true", + "height": "100", + "maxwidth": "200", + "open": "true", + "subtext": "sub_text1", + "title": "Hello", + "type": "close", + "width": "100", + }, + lines=[], + commands=[], + ), + Command(indent=2, name=None, values=["text"], attrs={"value": "Are you sure?"}, lines=[], commands=[]), + Command(indent=2, name=None, values=["footer"], attrs={}, lines=[], commands=[]), + Command(indent=4, name=None, values=["button"], attrs={"text": "OK"}, lines=[], commands=[]), + Command(indent=4, name=None, values=["button"], attrs={"text": "Cancel"}, lines=[], commands=[]), + ], "Test failed" diff --git a/sdk/python/tests/test_dropdown.py b/sdk/python/tests/test_dropdown.py new file mode 100644 index 0000000000..abc011e4c2 --- /dev/null +++ b/sdk/python/tests/test_dropdown.py @@ -0,0 +1,58 @@ +import flet +from flet.dropdown import Option +from flet.protocol import Command + + +def test_option(): + opt = Option("key1") + assert isinstance(opt, flet.Control) + assert isinstance(opt, Option) + + +def test_dropdown(): + dd = flet.Dropdown( + id="list1", + label="Your favorite color:", + options=[Option(key="key1", text="value1"), Option(key="key2", text="value2")], + ) + + assert isinstance(dd, flet.Control) + assert isinstance(dd, flet.Dropdown) + assert dd.get_cmd_str() == [ + Command( + indent=0, + name=None, + values=["dropdown"], + attrs={"label": "Your favorite color:", "id": ("list1", True)}, + lines=[], + commands=[], + ), + Command(indent=2, name=None, values=["option"], attrs={"key": "key1", "text": "value1"}, lines=[], commands=[]), + Command(indent=2, name=None, values=["option"], attrs={"key": "key2", "text": "value2"}, lines=[], commands=[]), + ], "Test failed" + + do = Option("key1") + assert isinstance(do, Option) + + +def test_dropdown_with_just_keys(): + dd = flet.Dropdown( + id="list1", + label="Your favorite color:", + options=[Option(key="key1"), Option(key="key2")], + ) + assert dd.get_cmd_str() == [ + Command( + indent=0, + name=None, + values=["dropdown"], + attrs={"label": "Your favorite color:", "id": ("list1", True)}, + lines=[], + commands=[], + ), + Command(indent=2, name=None, values=["option"], attrs={"key": "key1"}, lines=[], commands=[]), + Command(indent=2, name=None, values=["option"], attrs={"key": "key2"}, lines=[], commands=[]), + ], "Test failed" + + do = Option("key1") + assert isinstance(do, Option) diff --git a/sdk/python/tests/test_form.py b/sdk/python/tests/test_form.py new file mode 100644 index 0000000000..fb272932ef --- /dev/null +++ b/sdk/python/tests/test_form.py @@ -0,0 +1,113 @@ +from dataclasses import dataclass +from dataclasses import field +from datetime import date +from datetime import datetime +from datetime import time +from enum import Enum + +from beartype.typing import List +from flet import Button +from flet import Checkbox +from flet import ChoiceGroup +from flet import ComboBox +from flet import Dropdown +from flet import Form +from flet import Message +from flet import SpinButton +from flet import Stack +from flet import Textbox +from flet.form import ListControl + + +class SelectionData(str, Enum): + A = "a" + B = "b" + C = "c" + + +class MoreSelectionData(str, Enum): + A = "a" + B = "b" + C = "c" + D = "d" + + +@dataclass +class ContainedObject: + contained_value: str = "a" + + +@dataclass +class TopLevelData: + string: str = "a" + integer: int = 1 + floating_point: float = 0.1 + boolean: bool = True + date_time: datetime = datetime(2022, 2, 2, 2, 2, 2) + just_date: date = date(2022, 2, 2) + just_time: time = time(2, 2, 2) + selection: SelectionData = SelectionData.A + dropdown_for_more_than_three_values: MoreSelectionData = MoreSelectionData.A + multiple_selection: List[SelectionData] = field(default_factory=lambda: [SelectionData.A, SelectionData.B]) + list_of_fields: List[str] = field(default_factory=lambda: ["a", "b", "c"]) + contained_object: ContainedObject = field(default_factory=ContainedObject) + list_of_contained_objects: List[ContainedObject] = field(default_factory=lambda: [ContainedObject()]) + + +def test_form_empty(control_type_tree): + form = Form(TopLevelData()) + + assert control_type_tree(form) == { + Form: [ + {Stack: [{Stack: ["String"]}, {Stack: [Textbox, Message]}]}, + {Stack: [{Stack: ["Integer"]}, {Stack: [SpinButton, Message]}]}, + {Stack: [{Stack: ["Floating point"]}, {Stack: [SpinButton, Message]}]}, + {Stack: [{Stack: ["Boolean"]}, {Stack: [Checkbox, Message]}]}, + {Stack: [{Stack: ["Date time"]}, {Stack: [Textbox, Message]}]}, + {Stack: [{Stack: ["Just date"]}, {Stack: [Textbox, Message]}]}, + {Stack: [{Stack: ["Just time"]}, {Stack: [Textbox, Message]}]}, + {Stack: [{Stack: ["Selection"]}, {Stack: [ChoiceGroup, Message]}]}, + {Stack: [{Stack: ["Dropdown for more than three values"]}, {Stack: [Dropdown, Message]}]}, + {Stack: [{Stack: ["Multiple selection"]}, {Stack: [ComboBox, Message]}]}, + { + Stack: [ + {Stack: ["List of fields", Button]}, + { + Stack: [ + { + ListControl: [ + {Stack: [Textbox, Button]}, + {Stack: [Textbox, Button]}, + {Stack: [Textbox, Button]}, + ] + }, + Message, + ] + }, + ] + }, + { + Stack: [ + {Stack: ["Contained object"]}, + {Stack: [{Stack: [{Stack: [{Stack: ["Contained value"]}, {Stack: [Textbox, Message]}]}]}]}, # str + ] + }, + { + Stack: [ + {Stack: ["List of contained objects", Button]}, + { + Stack: [ + { + ListControl: [ + {Stack: [Button, Button, Button]}, + Stack, + ] + }, + Message, + ] + }, + ] + }, + {Stack: [Message, Button]}, # Submit button + ] + } diff --git a/sdk/python/tests/test_grid.py b/sdk/python/tests/test_grid.py new file mode 100644 index 0000000000..c4d2c540ea --- /dev/null +++ b/sdk/python/tests/test_grid.py @@ -0,0 +1,144 @@ +import pytest +from beartype.roar import BeartypeCallHintPepParamException + +import flet +from flet import Grid +from flet.grid import Column +from flet.protocol import Command + + +class Contact: + def __init__(self, first_name, last_name): + self.first_name = first_name + self.last_name = last_name + + +expected_result = [ + Command( + indent=0, + name=None, + values=["grid"], + attrs={ + "compact": "true", + "headervisible": "true", + "selection": "multiple", + "shimmerlines": "1", + }, + lines=[], + commands=[], + ), + Command(indent=2, name=None, values=["columns"], attrs={}, lines=[], commands=[]), + Command( + indent=4, + name=None, + values=["column"], + attrs={ + "fieldname": "first_name", + "icon": "mail", + "icononly": "true", + "maxwidth": "200", + "minwidth": "100", + "name": "First name", + "resizable": "false", + "sortable": "string", + "sorted": "false", + "sortfield": "sort field name", + }, + lines=[], + commands=[], + ), + Command( + indent=4, + name=None, + values=["column"], + attrs={"fieldname": "last_name", "name": "Last name"}, + lines=[], + commands=[], + ), + Command(indent=2, name=None, values=["items"], attrs={}, lines=[], commands=[]), + Command( + indent=4, + name=None, + values=["item"], + attrs={"first_name": "Inesa", "last_name": "Fitsner"}, + lines=[], + commands=[], + ), + Command( + indent=4, + name=None, + values=["item"], + attrs={"first_name": "Fiodar", "last_name": "Fitsner"}, + lines=[], + commands=[], + ), +] + + +def test_grid_add__with_class(): + g = Grid( + selection_mode="multiple", + compact=True, + header_visible=True, + shimmer_lines=1, + columns=[ + Column( + field_name="first_name", + name="First name", + icon="mail", + icon_only=True, + sortable="string", + sort_field="sort field name", + sorted=False, + resizable=False, + min_width=100, + max_width=200, + ), + Column(field_name="last_name", name="Last name"), + ], + items=[ + Contact(first_name="Inesa", last_name="Fitsner"), + Contact(first_name="Fiodar", last_name="Fitsner"), + ], + ) + + assert isinstance(g, flet.Control) + assert isinstance(g, flet.Grid) + assert g.get_cmd_str() == expected_result, "Test failed" + + +def test_grid_add__with_dict(): + g = Grid( + selection_mode="multiple", + compact=True, + header_visible=True, + shimmer_lines=1, + columns=[ + Column( + field_name="first_name", + name="First name", + icon="mail", + icon_only=True, + sortable="string", + sort_field="sort field name", + sorted=False, + resizable=False, + min_width=100, + max_width=200, + ), + Column(field_name="last_name", name="Last name"), + ], + items=[ + {"first_name": "Inesa", "last_name": "Fitsner"}, + {"first_name": "Fiodar", "last_name": "Fitsner"}, + ], + ) + + assert isinstance(g, flet.Control) + assert isinstance(g, flet.Grid) + assert g.get_cmd_str() == expected_result, "Test failed" + + +def test_property_value_check(): + with pytest.raises(BeartypeCallHintPepParamException): + Column(icon_only="foo") diff --git a/sdk/python/tests/test_icon.py b/sdk/python/tests/test_icon.py new file mode 100644 index 0000000000..91351e4879 --- /dev/null +++ b/sdk/python/tests/test_icon.py @@ -0,0 +1,20 @@ +import flet +from flet import Icon +from flet.protocol import Command + + +def test_icon_add(): + c = Icon(name="Mail", color="#FF7F50", size="tiny") + assert isinstance(c, flet.Control) + assert isinstance(c, flet.Icon) + # raise Exception(s.get_cmd_str()) + assert c.get_cmd_str() == [ + Command( + indent=0, + name=None, + values=["icon"], + attrs={"color": "#FF7F50", "name": "Mail", "size": "tiny"}, + lines=[], + commands=[], + ) + ], "Test failed" diff --git a/sdk/python/tests/test_iframe.py b/sdk/python/tests/test_iframe.py new file mode 100644 index 0000000000..a85f0e71ff --- /dev/null +++ b/sdk/python/tests/test_iframe.py @@ -0,0 +1,128 @@ +from typing import List + +import flet +from flet import IFrame +from flet.protocol import Command + + +def test_iframe_add(): + c = IFrame(src="https://google.com", border_style="solid") + assert isinstance(c, flet.Control) + assert isinstance(c, flet.IFrame) + + assert c.get_cmd_str() == [ + Command( + indent=0, + name=None, + values=["iframe"], + attrs={"borderstyle": "solid", "src": "https://google.com"}, + lines=[], + commands=[], + ) + ], "Test failed" + + +def test_iframe_multiple_border_styles(): + c = IFrame(src="https://google.com", border_style=["solid", "none", "groove"]) + assert isinstance(c, flet.Control) + assert isinstance(c, flet.IFrame) + + # check property reading as a list + style = c.border_style + assert isinstance(style, List) + assert len(style) == 3 + + assert c.get_cmd_str() == [ + Command( + indent=0, + name=None, + values=["iframe"], + attrs={"borderstyle": "solid none groove", "src": "https://google.com"}, + lines=[], + commands=[], + ) + ], "Test failed" + + +def test_iframe_border_style(): + # list of values + c = IFrame(border_style=["solid", "dashed"]) + v = c.border_style + assert isinstance(v, List) + assert len(v) == 2 + + # single value + c.border_style = "groove" + v = c.border_style + assert isinstance(v, str) + assert v == "groove" + + # none + c.border_style = None + v = c.border_style + assert v == "" + + +def test_iframe_border_color(): + # list of values + c = IFrame(border_color=["red", "yellow"]) + v = c.border_color + assert isinstance(v, List) + assert len(v) == 2 + + # single value + c.border_color = "blue" + v = c.border_color + assert isinstance(v, str) + assert v == "blue" + + # none + c.border_color = None + v = c.border_color + assert v == "" + + +def test_iframe_border_width(): + # list of values + c = IFrame(border_width=["1px", "2px", "1", "2"]) + v = c.border_width + assert isinstance(v, List) + assert len(v) == 4 + + # single value + c.border_width = "10" + v = c.border_width + assert isinstance(v, str) + assert v == "10" + + # none + c.border_width = None + v = c.border_width + assert v == "" + + +def test_iframe_border_width_mixed(): + # list of values + c = IFrame(border_width=["1px", "2px", 1, 2]) + v = c.border_width + assert isinstance(v, List) + assert len(v) == 4 + + +def test_iframe_border_radius(): + # list of values + c = IFrame(border_radius=["1px", "2px", "1", "2"]) + v = c.border_radius + assert isinstance(v, List) + assert len(v) == 4 + + # single value + c.border_radius = "10" + v = c.border_radius + assert isinstance(v, str) + assert v == "10" + + # none + c.border_radius = None + v = c.border_radius + assert v == "" diff --git a/sdk/python/tests/test_image.py b/sdk/python/tests/test_image.py new file mode 100644 index 0000000000..a436cefdb9 --- /dev/null +++ b/sdk/python/tests/test_image.py @@ -0,0 +1,29 @@ +import flet +from flet import Image +from flet.protocol import Command + + +def test_image_add(): + i = Image( + src="https://www.w3schools.com/css/img_5terre.jpg", + alt="This is image", + title="This is title", + maximize_frame=False, + ) + assert isinstance(i, flet.Control) + assert isinstance(i, flet.Image) + assert i.get_cmd_str() == [ + Command( + indent=0, + name=None, + values=["image"], + attrs={ + "alt": "This is image", + "maximizeframe": "false", + "src": "https://www.w3schools.com/css/img_5terre.jpg", + "title": "This is title", + }, + lines=[], + commands=[], + ) + ], "Test failed" diff --git a/sdk/python/tests/test_linechart.py b/sdk/python/tests/test_linechart.py new file mode 100644 index 0000000000..06e61d36cb --- /dev/null +++ b/sdk/python/tests/test_linechart.py @@ -0,0 +1,70 @@ +import flet +from flet import LineChart +from flet.linechart import Data, Point +from flet.protocol import Command + + +def test_verticalbarchart_add(): + lc = LineChart( + legend=True, + tooltips=True, + stroke_width=4, + y_min=0, + y_max=100, + y_ticks=2, + y_format="{y}%", + x_type="number", + lines=[ + Data( + color="yellow", + legend="yellow color", + points=[Point(x=1, y=100), Point(x=5, y=50)], + ), + Data( + color="green", + legend="green color", + points=[Point(x=10, y=20), Point(x=20, y=10)], + ), + ], + ) + assert isinstance(lc, flet.Control) + assert isinstance(lc, flet.LineChart) + assert lc.get_cmd_str() == [ + Command( + indent=0, + name=None, + values=["linechart"], + attrs={ + "legend": "true", + "strokewidth": "4", + "tooltips": "true", + "xtype": "number", + "yformat": "{y}%", + "ymax": "100", + "ymin": "0", + "yticks": "2", + }, + lines=[], + commands=[], + ), + Command( + indent=2, + name=None, + values=["data"], + attrs={"color": "yellow", "legend": "yellow color"}, + lines=[], + commands=[], + ), + Command(indent=4, name=None, values=["p"], attrs={"x": "1", "y": "100"}, lines=[], commands=[]), + Command(indent=4, name=None, values=["p"], attrs={"x": "5", "y": "50"}, lines=[], commands=[]), + Command( + indent=2, + name=None, + values=["data"], + attrs={"color": "green", "legend": "green color"}, + lines=[], + commands=[], + ), + Command(indent=4, name=None, values=["p"], attrs={"x": "10", "y": "20"}, lines=[], commands=[]), + Command(indent=4, name=None, values=["p"], attrs={"x": "20", "y": "10"}, lines=[], commands=[]), + ], "Test failed" diff --git a/sdk/python/tests/test_link.py b/sdk/python/tests/test_link.py new file mode 100644 index 0000000000..aa29effb6a --- /dev/null +++ b/sdk/python/tests/test_link.py @@ -0,0 +1,61 @@ +import flet +from flet import Link, Text +from flet.protocol import Command + +""" +def test_button_primary_must_be_bool(): + with pytest.raises(Exception): + Button(id="button1", text="My button", primary="1") + +""" + + +def test_link_add(): + l = Link(value="search", url="http://google.com", align="left", new_window=True) + assert isinstance(l, flet.Control) + assert isinstance(l, flet.Link) + assert l.get_cmd_str() == [ + Command( + indent=0, + name=None, + values=["link"], + attrs={"align": "left", "newwindow": "true", "url": "http://google.com", "value": "search"}, + lines=[], + commands=[], + ) + ], "Test failed" + + +def test_link_with_controls(): + l = Link( + value="Visit google", + url="https://google.com", + pre=True, + align="right", + width="100", + size="large1", + title="Link title", + controls=[Text(value="LinkText1"), Text(value="LinkText2")], + ) + assert isinstance(l, flet.Control) + assert isinstance(l, flet.Link) + assert l.get_cmd_str() == [ + Command( + indent=0, + name=None, + values=["link"], + attrs={ + "align": "right", + "pre": "true", + "size": "large1", + "title": "Link title", + "url": "https://google.com", + "value": "Visit google", + "width": "100", + }, + lines=[], + commands=[], + ), + Command(indent=2, name=None, values=["text"], attrs={"value": "LinkText1"}, lines=[], commands=[]), + Command(indent=2, name=None, values=["text"], attrs={"value": "LinkText2"}, lines=[], commands=[]), + ], "Test failed" diff --git a/sdk/python/tests/test_message.py b/sdk/python/tests/test_message.py new file mode 100644 index 0000000000..45ae4bb535 --- /dev/null +++ b/sdk/python/tests/test_message.py @@ -0,0 +1,75 @@ +import flet +from flet.message import MessageButton +from flet.protocol import Command + + +def test_button(): + b1 = MessageButton("text1") + assert isinstance(b1, flet.Control) + assert isinstance(b1, MessageButton) + + +def test_message(): + m = flet.Message( + value="This is message", + dismiss=True, + buttons=[ + MessageButton(text="Yes, I agree", action="Yes"), + MessageButton(text="No, I disagree", action="No"), + ], + ) + + assert isinstance(m, flet.Control) + assert isinstance(m, flet.Message) + assert m.get_cmd_str() == [ + Command( + indent=0, + name=None, + values=["message"], + attrs={"dismiss": "true", "value": "This is message"}, + lines=[], + commands=[], + ), + Command( + indent=2, + name=None, + values=["button"], + attrs={"action": "Yes", "text": "Yes, I agree"}, + lines=[], + commands=[], + ), + Command( + indent=2, + name=None, + values=["button"], + attrs={"action": "No", "text": "No, I disagree"}, + lines=[], + commands=[], + ), + ], "Test failed" + + +def test_message_button_with_just_text(): + m = flet.Message( + value="This is message", + dismiss=True, + buttons=[ + MessageButton(text="Yes, I agree"), + MessageButton(text="No, I disagree"), + ], + ) + + assert isinstance(m, flet.Control) + assert isinstance(m, flet.Message) + assert m.get_cmd_str() == [ + Command( + indent=0, + name=None, + values=["message"], + attrs={"dismiss": "true", "value": "This is message"}, + lines=[], + commands=[], + ), + Command(indent=2, name=None, values=["button"], attrs={"text": "Yes, I agree"}, lines=[], commands=[]), + Command(indent=2, name=None, values=["button"], attrs={"text": "No, I disagree"}, lines=[], commands=[]), + ], "Test failed" diff --git a/sdk/python/tests/test_nav.py b/sdk/python/tests/test_nav.py new file mode 100644 index 0000000000..7cdc0252e5 --- /dev/null +++ b/sdk/python/tests/test_nav.py @@ -0,0 +1,70 @@ +import flet +from flet.nav import Item +from flet.protocol import Command + + +def test_item(): + ni = Item("key1") + assert isinstance(ni, flet.Control) + assert isinstance(ni, Item) + + +def test_nav(): + n = flet.Nav( + id="list1", + value="list1", + items=[ + Item( + key="key1", + text="item1", + icon="mail", + icon_color="green", + url="https://google.com", + new_window=True, + expanded=True, + ), + Item(key="key2", text="item2"), + ], + ) + + assert isinstance(n, flet.Control) + assert isinstance(n, flet.Nav) + assert n.get_cmd_str() == [ + Command( + indent=0, name=None, values=["nav"], attrs={"value": "list1", "id": ("list1", True)}, lines=[], commands=[] + ), + Command( + indent=2, + name=None, + values=["item"], + attrs={ + "expanded": "true", + "icon": "mail", + "iconcolor": "green", + "key": "key1", + "newwindow": "true", + "text": "item1", + "url": "https://google.com", + }, + lines=[], + commands=[], + ), + Command(indent=2, name=None, values=["item"], attrs={"key": "key2", "text": "item2"}, lines=[], commands=[]), + ], "Test failed" + + ni = Item("key1") + assert isinstance(ni, Item) + + +def test_nav_with_just_keys(): + n = flet.Nav(id="list1", value="list1", items=[Item(key="key1"), Item(key="key2")]) + assert n.get_cmd_str() == [ + Command( + indent=0, name=None, values=["nav"], attrs={"value": "list1", "id": ("list1", True)}, lines=[], commands=[] + ), + Command(indent=2, name=None, values=["item"], attrs={"key": "key1"}, lines=[], commands=[]), + Command(indent=2, name=None, values=["item"], attrs={"key": "key2"}, lines=[], commands=[]), + ], "Test failed" + + ni = Item("key1") + assert isinstance(ni, Item) diff --git a/sdk/python/tests/test_page.py b/sdk/python/tests/test_page.py new file mode 100644 index 0000000000..52e9adca97 --- /dev/null +++ b/sdk/python/tests/test_page.py @@ -0,0 +1,6 @@ +import pytest + + +@pytest.mark.skip(reason="no way of currently testing this") +def test_page(page): + assert page.url != "" and page.url.startswith("http"), "Test failed" diff --git a/sdk/python/tests/test_panel.py b/sdk/python/tests/test_panel.py new file mode 100644 index 0000000000..1e48f021ba --- /dev/null +++ b/sdk/python/tests/test_panel.py @@ -0,0 +1,44 @@ +import flet +from flet import Button, Panel, Text +from flet.protocol import Command + + +def test_panel_add(): + p = Panel( + open=True, + title="Hello", + type="small", + auto_dismiss=True, + light_dismiss=False, + width=100, + blocking=False, + data="data1", + controls=[Text(value="Are you sure?")], + footer=[Button(text="OK"), Button(text="Cancel")], + ) + + assert isinstance(p, flet.Control) + assert isinstance(p, flet.Panel) + assert p.get_cmd_str() == [ + Command( + indent=0, + name=None, + values=["panel"], + attrs={ + "autodismiss": "true", + "blocking": "false", + "data": "data1", + "lightdismiss": "false", + "open": "true", + "title": "Hello", + "type": "small", + "width": "100", + }, + lines=[], + commands=[], + ), + Command(indent=2, name=None, values=["text"], attrs={"value": "Are you sure?"}, lines=[], commands=[]), + Command(indent=2, name=None, values=["footer"], attrs={}, lines=[], commands=[]), + Command(indent=4, name=None, values=["button"], attrs={"text": "OK"}, lines=[], commands=[]), + Command(indent=4, name=None, values=["button"], attrs={"text": "Cancel"}, lines=[], commands=[]), + ], "Test failed" diff --git a/sdk/python/tests/test_piechart.py b/sdk/python/tests/test_piechart.py new file mode 100644 index 0000000000..dc89df2150 --- /dev/null +++ b/sdk/python/tests/test_piechart.py @@ -0,0 +1,48 @@ +import flet +from flet import PieChart +from flet.piechart import Point +from flet.protocol import Command + + +def test_piechart_add(): + pc = PieChart( + legend=True, + tooltips=True, + inner_value=40, + inner_radius=42, + width="100%", + points=[ + Point(value=20, color="yellow", legend="Yellow color", tooltip="20%"), + Point(value=30, color="green", legend="Green color", tooltip="30%"), + ], + ) + + assert isinstance(pc, flet.Control) + assert isinstance(pc, flet.PieChart) + assert pc.get_cmd_str() == [ + Command( + indent=0, + name=None, + values=["piechart"], + attrs={"innerradius": "42", "innervalue": "40", "legend": "true", "tooltips": "true", "width": "100%"}, + lines=[], + commands=[], + ), + Command(indent=2, name=None, values=["data"], attrs={}, lines=[], commands=[]), + Command( + indent=4, + name=None, + values=["p"], + attrs={"color": "yellow", "legend": "Yellow color", "tooltip": "20%", "value": "20"}, + lines=[], + commands=[], + ), + Command( + indent=4, + name=None, + values=["p"], + attrs={"color": "green", "legend": "Green color", "tooltip": "30%", "value": "30"}, + lines=[], + commands=[], + ), + ], "Test failed" diff --git a/sdk/python/tests/test_progress.py b/sdk/python/tests/test_progress.py new file mode 100644 index 0000000000..1748427e0e --- /dev/null +++ b/sdk/python/tests/test_progress.py @@ -0,0 +1,20 @@ +import flet +from flet import Progress +from flet.protocol import Command + + +def test_progress_add(): + c = Progress(label="Doing something...", value=10) + assert isinstance(c, flet.Control) + assert isinstance(c, flet.Progress) + # raise Exception(s.get_cmd_str()) + assert c.get_cmd_str() == [ + Command( + indent=0, + name=None, + values=["progress"], + attrs={"label": "Doing something...", "value": "10"}, + lines=[], + commands=[], + ) + ], "Test failed" diff --git a/sdk/python/tests/test_searchbox.py b/sdk/python/tests/test_searchbox.py new file mode 100644 index 0000000000..3e44de1118 --- /dev/null +++ b/sdk/python/tests/test_searchbox.py @@ -0,0 +1,34 @@ +import flet +from flet import SearchBox +from flet.protocol import Command + + +def test_searchbox_add(): + sb = SearchBox( + value="", + placeholder="search for something", + underlined=True, + icon="icon1", + icon_color="color1", + data="data1", + ) + assert isinstance(sb, flet.Control) + assert isinstance(sb, flet.SearchBox) + assert sb.get_cmd_str() == [ + Command( + indent=0, + name=None, + values=["searchbox"], + attrs={ + "data": "data1", + "icon": "icon1", + "iconcolor": "color1", + "onchange": "false", + "placeholder": "search for something", + "underlined": "true", + "value": "", + }, + lines=[], + commands=[], + ) + ], "Test failed" diff --git a/sdk/python/tests/test_slider.py b/sdk/python/tests/test_slider.py new file mode 100644 index 0000000000..da3fe80752 --- /dev/null +++ b/sdk/python/tests/test_slider.py @@ -0,0 +1,39 @@ +import flet +from flet import Slider +from flet.protocol import Command + + +def test_slider_add(): + s = Slider( + value=1, + label="To what extend you agree", + min=0, + max=10, + step=1, + show_value=True, + value_format="current_value is {value}", + vertical=True, + height=200, + ) + assert isinstance(s, flet.Control) + assert isinstance(s, flet.Slider) + assert s.get_cmd_str() == [ + Command( + indent=0, + name=None, + values=["slider"], + attrs={ + "height": "200", + "label": "To what extend you agree", + "max": "10", + "min": "0", + "showvalue": "true", + "step": "1", + "value": "1", + "valueformat": "current_value is {value}", + "vertical": "true", + }, + lines=[], + commands=[], + ) + ], "Test failed" diff --git a/sdk/python/tests/test_spinbutton.py b/sdk/python/tests/test_spinbutton.py new file mode 100644 index 0000000000..7493e14174 --- /dev/null +++ b/sdk/python/tests/test_spinbutton.py @@ -0,0 +1,37 @@ +import flet +from flet import SpinButton +from flet.protocol import Command + + +def test_spinbutton_add(): + s = SpinButton( + value=1, + label="To what extent you agree", + min=0, + max=10, + step=1, + icon="icon_name", + width=200, + data="data1", + ) + assert isinstance(s, flet.Control) + assert isinstance(s, flet.SpinButton) + assert s.get_cmd_str() == [ + Command( + indent=0, + name=None, + values=["spinbutton"], + attrs={ + "data": "data1", + "icon": "icon_name", + "label": "To what extent you agree", + "max": "10", + "min": "0", + "step": "1", + "value": "1", + "width": "200", + }, + lines=[], + commands=[], + ) + ], "Test failed" diff --git a/sdk/python/tests/test_splitstack.py b/sdk/python/tests/test_splitstack.py new file mode 100644 index 0000000000..8b7f22b1cb --- /dev/null +++ b/sdk/python/tests/test_splitstack.py @@ -0,0 +1,49 @@ +import flet +from flet import SplitStack, Stack +from flet.protocol import Command + + +def test_splitstack_add(): + s = SplitStack( + horizontal=True, + gutter_size=10, + gutter_color="yellow", + gutter_hover_color="orange", + gutter_drag_color="blue", + controls=[Stack(id="left"), Stack(id="center")], + ) + assert isinstance(s, flet.Control) + assert isinstance(s, flet.SplitStack) + + assert s.get_cmd_str() == [ + Command( + indent=0, + name=None, + values=["splitstack"], + attrs={ + "guttercolor": "yellow", + "gutterdragcolor": "blue", + "gutterhovercolor": "orange", + "guttersize": "10", + "horizontal": "true", + }, + lines=[], + commands=[], + ), + Command( + indent=2, + name=None, + values=["stack"], + attrs={"id": ("left", True)}, + lines=[], + commands=[], + ), + Command( + indent=2, + name=None, + values=["stack"], + attrs={"id": ("center", True)}, + lines=[], + commands=[], + ), + ], "Test failed" diff --git a/sdk/python/tests/test_stack.py b/sdk/python/tests/test_stack.py new file mode 100644 index 0000000000..ae445cda60 --- /dev/null +++ b/sdk/python/tests/test_stack.py @@ -0,0 +1,82 @@ +import flet +from flet import Button, Stack, Textbox +from flet.protocol import Command + + +def test_stack_add(): + s = Stack( + horizontal=True, + vertical_fill=True, + horizontal_align="center", + vertical_align="baseline", + gap="large", + wrap=True, + scroll_x=True, + scroll_y=True, + controls=[Textbox(id="firstName"), Textbox(id="lastName")], + ) + assert isinstance(s, flet.Control) + assert isinstance(s, flet.Stack) + # raise Exception(s.get_cmd_str()) + assert s.get_cmd_str() == [ + Command( + indent=0, + name=None, + values=["stack"], + attrs={ + "gap": "large", + "horizontal": "true", + "horizontalalign": "center", + "scrollx": "true", + "scrolly": "true", + "verticalalign": "baseline", + "verticalfill": "true", + "wrap": "true", + }, + lines=[], + commands=[], + ), + Command(indent=2, name=None, values=["textbox"], attrs={"id": ("firstName", True)}, lines=[], commands=[]), + Command(indent=2, name=None, values=["textbox"], attrs={"id": ("lastName", True)}, lines=[], commands=[]), + ], "Test failed" + + +def test_nested_stacks_add(): + s = Stack( + controls=[ + Textbox(id="firstName"), + Textbox(id="lastName"), + Stack( + horizontal=True, + controls=[ + Button(id="ok", text="OK", primary=True), + Button(id="cancel", text="Cancel"), + ], + ), + ] + ) + assert isinstance(s, flet.Control) + assert isinstance(s, flet.Stack) + # raise Exception(s.get_cmd_str()) + assert s.get_cmd_str() == [ + Command(indent=0, name=None, values=["stack"], attrs={}, lines=[], commands=[]), + Command(indent=2, name=None, values=["textbox"], attrs={"id": ("firstName", True)}, lines=[], commands=[]), + Command(indent=2, name=None, values=["textbox"], attrs={"id": ("lastName", True)}, lines=[], commands=[]), + Command(indent=2, name=None, values=["stack"], attrs={"horizontal": "true"}, lines=[], commands=[]), + Command( + indent=4, + name=None, + values=["button"], + attrs={"primary": "true", "text": "OK", "id": ("ok", True)}, + lines=[], + commands=[], + ), + Command( + indent=4, + name=None, + values=["button"], + attrs={"text": "Cancel", "id": ("cancel", True)}, + lines=[], + commands=[], + ), + ], "Test failed" diff --git a/sdk/python/tests/test_tabs.py b/sdk/python/tests/test_tabs.py new file mode 100644 index 0000000000..75f6f50a55 --- /dev/null +++ b/sdk/python/tests/test_tabs.py @@ -0,0 +1,97 @@ +import flet +import pytest +from flet import Button, Tab, Tabs, Textbox +from flet.protocol import Command + + +def test_tabs_add(): + t = Tabs(tabs=[Tab(text="Tab1"), Tab("Tab2"), Tab("Tab3")]) + + assert isinstance(t, flet.Control) + assert isinstance(t, flet.Tabs) + assert t.get_cmd_str() == [ + Command(indent=0, name=None, values=["tabs"], attrs={"value": "Tab1"}, lines=[], commands=[]), + Command(indent=2, name=None, values=["tab"], attrs={"text": "Tab1"}, lines=[], commands=[]), + Command(indent=2, name=None, values=["tab"], attrs={"text": "Tab2"}, lines=[], commands=[]), + Command(indent=2, name=None, values=["tab"], attrs={"text": "Tab3"}, lines=[], commands=[]), + ], "Test failed" + + +def test_tabs_with_controls_add(): + t = Tabs( + tabs=[ + Tab(text="Tab1", controls=[Button(text="OK"), Button(text="Cancel")]), + Tab( + "Tab2", + controls=[Textbox(label="Textbox 1"), Textbox(label="Textbox 2")], + ), + ] + ) + assert isinstance(t, flet.Control) + assert isinstance(t, flet.Tabs) + assert t.get_cmd_str() == [ + Command(indent=0, name=None, values=["tabs"], attrs={"value": "Tab1"}, lines=[], commands=[]), + Command(indent=2, name=None, values=["tab"], attrs={"text": "Tab1"}, lines=[], commands=[]), + Command(indent=4, name=None, values=["button"], attrs={"text": "OK"}, lines=[], commands=[]), + Command(indent=4, name=None, values=["button"], attrs={"text": "Cancel"}, lines=[], commands=[]), + Command(indent=2, name=None, values=["tab"], attrs={"text": "Tab2"}, lines=[], commands=[]), + Command(indent=4, name=None, values=["textbox"], attrs={"label": "Textbox 1"}, lines=[], commands=[]), + Command(indent=4, name=None, values=["textbox"], attrs={"label": "Textbox 2"}, lines=[], commands=[]), + ], "Test failed" + + +def test_value__initialized_as_empty(): + t = Tabs() + + assert t.value == "" + + +def test_value__has_an_initial_value(): + t = Tabs(tabs=[Tab(text="Tab1", controls=[Button(text="OK")])]) + + assert t.value == "Tab1" + + +def test_value__key_is_preferred(): + t = Tabs(tabs=[Tab(text="Tab1", key="first_tab_key", controls=[Button(text="OK")])]) + + assert t.value == "first_tab_key" + + +def test_value__can_be_set_on_initialization(): + t = Tabs( + value="Tab2", + tabs=[ + Tab("Tab1", controls=[Button(text="OK")]), + Tab("Tab2", controls=[Button(text="Not OK")]), + ], + ) + assert t.value == "Tab2" + + +def test_value__emptying_tabs_after_initialization_clears_value_as_well(): + t = Tabs(tabs=[Tab(text="Tab1", controls=[Button(text="OK")])]) + t.tabs = None + + assert t.tabs == [] + assert t.value == "" + + +def test_value__setting_value_to_empty_only_allowed_when_no_tabs(): + t = Tabs(tabs=[Tab(text="Tab1", controls=[Button(text="OK")])]) + with pytest.raises(AssertionError): + t.value = "" + + t = Tabs() + t.value = "" + + assert t.value == "" + + +def test_value__can_be_set_to_valid_tab_key_only_when_tabs(): + t = Tabs(tabs=[Tab(text="Tab1", key="first_tab_key", controls=[Button(text="OK")])]) + + t.value = "Tab1" + t.value = "first_tab_key" + with pytest.raises(AssertionError): + t.value = "Unknown value" diff --git a/sdk/python/tests/test_text.py b/sdk/python/tests/test_text.py new file mode 100644 index 0000000000..5544c11b26 --- /dev/null +++ b/sdk/python/tests/test_text.py @@ -0,0 +1,82 @@ +import flet +from flet import Button, Stack, Text +from flet.protocol import Command + + +def test_text_add(): + c = Text( + value="Hello,\nworld!", + markdown=True, + align="left", + vertical_align="top", + size="tiny", + bold=True, + italic=False, + pre=False, + nowrap=True, + block=False, + color="#9FE2BF", + bgcolor="#FF7F50", + border_style="dotted", + border_width="1", + border_color="yellow", + border_radius="4px", + ) + assert isinstance(c, flet.Control) + assert isinstance(c, flet.Text) + # raise Exception(s.get_cmd_str()) + # assert c.get_cmd_str() == ('text align="left" block="false" bold='true italic="false" nowrap="true" pre="false" size="tiny" value="Hello,\\nworld!" verticalAlign="left"'), "Test failed" + assert c.get_cmd_str() == [ + Command( + indent=0, + name=None, + values=["text"], + attrs={ + "align": "left", + "bgcolor": "#FF7F50", + "block": "false", + "bold": "true", + "bordercolor": "yellow", + "borderradius": "4px", + "borderstyle": "dotted", + "borderwidth": "1", + "color": "#9FE2BF", + "italic": "false", + "markdown": "true", + "nowrap": "true", + "pre": "false", + "size": "tiny", + "value": "Hello,\nworld!", + "verticalalign": "top", + }, + lines=[], + commands=[], + ) + ], "Test failed" + + +def test_text_double_quotes(): + c = Text(value='Hello, "world!"') + # raise Exception(c.get_cmd_str()) + assert c.get_cmd_str() == [ + Command(indent=0, name=None, values=["text"], attrs={"value": 'Hello, "world!"'}, lines=[], commands=[]) + ], "Test failed" + + +def test_add_text_inside_stack(): + text = Text(id="txt1", value='Hello, "world!"') + button = Button(text="Super button") + stack = Stack(id="header", controls=[text, button]) + + assert stack.get_cmd_str() == [ + Command(indent=0, name=None, values=["stack"], attrs={"id": ("header", True)}, lines=[], commands=[]), + Command( + indent=2, + name=None, + values=["text"], + attrs={"value": 'Hello, "world!"', "id": ("txt1", True)}, + lines=[], + commands=[], + ), + Command(indent=2, name=None, values=["button"], attrs={"text": "Super button"}, lines=[], commands=[]), + ], "Test failed" diff --git a/sdk/python/tests/test_textbox.py b/sdk/python/tests/test_textbox.py new file mode 100644 index 0000000000..8d33dd717f --- /dev/null +++ b/sdk/python/tests/test_textbox.py @@ -0,0 +1,18 @@ +import flet +from flet.protocol import Command + + +def test_textbox_add(): + tb = flet.Textbox(id="txt1", label="Your name:") + assert isinstance(tb, flet.Control) + assert isinstance(tb, flet.Textbox) + assert [ + Command( + indent=" ", + name=None, + values=["textbox"], + attrs={"label": "Your name:", "id": ("txt1", True)}, + lines=[], + commands=[], + ) + ], "Test failed" diff --git a/sdk/python/tests/test_toggle.py b/sdk/python/tests/test_toggle.py new file mode 100644 index 0000000000..dc2dd4ddcc --- /dev/null +++ b/sdk/python/tests/test_toggle.py @@ -0,0 +1,38 @@ +import flet +from flet import Toggle +from flet.protocol import Command + +""" +def test_button_primary_must_be_bool(): + with pytest.raises(Exception): + Button(id="button1", text="My button", primary="1") + +""" + + +def test_toggle_add(): + t = Toggle( + value=True, + label="This is toggle", + inline=True, + on_text="on text", + off_text="off text", + ) + assert isinstance(t, flet.Control) + assert isinstance(t, flet.Toggle) + assert t.get_cmd_str() == [ + Command( + indent=0, + name=None, + values=["toggle"], + attrs={ + "inline": "true", + "label": "This is toggle", + "offtext": "off text", + "ontext": "on text", + "value": "true", + }, + lines=[], + commands=[], + ) + ], "Test failed" diff --git a/sdk/python/tests/test_toolbar.py b/sdk/python/tests/test_toolbar.py new file mode 100644 index 0000000000..3d4853735e --- /dev/null +++ b/sdk/python/tests/test_toolbar.py @@ -0,0 +1,83 @@ +import flet +from flet import Toolbar, toolbar +from flet.protocol import Command + + +def test_toolbar_add(): + t = Toolbar( + inverted=True, + items=[ + toolbar.Item( + text="text1", + secondary_text="text2", + url="url", + new_window=True, + icon="icon", + icon_color="green", + icon_only=False, + split=True, + divider=True, + ) + ], + overflow=[ + toolbar.Item( + text="text12", + secondary_text="text22", + url="url2", + new_window=True, + icon="icon", + icon_color="green", + icon_only=False, + split=True, + divider=True, + ), + toolbar.Item(text="overflow"), + ], + far=[toolbar.Item(text="far")], + ) + + assert isinstance(t, flet.Control) + assert isinstance(t, flet.Toolbar) + assert t.get_cmd_str() == [ + Command(indent=0, name=None, values=["toolbar"], attrs={"inverted": "true"}, lines=[], commands=[]), + Command( + indent=2, + name=None, + values=["item"], + attrs={ + "divider": "true", + "icon": "icon", + "iconcolor": "green", + "icononly": "false", + "newwindow": "true", + "secondarytext": "text2", + "split": "true", + "text": "text1", + "url": "url", + }, + lines=[], + commands=[], + ), + Command(indent=2, name=None, values=["overflow"], attrs={}, lines=[], commands=[]), + Command( + indent=4, + name=None, + values=["item"], + attrs={ + "divider": "true", + "icon": "icon", + "iconcolor": "green", + "icononly": "false", + "newwindow": "true", + "secondarytext": "text22", + "split": "true", + "text": "text12", + "url": "url2", + }, + lines=[], + commands=[], + ), + Command(indent=4, name=None, values=["item"], attrs={"text": "overflow"}, lines=[], commands=[]), + Command(indent=2, name=None, values=["far"], attrs={}, lines=[], commands=[]), + Command(indent=4, name=None, values=["item"], attrs={"text": "far"}, lines=[], commands=[]), + ], "Test failed" diff --git a/sdk/python/tests/test_update.py b/sdk/python/tests/test_update.py new file mode 100644 index 0000000000..e84006ab5d --- /dev/null +++ b/sdk/python/tests/test_update.py @@ -0,0 +1,10 @@ +import pytest + +from flet import Textbox + + +@pytest.mark.skip(reason="no way of currently testing this") +def test_update_single_control(page): + txt = Textbox(id="txt1", label="First name:") + page.add(txt) + page.update(txt) diff --git a/sdk/python/tests/test_verticalbarchart.py b/sdk/python/tests/test_verticalbarchart.py new file mode 100644 index 0000000000..dd1443a382 --- /dev/null +++ b/sdk/python/tests/test_verticalbarchart.py @@ -0,0 +1,70 @@ +import flet +from flet import VerticalBarChart +from flet.protocol import Command +from flet.verticalbarchart import Point + + +def test_verticalbarchart_add(): + vbc = VerticalBarChart( + legend=True, + tooltips=False, + bar_width=56, + colors="green yellow", + y_min=0, + y_max=1000, + y_ticks=200, + y_format="format{y}", + x_type="number", + points=[ + Point( + x="1", + y=100, + legend="legend", + color="green", + x_tooltip="x tooltip", + y_tooltip="y tooltip", + ), + Point(x="80", y=200), + Point(x="100", y=300), + ], + ) + assert isinstance(vbc, flet.Control) + assert isinstance(vbc, flet.VerticalBarChart) + assert vbc.get_cmd_str() == [ + Command( + indent=0, + name=None, + values=["verticalbarchart"], + attrs={ + "barwidth": "56", + "colors": "green yellow", + "legend": "true", + "tooltips": "false", + "xtype": "number", + "yformat": "format{y}", + "ymax": "1000", + "ymin": "0", + "yticks": "200", + }, + lines=[], + commands=[], + ), + Command(indent=2, name=None, values=["data"], attrs={}, lines=[], commands=[]), + Command( + indent=4, + name=None, + values=["p"], + attrs={ + "color": "green", + "legend": "legend", + "x": "1", + "xtooltip": "x tooltip", + "y": "100", + "ytooltip": "y tooltip", + }, + lines=[], + commands=[], + ), + Command(indent=4, name=None, values=["p"], attrs={"x": "80", "y": "200"}, lines=[], commands=[]), + Command(indent=4, name=None, values=["p"], attrs={"x": "100", "y": "300"}, lines=[], commands=[]), + ], "Test failed" diff --git a/server/cmd/flet/main.go b/server/cmd/flet/main.go index b860f07a26..df88780769 100644 --- a/server/cmd/flet/main.go +++ b/server/cmd/flet/main.go @@ -24,7 +24,7 @@ func main() { } }() - if err := commands.NewRootCmd().ExecuteContext(ctx); err != nil { + if err := commands.NewRootCmd(cancel).ExecuteContext(ctx); err != nil { os.Exit(1) } } diff --git a/server/commands/root.go b/server/commands/root.go index bd1030a729..c87abe2e60 100644 --- a/server/commands/root.go +++ b/server/commands/root.go @@ -1,6 +1,8 @@ package commands import ( + "context" + "github.com/spf13/cobra" ) @@ -9,7 +11,7 @@ var ( LogLevel string ) -func NewRootCmd() *cobra.Command { +func NewRootCmd(cancel context.CancelFunc) *cobra.Command { cmd := &cobra.Command{ Use: "flet", Short: "Flet", @@ -24,7 +26,7 @@ func NewRootCmd() *cobra.Command { cmd.PersistentFlags().StringVarP(&LogLevel, "log-level", "l", "info", "verbosity level for logs") cmd.AddCommand( - newServerCommand(), + newServerCommand(cancel), ) return cmd diff --git a/server/commands/server.go b/server/commands/server.go index bdad5ab5f7..7d65f1b8e1 100644 --- a/server/commands/server.go +++ b/server/commands/server.go @@ -1,13 +1,14 @@ package commands import ( + "context" "fmt" + "net" "os" "os/exec" - "path/filepath" + "strconv" "sync" - "github.com/alexflint/go-filemutex" "github.com/flet-dev/flet/server/cache" "github.com/flet-dev/flet/server/config" "github.com/flet-dev/flet/server/server" @@ -16,7 +17,7 @@ import ( "github.com/spf13/cobra" ) -func newServerCommand() *cobra.Command { +func newServerCommand(cancel context.CancelFunc) *cobra.Command { var serverPort int var background bool @@ -28,24 +29,23 @@ func newServerCommand() *cobra.Command { Long: `Server is for ...`, Run: func(cmd *cobra.Command, args []string) { - if background { - startServerService(attachedProcess) - return + if serverPort == 0 { + var err error + serverPort, err = getFreePort() + if err != nil { + log.Fatalf("Error finding a free TCP port: %s.", err) + } } - // ensure one executable instance is running - m, err := filemutex.New(getLockFilename(serverPort)) - if err != nil { - log.Fatalln("Cannot create mutex - directory did not exist or file could not be created") + if background { + startServerService(serverPort, attachedProcess) + return } - err = m.TryLock() - if err != nil { - log.Fatalf("Another instance of Flet Server is already listening on port %d", serverPort) + if attachedProcess { + go monitorParentProcess(cancel) } - defer m.Unlock() - // init cache cache.Init() @@ -63,17 +63,36 @@ func newServerCommand() *cobra.Command { return cmd } -func startServerService(attached bool) { - log.Traceln("Starting Flet Server") +func monitorParentProcess(cancel context.CancelFunc) { + + ppid := os.Getppid() + pp, err := os.FindProcess(ppid) + if err != nil { + log.Fatalf("Cannot find parent process with PID %d: %s", ppid, err) + } + + ps, err := pp.Wait() + if err != nil { + log.Fatalf("Error waiting parent process to exit: %s", err) + } + + log.Println("Parent process exited with code", ps.ExitCode()) + + cancel() +} + +func startServerService(serverPort int, attached bool) { + log.Debugln("Starting Flet Server") // run server execPath, _ := os.Executable() var cmd *exec.Cmd + args := []string{"server", "--port", strconv.Itoa(serverPort)} if attached { - cmd = exec.Command(execPath, "server") + cmd = exec.Command(execPath, args...) } else { - cmd = utils.GetDetachedCmd(execPath, "server") + cmd = utils.GetDetachedCmd(execPath, args...) } cmd.Env = os.Environ() cmd.Env = append(cmd.Env, fmt.Sprintf("%s=true", config.LogToFileFlag)) @@ -84,9 +103,20 @@ func startServerService(attached bool) { log.Fatalln(err) } - log.Traceln("Server process started with PID:", cmd.Process.Pid) + log.Debugln("Server process started with PID:", cmd.Process.Pid) + fmt.Println(serverPort) } -func getLockFilename(serverPort int) string { - return filepath.Join(os.TempDir(), fmt.Sprintf("flet-%d.lock", serverPort)) +func getFreePort() (int, error) { + addr, err := net.ResolveTCPAddr("tcp", "localhost:0") + if err != nil { + return 0, err + } + + l, err := net.ListenTCP("tcp", addr) + if err != nil { + return 0, err + } + defer l.Close() + return l.Addr().(*net.TCPAddr).Port, nil } diff --git a/server/go.mod b/server/go.mod index d41e27c0b3..1ce3bc80ee 100644 --- a/server/go.mod +++ b/server/go.mod @@ -5,7 +5,6 @@ go 1.16 require ( cloud.google.com/go/iam v0.3.0 // indirect cloud.google.com/go/secretmanager v1.3.0 - github.com/alexflint/go-filemutex v1.2.0 github.com/gin-gonic/contrib v0.0.0-20201101042839-6a891bf89f19 github.com/gin-gonic/gin v1.7.7 github.com/go-playground/validator/v10 v10.10.1 // indirect diff --git a/server/go.sum b/server/go.sum index 08b2c135c8..962c53e4bf 100644 --- a/server/go.sum +++ b/server/go.sum @@ -68,8 +68,6 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alexflint/go-filemutex v1.2.0 h1:1v0TJPDtlhgpW4nJ+GvxCLSlUDC3+gW0CQQvlmfDR/s= -github.com/alexflint/go-filemutex v1.2.0/go.mod h1:mYyQSWvw9Tx2/H2n9qXPb52tTYfE0pZAWcBq5mK025c= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= diff --git a/server/model/page_name.go b/server/model/page_name.go index 74cab7a04e..d3576d81f8 100644 --- a/server/model/page_name.go +++ b/server/model/page_name.go @@ -12,6 +12,7 @@ import ( const ( publicAccount = "p" + indexPage = "index" maxSlugSize = 60 ) @@ -22,6 +23,13 @@ type PageName struct { func ParsePageName(pageName string) (*PageName, error) { + if strings.TrimSpace(pageName) == "" { + return &PageName{ + Account: publicAccount, + Name: indexPage, + }, nil + } + p := &PageName{} p.Name = strings.ToLower(strings.Trim(strings.ReplaceAll(pageName, "\\", "/"), "/")) diff --git a/server/page/client.go b/server/page/client.go index 88fbf68e65..3a32984126 100644 --- a/server/page/client.go +++ b/server/page/client.go @@ -202,8 +202,14 @@ func (c *Client) registerWebClientCore(request *RegisterWebClientRequestPayload) sessionCreated = false + pageName, err := model.ParsePageName(request.PageName) + if err != nil { + response.Error = err.Error() + return + } + // get page - page := store.GetPageByName(request.PageName) + page := store.GetPageByName(pageName.String()) if page == nil { response.Error = pageNotFoundMessage return