Skip to content

Commit

Permalink
feat: make theme change interactive (#913)
Browse files Browse the repository at this point in the history
  • Loading branch information
dfguerrerom authored May 10, 2024
2 parents 9cb21d3 + 72d98e7 commit 0b0af7e
Show file tree
Hide file tree
Showing 19 changed files with 273 additions and 153 deletions.
4 changes: 0 additions & 4 deletions sepal_ui/frontend/css/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,6 @@ main.v-content {
padding-top: 0px !important;
}

/* remove all the backgrounds from the controls and widget to be colored naturelly by the map */
.leaflet-control-container .vuetify-styles .v-application {
background: rgb(0, 0, 0, 0);
}
.v-alert__wrapper .progress {
background-color: transparent;
}
Expand Down
20 changes: 10 additions & 10 deletions sepal_ui/frontend/json/file_icons.json
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
{
"": { "color": ["#ffca28", "#ffc107"], "icon": "fa-regular fa-folder" },
".csv": { "color": ["#4caf50", "#00c853"], "icon": "fa-solid fa-table" },
".txt": { "color": ["#4caf50", "#00c853"], "icon": "fa-solid fa-table" },
".tif": { "color": ["#9c27b0", "#673ab7"], "icon": "fa-regular fa-image" },
".tiff": { "color": ["#9c27b0", "#673ab7"], "icon": "fa-regular fa-image" },
".vrt": { "color": ["#9c27b0", "#673ab7"], "icon": "fa-regular fa-image" },
"": { "color": "primary_contrast", "icon": "fa-regular fa-folder" },
".csv": { "color": "secondary_contrast", "icon": "fa-solid fa-table" },
".txt": { "color": "secondary_contrast", "icon": "fa-solid fa-table" },
".tif": { "color": "secondary_contrast", "icon": "fa-regular fa-image" },
".tiff": { "color": "secondary_contrast", "icon": "fa-regular fa-image" },
".vrt": { "color": "secondary_contrast", "icon": "fa-regular fa-image" },
".shp": {
"color": ["#9c27b0", "#673ab7"],
"color": "secondary_contrast",
"icon": "fa-solid fa-vector-square"
},
".geojson": {
"color": ["#9c27b0", "#673ab7"],
"color": "secondary_contrast",
"icon": "fa-solid fa-vector-square"
},
"DEFAULT": { "color": ["#00bcd4", "#03a9f4"], "icon": "fa-regular fa-file" },
"DEFAULT": { "color": "anchor", "icon": "fa-regular fa-file" },
"PARENT": {
"color": ["#424242", "#ffffff"],
"color": "anchor",
"icon": "fa-regular fa-folder-open"
}
}
3 changes: 0 additions & 3 deletions sepal_ui/frontend/json/progress_bar.json

This file was deleted.

155 changes: 96 additions & 59 deletions sepal_ui/frontend/styles.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

from pathlib import Path
from types import SimpleNamespace
from typing import Dict, Tuple
from typing import Tuple

import ipyvuetify as v
from IPython.display import HTML, Javascript, display
from traitlets import Bool, HasTraits, observe
from ipyvuetify._version import semver
from ipywidgets import Widget
from traitlets import Bool, HasTraits, Unicode, link

import sepal_ui.scripts.utils as su
from sepal_ui.conf import config
Expand All @@ -31,48 +33,101 @@
# define all the colors that we want to use in the theme
#

DARK_THEME: Dict[str, str] = {
"primary": "#b3842e",
"accent": "#a1458e",
"secondary": "#324a88",
"success": "#3f802a",
"info": "#79b1c9",
"warning": "#b8721d",
"error": "#a63228",
"main": "#24221f", # Are not traits
"darker": "#1a1a1a", # Are not traits
"bg": "#121212", # Are not traits
"menu": "#424242", # Are not traits
}
"colors used for the dark theme"

LIGHT_THEME: Dict[str, str] = {
"primary": v.theme.themes.light.primary,
"accent": v.theme.themes.light.accent,
"secondary": v.theme.themes.light.secondary,
"success": v.theme.themes.light.success,
"info": v.theme.themes.light.info,
"warning": v.theme.themes.light.warning,
"error": v.theme.themes.light.error,
"main": "#2e7d32",
"darker": "#005005",
"bg": "#FFFFFF",
"menu": "#FFFFFF",
}
"colors used for the light theme"

TYPES: Tuple[str, ...] = (
"info",
"primary",
"primary_contarst",
"secondary",
"secondary_contrast",
"accent",
"error",
"success",
"warning",
"anchor",
"main",
"darker",
"bg",
"menu",
)
"The different types defined by ipyvuetify"


class ThemeColors(Widget):

_model_name = Unicode("ThemeColorsModel").tag(sync=True)

_model_module = Unicode("jupyter-vuetify").tag(sync=True)

_view_module_version = Unicode(semver).tag(sync=True)

_model_module_version = Unicode(semver).tag(sync=True)

_theme_name = Unicode().tag(sync=True)

primary = Unicode().tag(sync=True)
primary_contrast = Unicode().tag(sync=True)
secondary = Unicode().tag(sync=True)
secondary_contrast = Unicode().tag(sync=True)
accent = Unicode().tag(sync=True)
error = Unicode().tag(sync=True)
info = Unicode().tag(sync=True)
success = Unicode().tag(sync=True)
warning = Unicode().tag(sync=True)
anchor = Unicode(None, allow_none=True).tag(sync=True)
main = Unicode().tag(sync=True)
bg = Unicode().tag(sync=True)
menu = Unicode().tag(sync=True)
darker = Unicode().tag(sync=True)


dark_theme_colors = ThemeColors(
_theme_name="dark",
primary="#76591e",
primary_contrast="#bf8f2d", # a bit lighter than the primary color
secondary="#363e4f",
secondary_contrast="#5d76ab",
error="#a63228",
info="#c5c6c9",
success="#3f802a",
warning="#b8721d",
accent="#272727",
anchor="#f3f3f3",
main="#24221f",
darker="#1a1a1a",
bg="#121212",
menu="#424242",
)

light_theme_colors = ThemeColors(
_theme_name="light",
primary="#5BB624",
primary_contrast="#76b353",
accent="#f3f3f3",
anchor="#f3f3f3",
secondary="#2199C4",
secondary_contrast="#5d76ab",
success=v.theme.themes.light.success,
info=v.theme.themes.light.info,
warning=v.theme.themes.light.warning,
error=v.theme.themes.light.error,
main="#2196f3", # used by appbar and versioncard
darker="#ffffff", # used for the navdrawer
bg="#FFFFFF",
menu="#FFFFFF",
)

DARK_THEME = {k: v for k, v in dark_theme_colors.__dict__["_trait_values"].items() if k in TYPES}
"colors used for the dark theme"

LIGHT_THEME = {k: v for k, v in light_theme_colors.__dict__["_trait_values"].items() if k in TYPES}
"colors used for the light theme"


# override the default theme with the custom ones
v.theme.themes.light = light_theme_colors
v.theme.themes.dark = dark_theme_colors

################################################################################
# define classes and method to make the application responsive
#
Expand All @@ -92,47 +147,29 @@ class SepalColor(HasTraits, SimpleNamespace):
_dark_theme: Bool = Bool(True if get_theme() == "dark" else False).tag(sync=True)
"Whether to use dark theme or not. By changing this value, the theme value will be stored in the conf file. Is only intended to be accessed in development mode."

new_colors: dict = {}
"Dictionary with name:color structure."

@observe("_dark_theme")
def __init__(self, *_, **new_colors) -> None:
"""Custom simple name space to store and access to the sepal_ui colors and with a magic method to display theme.
def __init__(self) -> None:
"""Custom simple name space to store and access to the sepal_ui colors and with a magic method to display theme."""
link((self, "_dark_theme"), (v.theme, "dark"))
v.theme.observe(lambda *x: self.set_colors(), "dark")

Args:
**new_colors (optional): the new colors to set in hexadecimal as a dict (experimental)
"""
# set vuetify theme
v.theme.dark = self._dark_theme
self.set_colors()

def set_colors(self) -> None:
"""Set the current hexadecimal color in the object."""
# Get get current theme name
self.theme_name = "dark" if self._dark_theme else "light"

# Save "new" theme in configuration file
su.set_config("theme", self.theme_name)

self.kwargs = DARK_THEME if self._dark_theme else LIGHT_THEME
self.kwargs = new_colors or self.kwargs

# Even if the theme.themes.dark_theme trait could trigger the change on all elms
# we have to replace the default values every time:
theme = getattr(v.theme.themes, self.theme_name)

# TODO: Would be awesome to find a way to create traits for the new colors and
# assign them here directly
[setattr(theme, color_name, color) for color_name, color in self.kwargs.items()]

# Now instantiate the namespace
SimpleNamespace.__init__(self, **self.kwargs)
HasTraits.__init__(self)

return
self.colors_dict = DARK_THEME if self._dark_theme else LIGHT_THEME
SimpleNamespace.__init__(self, **self.colors_dict)

def _repr_html_(self, *_) -> str:
"""Rich display of the color palette in an HTML frontend."""
s = 60
html = f"<h3>Current theme: {self.theme_name}</h3><table>"
items = {k: v for k, v in self.kwargs.items()}.items()
items = {k: v for k, v in self.colors_dict.items()}.items()

for name, color in items:
c = su.to_colors(color)
Expand Down
7 changes: 1 addition & 6 deletions sepal_ui/mapping/inspector_control.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Customized ``Control`` to display the value of all available layers on a specific pixel."""

import json
from pathlib import Path
from typing import Optional, Sequence, Union

Expand All @@ -15,9 +14,7 @@
from shapely import geometry as sg
from traitlets import Bool

from sepal_ui import color
from sepal_ui import sepalwidgets as sw
from sepal_ui.frontend import styles as ss
from sepal_ui.mapping.layer import EELayer
from sepal_ui.mapping.menu_control import MenuControl
from sepal_ui.message import ms
Expand Down Expand Up @@ -61,11 +58,9 @@ def __init__(self, m: Map, open_tree: bool = True, **kwargs) -> None:

# create a loading to place it on top of the card. It will always be visible
# even when the card is scrolled
p_style = json.loads((ss.JSON_DIR / "progress_bar.json").read_text())
self.w_loading = sw.ProgressLinear(
indeterminate=False,
background_color=color.menu,
color=p_style["color"][v.theme.dark],
background_color="menu",
)

# set up the content
Expand Down
13 changes: 4 additions & 9 deletions sepal_ui/mapping/layers_control.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
"""Extend functionalities of the ipyleaflet layer control."""
import json

from types import SimpleNamespace
from typing import Optional

import ipyvuetify as v
from ipyleaflet import GeoJSON, Map, TileLayer
from ipywidgets import link

from sepal_ui import color
from sepal_ui import sepalwidgets as sw
from sepal_ui.frontend import styles as ss
from sepal_ui.mapping.menu_control import MenuControl
from sepal_ui.message import ms

Expand Down Expand Up @@ -82,7 +79,7 @@ def __init__(self, layer: TileLayer) -> None:
"""
# create the checkbox, by default layer are visible
self.w_checkbox = sw.SimpleCheckbox(
v_model=True, small=True, label=layer.name, color=color.primary
v_model=True, small=True, label=layer.name, color="primary"
)
kwargs = {"style": "width: 10%;", "tag": "td"}
checkbox_cell = sw.Html(children=[self.w_checkbox], **kwargs)
Expand Down Expand Up @@ -128,7 +125,7 @@ def __init__(self, layer: TileLayer) -> None:
"""
# create the checkbox, by default layer are visible
self.w_checkbox = sw.SimpleCheckbox(
v_model=True, small=True, label=layer.name, color=color.primary
v_model=True, small=True, label=layer.name, color="primary"
)
kwargs = {"style": "width: 10%;", "tag": "td"}
checkbox_cell = sw.Html(children=[self.w_checkbox], **kwargs)
Expand Down Expand Up @@ -170,11 +167,9 @@ def __init__(self, m: Map, **kwargs) -> None:

# create a loading to place it on top of the card. It will always be visible
# even when the card is scrolled
p_style = json.loads((ss.JSON_DIR / "progress_bar.json").read_text())
self.w_loading = sw.ProgressLinear(
indeterminate=False,
background_color=color.menu,
color=p_style["color"][v.theme.dark],
background_color="menu",
)
self.tile = sw.Tile("nested", "")

Expand Down
3 changes: 0 additions & 3 deletions sepal_ui/mapping/map_btn.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import ipyvuetify as v

from sepal_ui import color
from sepal_ui import sepalwidgets as sw


Expand All @@ -24,9 +23,7 @@ def __init__(self, content: str, **kwargs) -> None:
content = content[: min(3, len(content))].upper()

# some parameters are overloaded to match the map requirements
kwargs["color"] = "text-color"
kwargs["outlined"] = True
kwargs["style_"] = f"background: {color.bg};"
kwargs["children"] = [content]
kwargs["icon"] = False
kwargs.setdefault("class_", "v-map-btn")
Expand Down
4 changes: 1 addition & 3 deletions sepal_ui/mapping/menu_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from traitlets import Bool, Int
from typing_extensions import Self

from sepal_ui import color
from sepal_ui import sepalwidgets as sw
from sepal_ui.mapping.map_btn import MapBtn

Expand Down Expand Up @@ -170,7 +169,6 @@ def activate(self, *args) -> None:
"""Change the background color of the btn with respect to the status."""
# grey is contrasted enough for both light and dark theme
# could be customized further if requested
bg_color = "gray" if self.menu.v_model is True else color.bg
self.menu.v_slots[0]["children"].style_ = f"background: {bg_color};"
self.menu.v_slots[0]["children"].style_ = "background: gray;" if self.menu.v_model else ""

return
11 changes: 11 additions & 0 deletions sepal_ui/mapping/sepal_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,17 @@ def __init__(
self._id = "".join(random.choice(string.ascii_lowercase) for i in range(6))
self.add_class(self._id)

v.theme.observe(self._on_theme_change, "dark")

def _on_theme_change(self, _) -> None:
"""Change the url of the basemaps."""
light = "https://a.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png"
dark = "https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png"

for layer in self.layers:
if layer.base and layer.url in [light, dark]:
layer.url = dark if v.theme.dark is True else light

@deprecated(version="2.8.0", reason="the local_layer stored list has been dropped")
def _remove_local_raster(self, local_layer: str) -> Self:
"""Remove local layer from memory.
Expand Down
Loading

0 comments on commit 0b0af7e

Please sign in to comment.