Skip to content

Commit 0b0af7e

Browse files
authored
feat: make theme change interactive (#913)
2 parents 9cb21d3 + 72d98e7 commit 0b0af7e

File tree

19 files changed

+273
-153
lines changed

19 files changed

+273
-153
lines changed

sepal_ui/frontend/css/custom.css

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,6 @@ main.v-content {
1919
padding-top: 0px !important;
2020
}
2121

22-
/* remove all the backgrounds from the controls and widget to be colored naturelly by the map */
23-
.leaflet-control-container .vuetify-styles .v-application {
24-
background: rgb(0, 0, 0, 0);
25-
}
2622
.v-alert__wrapper .progress {
2723
background-color: transparent;
2824
}
Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
{
2-
"": { "color": ["#ffca28", "#ffc107"], "icon": "fa-regular fa-folder" },
3-
".csv": { "color": ["#4caf50", "#00c853"], "icon": "fa-solid fa-table" },
4-
".txt": { "color": ["#4caf50", "#00c853"], "icon": "fa-solid fa-table" },
5-
".tif": { "color": ["#9c27b0", "#673ab7"], "icon": "fa-regular fa-image" },
6-
".tiff": { "color": ["#9c27b0", "#673ab7"], "icon": "fa-regular fa-image" },
7-
".vrt": { "color": ["#9c27b0", "#673ab7"], "icon": "fa-regular fa-image" },
2+
"": { "color": "primary_contrast", "icon": "fa-regular fa-folder" },
3+
".csv": { "color": "secondary_contrast", "icon": "fa-solid fa-table" },
4+
".txt": { "color": "secondary_contrast", "icon": "fa-solid fa-table" },
5+
".tif": { "color": "secondary_contrast", "icon": "fa-regular fa-image" },
6+
".tiff": { "color": "secondary_contrast", "icon": "fa-regular fa-image" },
7+
".vrt": { "color": "secondary_contrast", "icon": "fa-regular fa-image" },
88
".shp": {
9-
"color": ["#9c27b0", "#673ab7"],
9+
"color": "secondary_contrast",
1010
"icon": "fa-solid fa-vector-square"
1111
},
1212
".geojson": {
13-
"color": ["#9c27b0", "#673ab7"],
13+
"color": "secondary_contrast",
1414
"icon": "fa-solid fa-vector-square"
1515
},
16-
"DEFAULT": { "color": ["#00bcd4", "#03a9f4"], "icon": "fa-regular fa-file" },
16+
"DEFAULT": { "color": "anchor", "icon": "fa-regular fa-file" },
1717
"PARENT": {
18-
"color": ["#424242", "#ffffff"],
18+
"color": "anchor",
1919
"icon": "fa-regular fa-folder-open"
2020
}
2121
}

sepal_ui/frontend/json/progress_bar.json

Lines changed: 0 additions & 3 deletions
This file was deleted.

sepal_ui/frontend/styles.py

Lines changed: 96 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22

33
from pathlib import Path
44
from types import SimpleNamespace
5-
from typing import Dict, Tuple
5+
from typing import Tuple
66

77
import ipyvuetify as v
88
from IPython.display import HTML, Javascript, display
9-
from traitlets import Bool, HasTraits, observe
9+
from ipyvuetify._version import semver
10+
from ipywidgets import Widget
11+
from traitlets import Bool, HasTraits, Unicode, link
1012

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

34-
DARK_THEME: Dict[str, str] = {
35-
"primary": "#b3842e",
36-
"accent": "#a1458e",
37-
"secondary": "#324a88",
38-
"success": "#3f802a",
39-
"info": "#79b1c9",
40-
"warning": "#b8721d",
41-
"error": "#a63228",
42-
"main": "#24221f", # Are not traits
43-
"darker": "#1a1a1a", # Are not traits
44-
"bg": "#121212", # Are not traits
45-
"menu": "#424242", # Are not traits
46-
}
47-
"colors used for the dark theme"
48-
49-
LIGHT_THEME: Dict[str, str] = {
50-
"primary": v.theme.themes.light.primary,
51-
"accent": v.theme.themes.light.accent,
52-
"secondary": v.theme.themes.light.secondary,
53-
"success": v.theme.themes.light.success,
54-
"info": v.theme.themes.light.info,
55-
"warning": v.theme.themes.light.warning,
56-
"error": v.theme.themes.light.error,
57-
"main": "#2e7d32",
58-
"darker": "#005005",
59-
"bg": "#FFFFFF",
60-
"menu": "#FFFFFF",
61-
}
62-
"colors used for the light theme"
6336

6437
TYPES: Tuple[str, ...] = (
6538
"info",
6639
"primary",
40+
"primary_contarst",
6741
"secondary",
42+
"secondary_contrast",
6843
"accent",
6944
"error",
7045
"success",
7146
"warning",
7247
"anchor",
48+
"main",
49+
"darker",
50+
"bg",
51+
"menu",
7352
)
7453
"The different types defined by ipyvuetify"
7554

55+
56+
class ThemeColors(Widget):
57+
58+
_model_name = Unicode("ThemeColorsModel").tag(sync=True)
59+
60+
_model_module = Unicode("jupyter-vuetify").tag(sync=True)
61+
62+
_view_module_version = Unicode(semver).tag(sync=True)
63+
64+
_model_module_version = Unicode(semver).tag(sync=True)
65+
66+
_theme_name = Unicode().tag(sync=True)
67+
68+
primary = Unicode().tag(sync=True)
69+
primary_contrast = Unicode().tag(sync=True)
70+
secondary = Unicode().tag(sync=True)
71+
secondary_contrast = Unicode().tag(sync=True)
72+
accent = Unicode().tag(sync=True)
73+
error = Unicode().tag(sync=True)
74+
info = Unicode().tag(sync=True)
75+
success = Unicode().tag(sync=True)
76+
warning = Unicode().tag(sync=True)
77+
anchor = Unicode(None, allow_none=True).tag(sync=True)
78+
main = Unicode().tag(sync=True)
79+
bg = Unicode().tag(sync=True)
80+
menu = Unicode().tag(sync=True)
81+
darker = Unicode().tag(sync=True)
82+
83+
84+
dark_theme_colors = ThemeColors(
85+
_theme_name="dark",
86+
primary="#76591e",
87+
primary_contrast="#bf8f2d", # a bit lighter than the primary color
88+
secondary="#363e4f",
89+
secondary_contrast="#5d76ab",
90+
error="#a63228",
91+
info="#c5c6c9",
92+
success="#3f802a",
93+
warning="#b8721d",
94+
accent="#272727",
95+
anchor="#f3f3f3",
96+
main="#24221f",
97+
darker="#1a1a1a",
98+
bg="#121212",
99+
menu="#424242",
100+
)
101+
102+
light_theme_colors = ThemeColors(
103+
_theme_name="light",
104+
primary="#5BB624",
105+
primary_contrast="#76b353",
106+
accent="#f3f3f3",
107+
anchor="#f3f3f3",
108+
secondary="#2199C4",
109+
secondary_contrast="#5d76ab",
110+
success=v.theme.themes.light.success,
111+
info=v.theme.themes.light.info,
112+
warning=v.theme.themes.light.warning,
113+
error=v.theme.themes.light.error,
114+
main="#2196f3", # used by appbar and versioncard
115+
darker="#ffffff", # used for the navdrawer
116+
bg="#FFFFFF",
117+
menu="#FFFFFF",
118+
)
119+
120+
DARK_THEME = {k: v for k, v in dark_theme_colors.__dict__["_trait_values"].items() if k in TYPES}
121+
"colors used for the dark theme"
122+
123+
LIGHT_THEME = {k: v for k, v in light_theme_colors.__dict__["_trait_values"].items() if k in TYPES}
124+
"colors used for the light theme"
125+
126+
127+
# override the default theme with the custom ones
128+
v.theme.themes.light = light_theme_colors
129+
v.theme.themes.dark = dark_theme_colors
130+
76131
################################################################################
77132
# define classes and method to make the application responsive
78133
#
@@ -92,47 +147,29 @@ class SepalColor(HasTraits, SimpleNamespace):
92147
_dark_theme: Bool = Bool(True if get_theme() == "dark" else False).tag(sync=True)
93148
"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."
94149

95-
new_colors: dict = {}
96-
"Dictionary with name:color structure."
97-
98-
@observe("_dark_theme")
99-
def __init__(self, *_, **new_colors) -> None:
100-
"""Custom simple name space to store and access to the sepal_ui colors and with a magic method to display theme.
150+
def __init__(self) -> None:
151+
"""Custom simple name space to store and access to the sepal_ui colors and with a magic method to display theme."""
152+
link((self, "_dark_theme"), (v.theme, "dark"))
153+
v.theme.observe(lambda *x: self.set_colors(), "dark")
101154

102-
Args:
103-
**new_colors (optional): the new colors to set in hexadecimal as a dict (experimental)
104-
"""
105-
# set vuetify theme
106-
v.theme.dark = self._dark_theme
155+
self.set_colors()
107156

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

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

114-
self.kwargs = DARK_THEME if self._dark_theme else LIGHT_THEME
115-
self.kwargs = new_colors or self.kwargs
116-
117-
# Even if the theme.themes.dark_theme trait could trigger the change on all elms
118-
# we have to replace the default values every time:
119-
theme = getattr(v.theme.themes, self.theme_name)
120-
121-
# TODO: Would be awesome to find a way to create traits for the new colors and
122-
# assign them here directly
123-
[setattr(theme, color_name, color) for color_name, color in self.kwargs.items()]
124-
125-
# Now instantiate the namespace
126-
SimpleNamespace.__init__(self, **self.kwargs)
127-
HasTraits.__init__(self)
128-
129-
return
165+
self.colors_dict = DARK_THEME if self._dark_theme else LIGHT_THEME
166+
SimpleNamespace.__init__(self, **self.colors_dict)
130167

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

137174
for name, color in items:
138175
c = su.to_colors(color)

sepal_ui/mapping/inspector_control.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""Customized ``Control`` to display the value of all available layers on a specific pixel."""
22

3-
import json
43
from pathlib import Path
54
from typing import Optional, Sequence, Union
65

@@ -15,9 +14,7 @@
1514
from shapely import geometry as sg
1615
from traitlets import Bool
1716

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

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

7166
# set up the content

sepal_ui/mapping/layers_control.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
"""Extend functionalities of the ipyleaflet layer control."""
2-
import json
2+
33
from types import SimpleNamespace
44
from typing import Optional
55

6-
import ipyvuetify as v
76
from ipyleaflet import GeoJSON, Map, TileLayer
87
from ipywidgets import link
98

10-
from sepal_ui import color
119
from sepal_ui import sepalwidgets as sw
12-
from sepal_ui.frontend import styles as ss
1310
from sepal_ui.mapping.menu_control import MenuControl
1411
from sepal_ui.message import ms
1512

@@ -82,7 +79,7 @@ def __init__(self, layer: TileLayer) -> None:
8279
"""
8380
# create the checkbox, by default layer are visible
8481
self.w_checkbox = sw.SimpleCheckbox(
85-
v_model=True, small=True, label=layer.name, color=color.primary
82+
v_model=True, small=True, label=layer.name, color="primary"
8683
)
8784
kwargs = {"style": "width: 10%;", "tag": "td"}
8885
checkbox_cell = sw.Html(children=[self.w_checkbox], **kwargs)
@@ -128,7 +125,7 @@ def __init__(self, layer: TileLayer) -> None:
128125
"""
129126
# create the checkbox, by default layer are visible
130127
self.w_checkbox = sw.SimpleCheckbox(
131-
v_model=True, small=True, label=layer.name, color=color.primary
128+
v_model=True, small=True, label=layer.name, color="primary"
132129
)
133130
kwargs = {"style": "width: 10%;", "tag": "td"}
134131
checkbox_cell = sw.Html(children=[self.w_checkbox], **kwargs)
@@ -170,11 +167,9 @@ def __init__(self, m: Map, **kwargs) -> None:
170167

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

sepal_ui/mapping/map_btn.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import ipyvuetify as v
44

5-
from sepal_ui import color
65
from sepal_ui import sepalwidgets as sw
76

87

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

2625
# some parameters are overloaded to match the map requirements
27-
kwargs["color"] = "text-color"
2826
kwargs["outlined"] = True
29-
kwargs["style_"] = f"background: {color.bg};"
3027
kwargs["children"] = [content]
3128
kwargs["icon"] = False
3229
kwargs.setdefault("class_", "v-map-btn")

sepal_ui/mapping/menu_control.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from traitlets import Bool, Int
88
from typing_extensions import Self
99

10-
from sepal_ui import color
1110
from sepal_ui import sepalwidgets as sw
1211
from sepal_ui.mapping.map_btn import MapBtn
1312

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

176174
return

sepal_ui/mapping/sepal_map.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,17 @@ def __init__(
150150
self._id = "".join(random.choice(string.ascii_lowercase) for i in range(6))
151151
self.add_class(self._id)
152152

153+
v.theme.observe(self._on_theme_change, "dark")
154+
155+
def _on_theme_change(self, _) -> None:
156+
"""Change the url of the basemaps."""
157+
light = "https://a.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png"
158+
dark = "https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png"
159+
160+
for layer in self.layers:
161+
if layer.base and layer.url in [light, dark]:
162+
layer.url = dark if v.theme.dark is True else light
163+
153164
@deprecated(version="2.8.0", reason="the local_layer stored list has been dropped")
154165
def _remove_local_raster(self, local_layer: str) -> Self:
155166
"""Remove local layer from memory.

0 commit comments

Comments
 (0)