diff --git a/examples/common_app.py b/examples/common_app.py index 452531c2e..b91fc6c0f 100644 --- a/examples/common_app.py +++ b/examples/common_app.py @@ -1,5 +1,6 @@ from kivy.utils import hex_colormap +from materialyoucolor.utils.platform_utils import SCHEMES from kivymd.uix.menu import MDDropdownMenu KV = """ @@ -55,7 +56,8 @@ def open_menu(self, menu_button): menu_items = [] for item, method in { "Set palette": lambda: self.set_palette(), - "Switch theme style": lambda: self.theme_cls.switch_theme(), + "Switch theme style": lambda: self.switch_theme(), + "Switch scheme type": lambda: self.set_scheme_type(), "Disabled widgets": lambda: self.disabled_widgets(), }.items(): menu_items.append( @@ -72,6 +74,9 @@ def open_menu(self, menu_button): def switch_palette(self, selected_palette): self.theme_cls.primary_palette = selected_palette + + def switch_theme(self): + self.theme_cls.switch_theme() def set_palette(self): instance_from_menu = self.get_instance_from_menu("Set palette") @@ -92,6 +97,25 @@ def set_palette(self): items=menu_items, ).open() + def set_scheme_type(self): + instance_from_menu = self.get_instance_from_menu("Switch scheme type") + + menu_items = [] + for scheme_name in SCHEMES.keys(): + menu_items.append( + { + "text": scheme_name, + "on_release": lambda x=scheme_name: self.update_scheme_name(x), + } + ) + MDDropdownMenu( + caller=instance_from_menu, + items=menu_items, + ).open() + + def update_scheme_name(self, scheme_name): + self.theme_cls.dynamic_scheme_name = scheme_name + def get_instance_from_menu(self, name_item): index = 0 rv = self.menu.ids.md_menu diff --git a/examples/dynamic_color_schemes.py b/examples/dynamic_color_schemes.py index eaf7c26d3..c0e22deb4 100644 --- a/examples/dynamic_color_schemes.py +++ b/examples/dynamic_color_schemes.py @@ -1,135 +1,81 @@ -from kivy.clock import Clock from kivy.lang import Builder -from kivy.properties import StringProperty, ColorProperty +from kivy.clock import Clock from kivy.uix.boxlayout import BoxLayout -from kivy.utils import hex_colormap +from kivymd.uix.boxlayout import MDBoxLayout -from kivymd.uix.menu import MDDropdownMenu from kivymd.app import MDApp - -KV = """ - - orientation: "vertical" - - MDLabel: - text: root.text - color: "grey" - adaptive_height: True - - MDCard: - theme_bg_color: "Custom" - md_bg_color: root.bg_color - - -MDScreen: - - MDIconButton: - on_release: app.open_menu(self) - pos_hint: {"top": .98} - x: "12dp" - icon: "menu" - - MDRecycleView: - id: card_list - viewclass: "ColorCard" - bar_width: 0 - size_hint_y: None - height: root.height - dp(68) - - RecycleGridLayout: - cols: 3 - spacing: "16dp" - padding: "16dp" - default_size: None, dp(56) - default_size_hint: 1, None - size_hint_y: None - height: self.minimum_height -""" - - -class ColorCard(BoxLayout): - text = StringProperty() - bg_color = ColorProperty() - - -class Example(MDApp): - menu: MDDropdownMenu = None +from kivymd.dynamic_color import DynamicColor +from examples.common_app import CommonApp, KV + +Builder.load_string(""" +#:import Clipboard kivy.core.clipboard.Clipboard + +: + name: "primaryColor" + color:[0,0,0,0] + size_hint_y:None + height:dp(130) + orientation:"vertical" + spacing:dp(10) + BoxLayout: + spacing:dp(10) + MDIconButton: + icon:"content-copy" + size_hint_x:None + width:dp(50) + on_release: + Clipboard.copy(root.name) + MDLabel: + text:root.name + adaptive_height:True + MDBoxLayout: + md_bg_color:root.color + radius:dp(10) + +: + ScrollView: + MDBoxLayout: + orientation:"vertical" + id:main_view + adaptive_height:True + spacing:dp(20) +""") + +class Container(MDBoxLayout): + pass + +class DynamicColorInfo(BoxLayout): + pass + +class Example(MDApp, CommonApp): def build(self): self.theme_cls.dynamic_color = True + self.theme_cls.path_to_wallpaper = "path_to_some_image.png" + self.theme_cls.on_colors = lambda : Clock.schedule_once(self.refresh) return Builder.load_string(KV) - def get_instance_from_menu(self, name_item): - index = 0 - rv = self.menu.ids.md_menu - opts = rv.layout_manager.view_opts - datas = rv.data[0] - - for data in rv.data: - if data["text"] == name_item: - index = rv.data.index(data) - break - - instance = rv.view_adapter.get_view( - index, datas, opts[index]["viewclass"] - ) - - return instance - - def open_menu(self, menu_button): - menu_items = [] - for item, method in { - "Set palette": lambda: self.set_palette(), - "Switch theme style": lambda: self.theme_switch(), - }.items(): - menu_items.append({"text": item, "on_release": method}) - self.menu = MDDropdownMenu( - caller=menu_button, - items=menu_items, - ) - self.menu.open() - - def set_palette(self): - instance_from_menu = self.get_instance_from_menu("Set palette") - available_palettes = [ - name_color.capitalize() for name_color in hex_colormap.keys() - ] - - menu_items = [] - for name_palette in available_palettes: - menu_items.append( - { - "text": name_palette, - "on_release": lambda x=name_palette: self.switch_palette(x), - } - ) - MDDropdownMenu( - caller=instance_from_menu, - items=menu_items, - ).open() - - def switch_palette(self, selected_palette): - self.theme_cls.primary_palette = selected_palette - Clock.schedule_once(self.generate_cards, 0.5) - - def theme_switch(self) -> None: - self.theme_cls.switch_theme() - Clock.schedule_once(self.generate_cards, 0.5) - - def generate_cards(self, *args): - self.root.ids.card_list.data = [] - for name_color in self.theme_cls.current_schemes_color_data: - self.root.ids.card_list.data.append( - { - "bg_color": getattr(self.theme_cls, name_color), - "text": name_color, - } - ) - def on_start(self): super().on_start() - Clock.schedule_once(self.generate_cards) - + parent_widget = self.root.ids.widget_box.parent.parent + parent_widget.clear_widgets() + self.container = Container() + parent_widget.add_widget(self.container) + self.container.ids.main_view.clear_widgets() + + for color in vars(DynamicColor).keys(): + if "__" in color: + continue + widget = DynamicColorInfo() + widget.name = color + widget.color = getattr(self.theme_cls, color) + self.container.ids.main_view.add_widget(widget) + + Clock.schedule_once(self.refresh) + + def refresh(self, *arg): + for widget in self.container.ids.main_view.children: + widget.color = getattr(self.theme_cls, widget.name) Example().run() diff --git a/kivymd/theming.py b/kivymd/theming.py index 5ddab0028..4f3811658 100755 --- a/kivymd/theming.py +++ b/kivymd/theming.py @@ -45,18 +45,14 @@ from kivymd.dynamic_color import DynamicColor from kivymd.font_definitions import theme_font_styles from kivymd.material_resources import DEVICE_IOS -from kivymd.utils.get_wallpaper import get_wallpaper -from PIL import Image - -from materialyoucolor.utils.color_utils import argb_from_rgb +from materialyoucolor.utils.color_utils import argb_from_rgba_01 from materialyoucolor.dynamiccolor.material_dynamic_colors import ( MaterialDynamicColors, ) -from materialyoucolor.scheme.scheme_tonal_spot import SchemeTonalSpot +from materialyoucolor.utils.platform_utils import SCHEMES, get_dynamic_scheme from materialyoucolor.hct import Hct -from materialyoucolor.quantize import QuantizeCelebi -from materialyoucolor.score.score import Score +from materialyoucolor.dislike.dislike_analyzer import DislikeAnalyzer class ThemeManager(EventDispatcher, DynamicColor): @@ -150,7 +146,8 @@ def build(self): dynamic_color_quality = NumericProperty(1 if platform == "android" else 10) """ - The quality of the generated color scheme from the system wallpaper. + The quality of the generated color scheme from the system wallpaper. + It is equal to or higher than `1`, with `1` representing the maximum quality. .. warning:: @@ -174,7 +171,7 @@ def build(self): To build the color scheme of your application from user wallpapers, you must enable the `READ_EXTERNAL_STORAGE `_ - permission: + permission if your android version is below 8.1: .. code-block:: python @@ -241,6 +238,24 @@ def callback(permission, results): :attr:`dynamic_color` is an :class:`~kivy.properties.BooleanProperty` and defaults to `False`. """ + + dynamic_scheme_name = OptionProperty("TONAL_SPOT", options=SCHEMES.keys()) + """ + Name of the dynamic scheme. Availabe schemes `TONAL_SPOT`, `SPRITZ` + `VIBRANT`, `EXPRESSIVE`, `FRUIT_SALAD`, `RAINBOW`, `MONOCHROME`, `FIDELITY` + and `CONTENT`. + + :attr:`dynamic_scheme_name` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'TONAL_SPOT'`. + """ + + dynamic_scheme_contrast = NumericProperty(0.0) + """ + The contrast of the generated color scheme. + + :attr:`dynamic_scheme_contrast` is an :class:`~kivy.properties.NumericProperty` + and defaults to `0.0`. + """ path_to_wallpaper = StringProperty() """ @@ -620,7 +635,15 @@ def build(self): :attr:`font_styles` is an :class:`~kivy.properties.DictProperty`. """ + on_colors = None + """ + A Helper function called when colors are changed. + + :attr: `on_colors` defaults to `None`. + """ + _size_current_wallpaper = NumericProperty(0) + _dark_mode = lambda self : False if self.theme_style == "Light" else True def __init__(self, **kwargs): super().__init__(**kwargs) @@ -636,29 +659,33 @@ def set_colors(self, *args) -> None: else: self._set_palette_color() else: - path_to_wallpaper = get_wallpaper( - App.get_running_app().user_data_dir, self.path_to_wallpaper + system_scheme = get_dynamic_scheme( + dark_mode=self._dark_mode(), + contrast=self.dynamic_scheme_contrast, + dynamic_color_quality=self.dynamic_color_quality, + fallback_wallpaper_path=self.path_to_wallpaper, + fallback_scheme_name=self.dynamic_scheme_name, + message_logger=Logger.info, + logger_head="KivyMD" ) - if path_to_wallpaper: - size_wallpaper = os.path.getsize(path_to_wallpaper) - if size_wallpaper != self._size_current_wallpaper: - self._size_current_wallpaper = os.path.getsize( - path_to_wallpaper - ) - self._set_dynamic_color(path_to_wallpaper) - else: - Logger.info( - "KivyMD: " - f"Color scheme generation. The color scheme of these " - f"wallpapers has already been generated. Skip it." - ) + if system_scheme: + self._set_color_names(system_scheme) else: - self._set_palette_color() + self._set_application_scheme() def update_theme_colors(self, *args) -> None: """Fired when the `theme_style` value changes.""" - self._set_application_scheme(self.primary_palette) + self.set_colors() + + def on_dynamic_scheme_name(self, *args): + self.set_colors() + + def on_dynamic_scheme_contrast(self, *args): + self.set_colors() + + def on_path_to_wallpaper(self, *args): + self.set_colors() def switch_theme(self) -> None: """Switches the theme from light to dark.""" @@ -674,54 +701,26 @@ def sync_theme_styles(self, *args) -> None: for style in self.font_styles.keys(): theme_font_styles.append(style) - def _set_dynamic_color(self, path_to_wallpaper: str) -> None: - start_time = default_timer() - image = Image.open(path_to_wallpaper) - pixel_len = image.width * image.height - image_data = image.getdata() - pixel_array = [ - image_data[_] - for _ in range(0, pixel_len, self.dynamic_color_quality) - ] - end_time = default_timer() - - Logger.info( - "KivyMD: " - f"Color scheme generation. Creating an array of pixels from a " - f"system wallpaper file - {end_time - start_time} sec." - ) - - start_time = default_timer() - colors = QuantizeCelebi(pixel_array, 128) - selected = Score.score(colors) - end_time = default_timer() - - Logger.info( - "KivyMD: " - f"Color scheme generation. Get dominant colors - " - f"{end_time - start_time} sec." - ) - self._set_application_scheme(color=selected[0]) - def _set_application_scheme( - self, default_color: str = None, color: int = None + self, + color = "blue", # Google default ) -> None: - # Default blue of Google. - start_time = default_timer() if not color: - color = get_color_from_hex( - hex_colormap[ - "blue" if not default_color else default_color.lower() - ] + color = "blue" + + color = get_color_from_hex(hex_colormap[color.lower()]) + color = Hct.from_int(argb_from_rgba_01(color)) + color = DislikeAnalyzer.fix_if_disliked(color).to_int() + + self._set_color_names( + SCHEMES[self.dynamic_scheme_name]( + Hct.from_int(color), + self._dark_mode(), + self.dynamic_scheme_contrast, ) - color = argb_from_rgb(*[c * 255 for c in color[:-1]]) - - scheme = SchemeTonalSpot( - Hct.from_int(color), # the color of current theme in int form - False if self.theme_style == "Light" else True, # dark mode - 0.0, # contrast level ) + def _set_color_names(self, scheme) -> None: for color_name in vars(MaterialDynamicColors).keys(): attr = getattr(MaterialDynamicColors, color_name) if hasattr(attr, "get_hct"): @@ -729,13 +728,8 @@ def _set_application_scheme( exec(f"self.{color_name}Color = {color_value}") self.disabledTextColor = self._get_disabled_hint_text_color() - end_time = default_timer() - - Logger.info( - "KivyMD: " - f"Color scheme generation. Get a color scheme from an installed " - f"palette - {end_time - start_time} sec." - ) + if self.on_colors: + self.on_colors() def _set_palette_color(self) -> None: if not self.primary_palette: diff --git a/kivymd/utils/get_wallpaper.py b/kivymd/utils/get_wallpaper.py deleted file mode 100644 index 21f57f3a2..000000000 --- a/kivymd/utils/get_wallpaper.py +++ /dev/null @@ -1,65 +0,0 @@ -from __future__ import annotations - -import os -import math - -from kivy import platform, Logger - - -def get_wallpaper( - user_data_dir: str, path_to_wallpaper: str = "" -) -> bool | str: - if platform == "android": - try: - from jnius import autoclass, cast - from android import mActivity - - CompressFormat = autoclass("android.graphics.Bitmap$CompressFormat") - FileOutputStream = autoclass("java.io.FileOutputStream") - WallpaperManager = autoclass("android.app.WallpaperManager") - Bitmap = autoclass("android.graphics.Bitmap") - - Context = mActivity.getApplicationContext() - mWallpaperManager = WallpaperManager.getInstance(Context) - bitmap = mWallpaperManager.getBitmap() - - # Scale the bitmap down as needed - # Taken from: - # https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-main/palette/palette/src/main/java/androidx/palette/graphics/Palette.java#890 - DEFAULT_RESIZE_BITMAP_AREA = 112 * 112 # Android default - bitmapArea = bitmap.getWidth() * bitmap.getHeight() - scaleRatio = -1 - - if bitmapArea > DEFAULT_RESIZE_BITMAP_AREA: - scaleRatio = math.sqrt(DEFAULT_RESIZE_BITMAP_AREA / bitmapArea) - - if scaleRatio >= 0: - bitmap = Bitmap.createScaledBitmap( - bitmap, - math.ceil(bitmap.getWidth() * scaleRatio), - math.ceil(bitmap.getHeight() * scaleRatio), - False - ) - - bitmap.compress( - CompressFormat.PNG, - 100, - FileOutputStream(f"{user_data_dir}/wallpaper.png"), - ) - return f"{user_data_dir}/wallpaper.png" - except Exception as exc: - Logger.error( - f"KivyMD: Dynamic color will not be used. " - f"The default palette is set. " - f"{exc}" - ) - return False - else: - if path_to_wallpaper: - return ( - path_to_wallpaper - if os.path.exists(path_to_wallpaper) - else False - ) - else: - return False diff --git a/setup.py b/setup.py index dd85d8052..276cb087b 100755 --- a/setup.py +++ b/setup.py @@ -129,7 +129,7 @@ def glob_paths(pattern): install_requires=[ "kivy>=2.3.0", "pillow", - "materialyoucolor", + "materialyoucolor>=2.0.7", "asynckivy>=0.6,<0.7", ], setup_requires=[],