diff --git a/.agents/skills/flet-validation/SKILL.md b/.agents/skills/flet-validation/SKILL.md index b86eb07456..2a2854f587 100644 --- a/.agents/skills/flet-validation/SKILL.md +++ b/.agents/skills/flet-validation/SKILL.md @@ -1,6 +1,6 @@ --- name: flet-validation -description: Use when adding or changing validation for Python controls (dataclasses) in sdk/python/packages/, including Annotated/V rules, __validation_rules__, and property Raises docstrings. +description: Use whenever editing validation for Python controls in sdk/python/packages/: adding/changing constrained properties, `Raises: ValueError` docstrings, `before_update()` checks, `raise ValueError`, Annotated/V rules, or __validation_rules__. --- ## When To Use diff --git a/CHANGELOG.md b/CHANGELOG.md index ef711fb468..cd9486c338 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ * Fix `Lottie` failing to load local asset files on Windows desktop (and unreliably on other desktop platforms), so animations referenced by `src="file.json"` from the app's `assets/` directory now display correctly ([#6386](https://github.com/flet-dev/flet/issues/6386), [#6426](https://github.com/flet-dev/flet/pull/6426)) by @ndonkoHenri. * Fix `flet pack` desktop packaging so Windows and Linux bundles include the expected client archive, and Windows taskbar pins point to the packed app instead of the cached `flet.exe` ([#5151](https://github.com/flet-dev/flet/issues/5151), [#6403](https://github.com/flet-dev/flet/pull/6403)) by @ndonkoHenri. * Fix environment variable priority in `flet build` template: inherit from `Platform.environment` and use `putIfAbsent` for FLET_* variables so pre-set system env vars are not overwritten ([#6394](https://github.com/flet-dev/flet/pull/6394)) by @Bahtya. +* Fix 3- and 4-digit hex color shorthand (e.g. `#c00`, `#fc00`) rendering as invisible by expanding them to their full 6/8-digit forms ([#6419](https://github.com/flet-dev/flet/issues/6419), [#6421](https://github.com/flet-dev/flet/pull/6421)) by @ndonkoHenri. ### Other changes diff --git a/packages/flet/lib/src/utils/colors.dart b/packages/flet/lib/src/utils/colors.dart index 72d4273f7f..60376af2bb 100644 --- a/packages/flet/lib/src/utils/colors.dart +++ b/packages/flet/lib/src/utils/colors.dart @@ -253,12 +253,20 @@ extension HexColor on Color { return null; } - /// String is in the format "aabbcc" or "ffaabbcc" with an optional leading "#". - static Color _fromHex(String hexString) { + /// String is in CSS hex format: "rgb", "argb", "rrggbb" or "aarrggbb" + /// (the leading "#" or "0x" is stripped by the caller). + /// Shorthand 3/4-digit forms are expanded by doubling each character + /// (e.g. "c00" -> "cc0000", "fc00" -> "ffcc0000"). + static Color? _fromHex(String hexString) { + if (hexString.length == 3 || hexString.length == 4) { + hexString = hexString.split('').map((c) => '$c$c').join(''); + } + if (hexString.length != 6 && hexString.length != 8) return null; final buffer = StringBuffer(); if (hexString.length == 6) buffer.write('ff'); buffer.write(hexString); - return Color(int.parse(buffer.toString(), radix: 16)); + final value = int.tryParse(buffer.toString(), radix: 16); + return value == null ? null : Color(value); } /// Prefixes a hash sign if [leadingHashSign] is set to `true` (default is `true`). diff --git a/sdk/python/examples/controls/map/overlay_images/main.py b/sdk/python/examples/controls/map/overlay_images/main.py new file mode 100644 index 0000000000..7118545981 --- /dev/null +++ b/sdk/python/examples/controls/map/overlay_images/main.py @@ -0,0 +1,51 @@ +import flet as ft +import flet_map as ftm + +base64_image = "iVBORw0KGgoAAAANSUhEUgAAABkAAAAgCAYAAADnnNMGAAAACXBIWXMAAAORAAADkQFnq8zdAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAA6dJREFUSImllltoHFUYx3/fzOzm0lt23ZrQ1AQbtBehNpvQohgkBYVo410RwQctNE3Sh0IfiiBoIAjqi6TYrKnFy4O3oiiRavDJFi3mXomIBmOxNZe63ay52GR3Zj4f2sTEzmx3m//TYf7/c35zvgPnO6KqrESXqpq3muocAikv6m+/zytj3ejik1VN21G31YA9CgJ6xC+bMyQZPVCuarciPAMYC99V6Vw5pLbFSibHmlVoRVj9P3cmPBM8tSJI/M6mzabpfoAQ9fIF7WK4bd5vvuFnLGgy2vi0abg94A0AcJGvMq3hDxGRyar9r4F+iLAm0yIiRk8m37tctS1WsrIhhrI30+Srmg+J87OXUf3lWGS1q89dC6ltsSanxk4Aj2QBABii96300g87P/rtlrWr8l+vyDMfdlXSyyEikqxsiOUAQJCBhfHdXRfCq1LSsSlcWG+KBAGStvvrMkgiuv8lUc2mREukPwLUfHG+uTQv8Eown7VL3XlbBxYhf1c17hbVF3MDwA9bts280TnaU1YYqPby07aeFlUlHt27wSQ4CLo+F8AvoTCvHmyKF+ZbEb/M77P2LgvAwmrTHAHflN3KZxVbMC2jMFNOpgPnrMSOhvvFkMezXdwV4ePbtvHtxnJAMQ0j4JtVnO+eLb5oiSlt5HDbv7t1O90lpYCCCKbhfzW5kAIwUAazR0BlfII8Ow0I6uoVmI9MyAMwbMs8CExmDbk4zgu931MyO4OI4KrYflkRjOoTI+uM9d1vjotwKPu9QMk/sxzuO8POiVFcdZ1M2YBVsMEAKOqLvaPIe7mACuw0z/80SMH58SMplxlfiDhVi7dw2pltRhjKBQTQdrSja2KKTfE551NHuaZ0QVPvWYQUn31/Vm2nDvgjF4grVJx6suSvrvrSJ/6cSW2Oz9mf264uNrB806xZ1k/CZ49dUKgDEtlCROX2hfHpx8pGuuo3PpqYulw8fjndOp1yhgtNKRevJ1FyR2Ola+jXAjdnwTkZ6o896GdWdxDw7IxFg+0DpmXchTKSBWQnIuJn9u4j7dt+13UfHXEkXQOcuQ4kMhVtqsgUyPiQiPQfHw1NB2sRjmXKuTg1NwwBYLhtPtQX26eqTwGXPDOqvmcC4Hnwfrrad94GrVsOYTqUTkQY+iTlNe/6O1miSP/x0VB/+wMIDwHn/vtV1iQC4Xv95uUEWVCoL9Y5Z+gdovoyMHUFJHv88jmVy0vTuw7cZNv2YaA61Bfb7ZX5F8SaUv2xwZevAAAAAElFTkSuQmCC" # noqa: E501 + + +def main(page: ft.Page): + page.add( + ft.SafeArea( + expand=True, + content=ftm.Map( + expand=True, + initial_center=ftm.MapLatitudeLongitude(51.5, -0.09), + initial_zoom=6, + layers=[ + ftm.TileLayer( + url_template="https://tile.memomaps.de/tilegen/{z}/{x}/{y}.png" + ), + ftm.OverlayImageLayer( + overlay_images=[ + ftm.OverlayImage( + src=base64_image, + bounds=ftm.MapLatitudeLongitudeBounds( + corner_1=ftm.MapLatitudeLongitude(51.5, -0.09), + corner_2=ftm.MapLatitudeLongitude(48.8566, 2.3522), + ), + opacity=0.8, + ), + ftm.RotatedOverlayImage( + src=base64_image, + top_left_corner=ftm.MapLatitudeLongitude( + 53.377, -2.999 + ), + bottom_left_corner=ftm.MapLatitudeLongitude( + 52.503, -1.868 + ), + bottom_right_corner=ftm.MapLatitudeLongitude( + 53.475, 0.275 + ), + opacity=0.8, + ), + ] + ), + ], + ), + ) + ) + + +if __name__ == "__main__": + ft.run(main) diff --git a/sdk/python/examples/controls/map/overlay_images/pyproject.toml b/sdk/python/examples/controls/map/overlay_images/pyproject.toml new file mode 100644 index 0000000000..547ea55171 --- /dev/null +++ b/sdk/python/examples/controls/map/overlay_images/pyproject.toml @@ -0,0 +1,26 @@ +[project] +name = "map-overlay-images" +version = "1.0.0" +description = "Places rectangular and rotated image overlays on top of an interactive map." +requires-python = ">=3.10" +keywords = ["map", "overlay", "image", "rotated overlay"] +authors = [{ name = "Flet team", email = "hello@flet.dev" }] +dependencies = ["flet", "flet-map"] + +[dependency-groups] +dev = ["flet-cli", "flet-desktop", "flet-web"] + +[tool.flet.gallery] +categories = ["Extensions/Map"] + +[tool.flet.metadata] +title = "Overlay Images" +controls = ["SafeArea", "Map", "TileLayer", "OverlayImageLayer", "OverlayImage", "RotatedOverlayImage"] +layout_pattern = "single-panel" +complexity = "basic" +features = ["rectangular image overlay", "rotated image overlay"] + +[tool.flet] +org = "dev.flet" +company = "Flet" +copyright = "Copyright (C) 2023-2026 by Flet" diff --git a/sdk/python/packages/flet-cli/src/flet_cli/__pyinstaller/rthooks/pyi_rth_localhost_fletd.py b/sdk/python/packages/flet-cli/src/flet_cli/__pyinstaller/rthooks/pyi_rth_localhost_fletd.py index 381fd97660..f9087befc8 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/__pyinstaller/rthooks/pyi_rth_localhost_fletd.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/__pyinstaller/rthooks/pyi_rth_localhost_fletd.py @@ -1,7 +1,5 @@ -import hashlib import logging import os -import re import sys logger = logging.getLogger("flet") @@ -14,11 +12,5 @@ # On Windows, set AppUserModelID so the taskbar associates the Flet client window # with the parent executable (a PyInstaller bundle in this case) rather than the cached # flet.exe. This ensures taskbar pins and shortcuts point to the correct executable. -# AppUserModelID must be <=128 chars and contain no spaces, so we derive a stable -# identifier from the exe name and a hash of its absolute path (unique per install). if sys.platform == "win32" and "FLET_APP_USER_MODEL_ID" not in os.environ: - exe_path = os.path.abspath(sys.executable) - exe_stem = os.path.splitext(os.path.basename(exe_path))[0] - safe_name = re.sub(r"[^A-Za-z0-9]", "", exe_stem)[:64] or "App" - path_hash = hashlib.sha1(exe_path.encode("utf-8")).hexdigest()[:16] - os.environ["FLET_APP_USER_MODEL_ID"] = f"Flet.{safe_name}.{path_hash}" + os.environ["FLET_APP_USER_MODEL_ID"] = os.path.abspath(sys.executable) diff --git a/sdk/python/packages/flet-map/CHANGELOG.md b/sdk/python/packages/flet-map/CHANGELOG.md index 405aad7cf7..da6616d915 100644 --- a/sdk/python/packages/flet-map/CHANGELOG.md +++ b/sdk/python/packages/flet-map/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 0.85.0 + +### Added + +- Added image overlay support for maps with `OverlayImageLayer`, `OverlayImage`, and `RotatedOverlayImage` ([#6319](https://github.com/flet-dev/flet/issues/6319), [#6421](https://github.com/flet-dev/flet/pull/6421)) by @ndonkoHenri. + ## 0.81.1 ### Added diff --git a/sdk/python/packages/flet-map/src/flet_map/__init__.py b/sdk/python/packages/flet-map/src/flet_map/__init__.py index 7fc09030d0..dd4b377d00 100644 --- a/sdk/python/packages/flet-map/src/flet_map/__init__.py +++ b/sdk/python/packages/flet-map/src/flet_map/__init__.py @@ -2,6 +2,12 @@ from flet_map.map import Map from flet_map.map_layer import MapLayer from flet_map.marker_layer import Marker, MarkerLayer +from flet_map.overlay_image_layer import ( + BaseOverlayImage, + OverlayImage, + OverlayImageLayer, + RotatedOverlayImage, +) from flet_map.polygon_layer import PolygonLayer, PolygonMarker from flet_map.polyline_layer import PolylineLayer, PolylineMarker from flet_map.rich_attribution import RichAttribution @@ -45,6 +51,7 @@ __all__ = [ "AttributionAlignment", + "BaseOverlayImage", "Camera", "CameraFit", "CircleLayer", @@ -73,12 +80,15 @@ "Marker", "MarkerLayer", "MultiFingerGesture", + "OverlayImage", + "OverlayImageLayer", "PatternFit", "PolygonLayer", "PolygonMarker", "PolylineLayer", "PolylineMarker", "RichAttribution", + "RotatedOverlayImage", "SimpleAttribution", "SolidStrokePattern", "SourceAttribution", diff --git a/sdk/python/packages/flet-map/src/flet_map/circle_layer.py b/sdk/python/packages/flet-map/src/flet_map/circle_layer.py index 4ec7bd9b10..1f2f4d4f86 100644 --- a/sdk/python/packages/flet-map/src/flet_map/circle_layer.py +++ b/sdk/python/packages/flet-map/src/flet_map/circle_layer.py @@ -8,7 +8,7 @@ @ft.control("CircleMarker") -class CircleMarker(ft.Control): +class CircleMarker(ft.BaseControl): """ A circular marker displayed on the Map at the specified location through the :class:`~flet_map.CircleLayer`. @@ -45,6 +45,11 @@ class CircleMarker(ft.Control): Whether the :attr:`radius` should use the unit meters. """ + visible: bool = True + """ + Whether this marker is rendered on the map. + """ + def before_update(self): super().before_update() if self.border_stroke_width < 0: diff --git a/sdk/python/packages/flet-map/src/flet_map/map.py b/sdk/python/packages/flet-map/src/flet_map/map.py index 7a420d2c22..4ec95b00c4 100644 --- a/sdk/python/packages/flet-map/src/flet_map/map.py +++ b/sdk/python/packages/flet-map/src/flet_map/map.py @@ -45,7 +45,8 @@ class Map(ft.LayoutControl): initial_zoom: ft.Number = 13.0 """ The zoom when the map is first loaded. - If initial_camera_fit is defined this has no effect. + + If :attr:`initial_camera_fit` is non-`None` this has no effect. """ interaction_configuration: InteractionConfiguration = field( @@ -379,7 +380,7 @@ async def get_camera(self) -> Camera: Gets the current camera snapshot of the map. Returns: - Current :class:`~flet_map.Camera` state. + The current camera state. """ camera = await self._invoke_method("get_camera") return from_dict(Camera, camera) diff --git a/sdk/python/packages/flet-map/src/flet_map/map_layer.py b/sdk/python/packages/flet-map/src/flet_map/map_layer.py index 2e954c0e03..aea198aa5c 100644 --- a/sdk/python/packages/flet-map/src/flet_map/map_layer.py +++ b/sdk/python/packages/flet-map/src/flet_map/map_layer.py @@ -12,6 +12,7 @@ class MapLayer(ft.Control): - :class:`~flet_map.CircleLayer` - :class:`~flet_map.MarkerLayer` + - :class:`~flet_map.OverlayImageLayer` - :class:`~flet_map.PolygonLayer` - :class:`~flet_map.PolylineLayer` - :class:`~flet_map.RichAttribution` diff --git a/sdk/python/packages/flet-map/src/flet_map/marker_layer.py b/sdk/python/packages/flet-map/src/flet_map/marker_layer.py index f45031b225..4a10bc7bc3 100644 --- a/sdk/python/packages/flet-map/src/flet_map/marker_layer.py +++ b/sdk/python/packages/flet-map/src/flet_map/marker_layer.py @@ -9,7 +9,7 @@ @ft.control("Marker") -class Marker(ft.Control): +class Marker(ft.BaseControl): """ A marker displayed on the Map at the specified location through the :class:`~flet_map.MarkerLayer`. @@ -68,6 +68,11 @@ class Marker(ft.Control): Defaults to the value of the parent :attr:`flet_map.MarkerLayer.alignment`. """ + visible: bool = True + """ + Whether this marker is rendered on the map. + """ + def before_update(self): super().before_update() if not self.content.visible: diff --git a/sdk/python/packages/flet-map/src/flet_map/overlay_image_layer.py b/sdk/python/packages/flet-map/src/flet_map/overlay_image_layer.py new file mode 100644 index 0000000000..a06c854afd --- /dev/null +++ b/sdk/python/packages/flet-map/src/flet_map/overlay_image_layer.py @@ -0,0 +1,116 @@ +from typing import Annotated, Union + +import flet as ft +from flet.utils.validation import V +from flet_map.map_layer import MapLayer +from flet_map.types import MapLatitudeLongitude, MapLatitudeLongitudeBounds + +__all__ = [ + "BaseOverlayImage", + "OverlayImage", + "OverlayImageLayer", + "RotatedOverlayImage", +] + + +@ft.control("BaseOverlayImage", kw_only=True) +class BaseOverlayImage(ft.BaseControl): + """ + Abstract class for image overlays displayed through + :class:`~flet_map.OverlayImageLayer`. + + The following overlay image types are available: + + - :class:`~flet_map.OverlayImage` + - :class:`~flet_map.RotatedOverlayImage` + """ + + src: Union[str, bytes] + """ + The image source. + + It can be one of the following: + - A URL or local [asset file](https://flet.dev/docs/cookbook/assets) path; + - A base64 string; + - Raw bytes. + """ + + opacity: Annotated[ + ft.Number, + V.between(0.0, 1.0), + ] = 1.0 + """ + The opacity in which the image should get rendered on the map. + + Raises: + ValueError: If it is not between `0.0` and `1.0`, inclusive. + """ + + gapless_playback: bool = False + """ + Whether to continue showing the old image (`True`), or briefly show nothing + (`False`), when the image provider changes. + """ + + filter_quality: ft.FilterQuality = ft.FilterQuality.MEDIUM + """ + The rendering quality of the image. + """ + + visible: bool = True + """ + Whether this overlay image is rendered on the map. + """ + + +@ft.control("OverlayImage", kw_only=True) +class OverlayImage(BaseOverlayImage): + """ + An unrotated image overlay that spans between a given bounding box. + """ + + bounds: MapLatitudeLongitudeBounds + """ + The latitude and longitude bounds where this image will be displayed. + """ + + +@ft.control("RotatedOverlayImage", kw_only=True) +class RotatedOverlayImage(BaseOverlayImage): + """ + An image overlay transformed across three corner points. + + The top-right corner is derived from :attr:`top_left_corner`, + :attr:`bottom_left_corner`, and :attr:`bottom_right_corner`. + """ + + top_left_corner: MapLatitudeLongitude + """ + The coordinates of the top-left corner of the image. + """ + + bottom_left_corner: MapLatitudeLongitude + """ + The coordinates of the bottom-left corner of the image. + """ + + bottom_right_corner: MapLatitudeLongitude + """ + The coordinates of the bottom-right corner of the image. + """ + + +@ft.control("OverlayImageLayer", kw_only=True) +class OverlayImageLayer(MapLayer): + """ + A layer to display image overlays. + + Tip: + Place this layer after every non-translucent layer that should appear + below it. Layers rendered after this one may cover its overlay images. + """ + + overlay_images: list[BaseOverlayImage] + """ + A list of image overlays to display. + """ diff --git a/sdk/python/packages/flet-map/src/flet_map/polygon_layer.py b/sdk/python/packages/flet-map/src/flet_map/polygon_layer.py index 2caf97c1d2..f0d371b36c 100644 --- a/sdk/python/packages/flet-map/src/flet_map/polygon_layer.py +++ b/sdk/python/packages/flet-map/src/flet_map/polygon_layer.py @@ -8,7 +8,7 @@ @ft.control("PolygonMarker") -class PolygonMarker(ft.Control): +class PolygonMarker(ft.BaseControl): """ A marker for the :class:`~flet_map.PolygonLayer`. """ @@ -74,6 +74,11 @@ class PolygonMarker(ft.Control): Style to use for line segment joins. """ + visible: bool = True + """ + Whether this marker is rendered on the map. + """ + def before_update(self): super().before_update() if self.border_stroke_width < 0: diff --git a/sdk/python/packages/flet-map/src/flet_map/polyline_layer.py b/sdk/python/packages/flet-map/src/flet_map/polyline_layer.py index 5d552e79ad..eb469317e5 100644 --- a/sdk/python/packages/flet-map/src/flet_map/polyline_layer.py +++ b/sdk/python/packages/flet-map/src/flet_map/polyline_layer.py @@ -9,7 +9,7 @@ @ft.control("PolylineMarker") -class PolylineMarker(ft.Control): +class PolylineMarker(ft.BaseControl): """ A marker for the :class:`~flet_map.PolylineLayer`. """ @@ -76,6 +76,11 @@ class PolylineMarker(ft.Control): Style to use for line segment joins. """ + visible: bool = True + """ + Whether this marker is rendered on the map. + """ + def before_update(self): super().before_update() if self.border_stroke_width < 0: diff --git a/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/circle_layer.dart b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/circle_layer.dart index be2fbfd009..86c4c8e778 100644 --- a/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/circle_layer.dart +++ b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/circle_layer.dart @@ -20,7 +20,7 @@ class CircleLayerControl extends StatelessWidget with FletStoreMixin { .map((circle) { circle.notifyParent = true; return CircleMarker( - point: parseLatLng(circle.get("coordinates"))!, + point: circle.getLatLng("coordinates")!, color: circle.getColor("color", context, const Color(0xFF00FF00))!, borderColor: circle.getColor( "border_color", context, const Color(0xFFFFFF00))!, @@ -29,6 +29,6 @@ class CircleLayerControl extends StatelessWidget with FletStoreMixin { radius: circle.getDouble("radius", 10)!); }).toList(); - return CircleLayer(circles: circles); + return BaseControl(control: control, child: CircleLayer(circles: circles)); } } diff --git a/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/extension.dart b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/extension.dart index 02e25d3902..d97d9f55b6 100644 --- a/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/extension.dart +++ b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/extension.dart @@ -4,6 +4,7 @@ import 'package:flutter/cupertino.dart'; import 'circle_layer.dart'; import 'map.dart'; import 'marker_layer.dart'; +import 'overlay_image_layer.dart'; import 'polygon_layer.dart'; import 'polyline_layer.dart'; import 'rich_attribution.dart'; @@ -24,6 +25,8 @@ class Extension extends FletExtension { return TileLayerControl(key: key, control: control); case "MarkerLayer": return MarkerLayerControl(key: key, control: control); + case "OverlayImageLayer": + return OverlayImageLayerControl(key: key, control: control); case "CircleLayer": return CircleLayerControl(key: key, control: control); case "PolygonLayer": diff --git a/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/marker_layer.dart b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/marker_layer.dart index cc8c19dafb..53563af9c9 100644 --- a/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/marker_layer.dart +++ b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/marker_layer.dart @@ -19,7 +19,7 @@ class MarkerLayerControl extends StatelessWidget with FletStoreMixin { .map((marker) { marker.notifyParent = true; return AnimatedMarker( - point: parseLatLng(marker.get("coordinates"))!, + point: marker.getLatLng("coordinates")!, rotate: marker.getBool("rotate"), height: marker.getDouble("height", 30.0)!, width: marker.getDouble("width", 30.0)!, @@ -30,10 +30,13 @@ class MarkerLayerControl extends StatelessWidget with FletStoreMixin { }); }).toList(); - return AnimatedMarkerLayer( - markers: markers, - rotate: control.getBool("rotate", false)!, - alignment: control.getAlignment("alignment", Alignment.center)!, + return BaseControl( + control: control, + child: AnimatedMarkerLayer( + markers: markers, + rotate: control.getBool("rotate", false)!, + alignment: control.getAlignment("alignment", Alignment.center)!, + ), ); } } diff --git a/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/overlay_image_layer.dart b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/overlay_image_layer.dart new file mode 100644 index 0000000000..305766ca3f --- /dev/null +++ b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/overlay_image_layer.dart @@ -0,0 +1,73 @@ +import 'package:flet/flet.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; + +import 'utils/map.dart'; + +class OverlayImageLayerControl extends StatelessWidget { + final Control control; + + const OverlayImageLayerControl({super.key, required this.control}); + + @override + Widget build(BuildContext context) { + debugPrint("OverlayImageLayerControl build: ${control.id}"); + + final overlayImages = control + .children("overlay_images") + .map((overlayImage) { + overlayImage.notifyParent = true; + + final imageProvider = overlayImage.getImageProvider("src", context); + if (imageProvider == null) return null; + + final opacity = overlayImage.getDouble("opacity", 1.0)!; + final gaplessPlayback = + overlayImage.getBool("gapless_playback", false)!; + final filterQuality = overlayImage.getFilterQuality( + "filter_quality", FilterQuality.medium)!; + + switch (overlayImage.type) { + case "OverlayImage": + final bounds = overlayImage.getLatLngBounds("bounds"); + if (bounds == null) return null; + return OverlayImage( + imageProvider: imageProvider, + bounds: bounds, + opacity: opacity, + gaplessPlayback: gaplessPlayback, + filterQuality: filterQuality, + ); + case "RotatedOverlayImage": + final topLeftCorner = overlayImage.getLatLng("top_left_corner"); + final bottomLeftCorner = + overlayImage.getLatLng("bottom_left_corner"); + final bottomRightCorner = + overlayImage.getLatLng("bottom_right_corner"); + if (topLeftCorner == null || + bottomLeftCorner == null || + bottomRightCorner == null) { + return null; + } + return RotatedOverlayImage( + imageProvider: imageProvider, + topLeftCorner: topLeftCorner, + bottomLeftCorner: bottomLeftCorner, + bottomRightCorner: bottomRightCorner, + opacity: opacity, + gaplessPlayback: gaplessPlayback, + filterQuality: filterQuality, + ); + default: + return null; + } + }) + .nonNulls + .toList(); + + return BaseControl( + control: control, + child: OverlayImageLayer(overlayImages: overlayImages), + ); + } +} diff --git a/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/polygon_layer.dart b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/polygon_layer.dart index cb8eb9c069..af588fcdca 100644 --- a/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/polygon_layer.dart +++ b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/polygon_layer.dart @@ -29,21 +29,20 @@ class PolygonLayerControl extends StatelessWidget with FletStoreMixin { "label_text_style", Theme.of(context), const TextStyle())!, strokeCap: polygon.getStrokeCap("stroke_cap", StrokeCap.round)!, strokeJoin: polygon.getStrokeJoin("stroke_join", StrokeJoin.round)!, - points: polygon - .get("coordinates", [])! - .map((c) => parseLatLng(c)) - .nonNulls - .toList()); + points: polygon.getLatLngList("coordinates")); }).toList(); - return PolygonLayer( - polygons: polygons, - polygonCulling: control.getBool("polygon_culling", true)!, - polygonLabels: control.getBool("polygon_labels", true)!, - drawLabelsLast: control.getBool("draw_labels_last", false)!, - simplificationTolerance: - control.getDouble("simplification_tolerance", 0.3)!, - useAltRendering: control.getBool("use_alternative_rendering", false)!, + return BaseControl( + control: control, + child: PolygonLayer( + polygons: polygons, + polygonCulling: control.getBool("polygon_culling", true)!, + polygonLabels: control.getBool("polygon_labels", true)!, + drawLabelsLast: control.getBool("draw_labels_last", false)!, + simplificationTolerance: + control.getDouble("simplification_tolerance", 0.3)!, + useAltRendering: control.getBool("use_alternative_rendering", false)!, + ), ); } } diff --git a/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/polyline_layer.dart b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/polyline_layer.dart index c604c31a12..e4c8bfd32e 100644 --- a/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/polyline_layer.dart +++ b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/polyline_layer.dart @@ -23,8 +23,8 @@ class PolylineLayerControl extends StatelessWidget with FletStoreMixin { borderColor: polyline.getColor("border_color", context, Colors.yellow)!, color: polyline.getColor("color", context, Colors.yellow)!, - pattern: parseStrokePattern( - polyline.get("stroke_pattern"), const StrokePattern.solid())!, + pattern: polyline.getStrokePattern( + "stroke_pattern", const StrokePattern.solid())!, strokeCap: polyline.getStrokeCap("stroke_cap", StrokeCap.round)!, strokeJoin: polyline.getStrokeJoin("stroke_join", StrokeJoin.round)!, strokeWidth: polyline.getDouble("stroke_width", 1.0)!, @@ -40,19 +40,18 @@ class PolylineLayerControl extends StatelessWidget with FletStoreMixin { .map((e) => parseColor(e, Theme.of(context))) .nonNulls .toList(), - points: polyline - .get("coordinates", [])! - .map((c) => parseLatLng(c)) - .nonNulls - .toList()); + points: polyline.getLatLngList("coordinates")); }).toList(); - return PolylineLayer( - polylines: polylines, - cullingMargin: control.getDouble("culling_margin", 10.0)!, - minimumHitbox: control.getDouble("min_hittable_radius", 10.0)!, - simplificationTolerance: - control.getDouble("simplification_tolerance", 0.3)!, + return BaseControl( + control: control, + child: PolylineLayer( + polylines: polylines, + cullingMargin: control.getDouble("culling_margin", 10.0)!, + minimumHitbox: control.getDouble("min_hittable_radius", 10.0)!, + simplificationTolerance: + control.getDouble("simplification_tolerance", 0.3)!, + ), ); } } diff --git a/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/rich_attribution.dart b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/rich_attribution.dart index 18bd4be9e8..344a56d58a 100644 --- a/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/rich_attribution.dart +++ b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/rich_attribution.dart @@ -44,19 +44,21 @@ class _RichAttributionControlState extends State .nonNulls .toList(); - return RichAttributionWidget( - attributions: attributions, - permanentHeight: widget.control.getDouble("permanent_height", 24.0)!, - popupBackgroundColor: widget.control.getColor( - "popup_bgcolor", context, Theme.of(context).colorScheme.surface), - showFlutterMapAttribution: - widget.control.getBool("show_flutter_map_attribution", true)!, - alignment: parseAttributionAlignment( - widget.control.getString("alignment"), - AttributionAlignment.bottomRight)!, - popupBorderRadius: - widget.control.getBorderRadius("popup_border_radius"), - popupInitialDisplayDuration: widget.control - .getDuration("popup_initial_display_duration", Duration.zero)!); + return BaseControl( + control: widget.control, + child: RichAttributionWidget( + attributions: attributions, + permanentHeight: widget.control.getDouble("permanent_height", 24.0)!, + popupBackgroundColor: widget.control.getColor( + "popup_bgcolor", context, Theme.of(context).colorScheme.surface), + showFlutterMapAttribution: + widget.control.getBool("show_flutter_map_attribution", true)!, + alignment: widget.control.getAttributionAlignment( + "alignment", AttributionAlignment.bottomRight)!, + popupBorderRadius: + widget.control.getBorderRadius("popup_border_radius"), + popupInitialDisplayDuration: widget.control + .getDuration("popup_initial_display_duration", Duration.zero)!), + ); } } diff --git a/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/simple_attribution.dart b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/simple_attribution.dart index 3153cbeedd..844601ded1 100644 --- a/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/simple_attribution.dart +++ b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/simple_attribution.dart @@ -12,12 +12,15 @@ class SimpleAttributionControl extends StatelessWidget { debugPrint("SimpleAttributionControl build: ${control.id}"); var text = control.buildTextOrWidget("text"); - return SimpleAttributionWidget( - source: text is Text ? text : const Text("Placeholder Text"), - onTap: () => control.triggerEvent("click"), - backgroundColor: control.getColor( - "bgcolor", context, Theme.of(context).colorScheme.surface)!, - alignment: control.getAlignment("alignment", Alignment.bottomRight)!, + return BaseControl( + control: control, + child: SimpleAttributionWidget( + source: text is Text ? text : const Text("Placeholder Text"), + onTap: () => control.triggerEvent("click"), + backgroundColor: control.getColor( + "bgcolor", context, Theme.of(context).colorScheme.surface)!, + alignment: control.getAlignment("alignment", Alignment.bottomRight)!, + ), ); } } diff --git a/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/tile_layer.dart b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/tile_layer.dart index f9552b5eee..dac12189a3 100644 --- a/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/tile_layer.dart +++ b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/tile_layer.dart @@ -34,8 +34,8 @@ class TileLayerControl extends StatelessWidget { control.get("subdomains")?.map((e) => e.toString()).toList() ?? const ['a', 'b', 'c'], tileProvider: NetworkTileProvider(), - tileDisplay: parseTileDisplay( - control.get("display_mode"), const TileDisplay.fadeIn())!, + tileDisplay: + control.getTileDisplay("display_mode", const TileDisplay.fadeIn())!, tileDimension: control.getInt("tile_size", 256)!, userAgentPackageName: control.getString("user_agent_package_name", 'unknown')!, @@ -46,13 +46,12 @@ class TileLayerControl extends StatelessWidget { keepBuffer: control.getInt("keep_buffer", 2)!, panBuffer: control.getInt("pan_buffer", 1)!, tms: control.getBool("enable_tms", false)!, - tileBounds: parseLatLngBounds(control.get("tile_bounds")), + tileBounds: control.getLatLngBounds("tile_bounds"), retinaMode: control.getBool("enable_retina_mode"), maxZoom: control.getDouble("max_zoom", double.infinity)!, minZoom: control.getDouble("min_zoom", 0)!, - evictErrorTileStrategy: parseEvictErrorTileStrategy( - control.getString("evict_error_tile_strategy"), - EvictErrorTileStrategy.none)!, + evictErrorTileStrategy: control.getEvictErrorTileStrategy( + "evict_error_tile_strategy", EvictErrorTileStrategy.none)!, errorImage: errorImage, errorTileCallback: (TileImage t, Object o, StackTrace? s) { control.triggerEvent("image_error", o.toString()); @@ -61,7 +60,7 @@ class TileLayerControl extends StatelessWidget { .get("additional_options") ?.map((k, v) => MapEntry(k.toString(), v.toString())) ?? const {}, - wmsOptions: parseWMSTileLayerOptions(control.get("wms_configuration")), + wmsOptions: control.getWMSTileLayerOptions("wms_configuration"), ); return BaseControl(control: control, child: tileLayer); diff --git a/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/utils/attribution_alignment.dart b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/utils/attribution_alignment.dart index 2bb3f29490..ac8b3c0bba 100644 --- a/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/utils/attribution_alignment.dart +++ b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/utils/attribution_alignment.dart @@ -5,3 +5,10 @@ AttributionAlignment? parseAttributionAlignment(String? value, [AttributionAlignment? defaultValue]) { return parseEnum(AttributionAlignment.values, value, defaultValue); } + +extension AttributionAlignmentControlExtension on Control { + AttributionAlignment? getAttributionAlignment(String propertyName, + [AttributionAlignment? defaultValue]) { + return parseAttributionAlignment(getString(propertyName), defaultValue); + } +} diff --git a/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/utils/map.dart b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/utils/map.dart index 34d0666811..7caaf54814 100644 --- a/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/utils/map.dart +++ b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/utils/map.dart @@ -153,10 +153,12 @@ KeyboardOptions? parseKeyboardOptions(dynamic value, parseDouble(value["rotate_leap_velocity_multiplier"], 3)!, zoomLeapVelocityMultiplier: parseDouble(value["zoom_leap_velocity_multiplier"], 3)!, - performLeapTriggerDuration: - parseDuration(value["perform_leap_trigger_duration"], const Duration(milliseconds: 100))!, - animationCurveReverseDuration: - parseDuration(value["animation_curve_reverse_duration"], const Duration(milliseconds: 600))!); + performLeapTriggerDuration: parseDuration( + value["perform_leap_trigger_duration"], + const Duration(milliseconds: 100))!, + animationCurveReverseDuration: parseDuration( + value["animation_curve_reverse_duration"], + const Duration(milliseconds: 600))!); } CursorRotationBehaviour? parseCursorRotationBehaviour(String? value, @@ -257,16 +259,16 @@ MapOptions? parseConfiguration(Control control, BuildContext context, [MapOptions? defaultValue]) { return MapOptions( initialCenter: - parseLatLng(control.get("initial_center"), const LatLng(50.5, 30.51))!, - interactionOptions: parseInteractionOptions( - control.get("interaction_configuration"), const InteractionOptions())!, + control.getLatLng("initial_center", const LatLng(50.5, 30.51))!, + interactionOptions: control.getInteractionOptions( + "interaction_configuration", const InteractionOptions())!, backgroundColor: control.getColor("bgcolor", context, Colors.grey[300])!, initialRotation: control.getDouble("initial_rotation", 0.0)!, initialZoom: control.getDouble("initial_zoom", 13.0)!, keepAlive: control.getBool("keep_alive", false)!, maxZoom: control.getDouble("max_zoom"), minZoom: control.getDouble("min_zoom"), - initialCameraFit: parseCameraFit(control.get("initial_camera_fit")), + initialCameraFit: control.getCameraFit("initial_camera_fit"), onPointerHover: control.hasEventHandler("hover") ? (PointerHoverEvent e, LatLng latlng) { control.triggerEvent("hover", { @@ -393,3 +395,63 @@ extension MapEventExtension on MapEvent { }; } } + +extension MapParsersControlExtension on Control { + LatLng? getLatLng(String propertyName, [LatLng? defaultValue]) { + return parseLatLng(get(propertyName), defaultValue); + } + + LatLngBounds? getLatLngBounds(String propertyName, + [LatLngBounds? defaultValue]) { + return parseLatLngBounds(get(propertyName), defaultValue); + } + + List getLatLngList(String propertyName, + [List defaultValue = const []]) { + return get(propertyName) + ?.map((c) => parseLatLng(c)) + .nonNulls + .toList() ?? + defaultValue; + } + + StrokePattern? getStrokePattern(String propertyName, + [StrokePattern? defaultValue]) { + return parseStrokePattern(get(propertyName), defaultValue); + } + + TileDisplay? getTileDisplay(String propertyName, + [TileDisplay? defaultValue]) { + return parseTileDisplay(get(propertyName), defaultValue); + } + + InteractionOptions? getInteractionOptions(String propertyName, + [InteractionOptions? defaultValue]) { + return parseInteractionOptions(get(propertyName), defaultValue); + } + + CameraFit? getCameraFit(String propertyName, [CameraFit? defaultValue]) { + return parseCameraFit(get(propertyName), defaultValue); + } + + KeyboardOptions? getKeyboardOptions(String propertyName, + [KeyboardOptions? defaultValue]) { + return parseKeyboardOptions(get(propertyName), defaultValue); + } + + CursorKeyboardRotationOptions? getCursorKeyboardRotationOptions( + String propertyName, + [CursorKeyboardRotationOptions? defaultValue]) { + return parseCursorKeyboardRotationOptions(get(propertyName), defaultValue); + } + + EvictErrorTileStrategy? getEvictErrorTileStrategy(String propertyName, + [EvictErrorTileStrategy? defaultValue]) { + return parseEvictErrorTileStrategy(getString(propertyName), defaultValue); + } + + WMSTileLayerOptions? getWMSTileLayerOptions(String propertyName, + [WMSTileLayerOptions? defaultValue]) { + return parseWMSTileLayerOptions(get(propertyName), defaultValue); + } +} diff --git a/website/docs/controls/map/baseoverlayimage.md b/website/docs/controls/map/baseoverlayimage.md new file mode 100644 index 0000000000..59c11bf3ad --- /dev/null +++ b/website/docs/controls/map/baseoverlayimage.md @@ -0,0 +1,7 @@ +--- +title: "BaseOverlayImage" +--- + +import {ClassAll} from '@site/src/components/crocodocs'; + + diff --git a/website/docs/controls/map/index.md b/website/docs/controls/map/index.md index 5d75fefea6..c73172b6ec 100644 --- a/website/docs/controls/map/index.md +++ b/website/docs/controls/map/index.md @@ -65,11 +65,15 @@ More details [here](tilelayer.md). +### Overlay Images + + + ## Reference - [`Map`](mapcontrol.md) -- Layers: [`TileLayer`](tilelayer.md), [`MarkerLayer`](markerlayer.md), [`CircleLayer`](circlelayer.md), [`PolygonLayer`](polygonlayer.md), [`PolylineLayer`](polylinelayer.md) -- Markers and overlays: [`Marker`](marker.md), [`CircleMarker`](circlemarker.md), [`PolygonMarker`](polygonmarker.md), [`PolylineMarker`](polylinemarker.md) -- Attributions: [`SimpleAttribution`](simpleattribution.md), [`RichAttribution`](richattribution.md), [`SourceAttribution`](sourceattribution.md) + - Layers: [`TileLayer`](tilelayer.md), [`MarkerLayer`](markerlayer.md), [`OverlayImageLayer`](overlayimagelayer.md), [`CircleLayer`](circlelayer.md), [`PolygonLayer`](polygonlayer.md), [`PolylineLayer`](polylinelayer.md) + - Markers and overlays: [`Marker`](marker.md), [`CircleMarker`](circlemarker.md), [`PolygonMarker`](polygonmarker.md), [`PolylineMarker`](polylinemarker.md), [`OverlayImage`](overlayimage.md), [`RotatedOverlayImage`](rotatedoverlayimage.md) + - Attributions: [`SimpleAttribution`](simpleattribution.md), [`RichAttribution`](richattribution.md), [`SourceAttribution`](sourceattribution.md) See the [types](types/attributionalignment.md) section for additional configuration helpers. diff --git a/website/docs/controls/map/overlayimage.md b/website/docs/controls/map/overlayimage.md new file mode 100644 index 0000000000..0060d0bdf5 --- /dev/null +++ b/website/docs/controls/map/overlayimage.md @@ -0,0 +1,14 @@ +--- +examples: "controls/map" +title: "OverlayImage" +--- + +import {ClassAll, CodeExample} from '@site/src/components/crocodocs'; + + + +## Examples + +### Basic example + + diff --git a/website/docs/controls/map/overlayimagelayer.md b/website/docs/controls/map/overlayimagelayer.md new file mode 100644 index 0000000000..227a24ae76 --- /dev/null +++ b/website/docs/controls/map/overlayimagelayer.md @@ -0,0 +1,14 @@ +--- +examples: "controls/map" +title: "OverlayImageLayer" +--- + +import {ClassAll, CodeExample} from '@site/src/components/crocodocs'; + + + +## Examples + +### Basic example + + diff --git a/website/docs/controls/map/rotatedoverlayimage.md b/website/docs/controls/map/rotatedoverlayimage.md new file mode 100644 index 0000000000..971cb1885a --- /dev/null +++ b/website/docs/controls/map/rotatedoverlayimage.md @@ -0,0 +1,14 @@ +--- +examples: "controls/map" +title: "RotatedOverlayImage" +--- + +import {ClassAll, CodeExample} from '@site/src/components/crocodocs'; + + + +## Examples + +### Basic example + + diff --git a/website/sidebars.js b/website/sidebars.js index f0e887ab7d..67ef5c7735 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -973,6 +973,10 @@ module.exports = { "type": "doc", "id": "controls/map/markerlayer" }, + { + "type": "doc", + "id": "controls/map/overlayimagelayer" + }, { "type": "doc", "id": "controls/map/circlelayer" @@ -1010,6 +1014,25 @@ module.exports = { } ] }, + { + "type": "category", + "label": "Overlays", + "collapsed": true, + "items": [ + { + "type": "doc", + "id": "controls/map/baseoverlayimage" + }, + { + "type": "doc", + "id": "controls/map/overlayimage" + }, + { + "type": "doc", + "id": "controls/map/rotatedoverlayimage" + } + ] + }, { "type": "category", "label": "Attributions", diff --git a/website/sidebars.yml b/website/sidebars.yml index 5abf04ebd5..6577fde4e2 100644 --- a/website/sidebars.yml +++ b/website/sidebars.yml @@ -198,6 +198,7 @@ docs: - controls/map/maplayer.md - controls/map/tilelayer.md - controls/map/markerlayer.md + - controls/map/overlayimagelayer.md - controls/map/circlelayer.md - controls/map/polygonlayer.md - controls/map/polylinelayer.md @@ -206,6 +207,10 @@ docs: - controls/map/circlemarker.md - controls/map/polygonmarker.md - controls/map/polylinemarker.md + Overlays: + - controls/map/baseoverlayimage.md + - controls/map/overlayimage.md + - controls/map/rotatedoverlayimage.md Attributions: - controls/map/sourceattribution.md - controls/map/simpleattribution.md diff --git a/website/src/components/crocodocs/ClassBlock.js b/website/src/components/crocodocs/ClassBlock.js index 21686a1783..2571600eb2 100644 --- a/website/src/components/crocodocs/ClassBlock.js +++ b/website/src/components/crocodocs/ClassBlock.js @@ -290,14 +290,13 @@ function renderMethod(item, classSymbol, docId) { /** * Build the compact member data used by ClassSummary lists. - * Carries labels/deprecation metadata so summary rows can show important badges - * without requiring readers to scroll to the full member documentation. + * Carries labels so summary rows can show important badges without requiring + * readers to scroll to the full member documentation. */ function memberSummary(item, kind) { return { name: item.name, kind, - deprecation: item.deprecation, labels: item.labels ?? [], summary: firstSentenceFromDocstring(item.docstring, item.docstring_sections), }; @@ -337,12 +336,7 @@ function SummarySection({title, items, classSymbol}) { {" "} {labels.map((label) => ( - - {label} - + {label} ))}