oomox-modified/oomox_gui/preview.py

584 lines
22 KiB
Python

import os
from typing import Any, Final, Sequence
from gi.repository import Gtk, GLib, Gdk
from .theme_model import get_theme_model
from .color import (
convert_theme_color_to_gdk, mix_theme_colors, mix_gdk_colors, hex_lightness,
)
from .gtk_helpers import ScaledImage
from .preview_terminal import TerminalThemePreview
from .preview_icons import IconThemePreview
from .config import FALLBACK_COLOR, DEFAULT_ENCODING
from .i18n import translate
from .theme_file import ThemeT
from .plugin_api import OomoxThemePlugin, OomoxIconsPlugin
WIDGET_SPACING: Final = 10
class CssProviders():
theme: dict[str, Gtk.CssProvider]
border: dict[str, Gtk.CssProvider]
gradient: dict[str, Gtk.CssProvider]
headerbar_border: Gtk.CssProvider
wm_border: Gtk.CssProvider
caret: Gtk.CssProvider
reset_style: Gtk.CssProvider
def __init__(self) -> None:
self.theme = {}
self.border = {}
self.gradient = {}
self.headerbar_border = Gtk.CssProvider()
self.headerbar_border.load_from_data((
"""
headerbar {
border: none;
border-radius: 0;
}
"""
).encode('ascii'))
self.wm_border = Gtk.CssProvider()
self.caret = Gtk.CssProvider()
self.reset_style = Gtk.CssProvider()
self.reset_style.load_from_data((
"""
* {
box-shadow:none;
border: none;
}
"""
).encode('ascii'))
class PreviewHeaderbar(Gtk.HeaderBar):
title: Gtk.Label
button: Gtk.Button
def __init__(self) -> None:
super().__init__()
self.set_show_close_button(False) # type: ignore[arg-type]
self.title = Gtk.Label(label=translate("Headerbar"))
self.props.custom_title = self.title # type: ignore[attr-defined]
self.button = Gtk.Button(label=f' {translate("Button")} ')
self.pack_end(self.button) # type: ignore[arg-type]
class PreviewWidgets(Gtk.Box):
# gtk preview widgets:
headerbar: PreviewHeaderbar
menubar: Gtk.MenuBar
label: Gtk.Label
sel_label: Gtk.Label
entry: Gtk.Entry
preview_imageboxes: dict[str, ScaledImage]
preview_imageboxes_templates: dict[str, str]
button: Gtk.Button
def __init__(self) -> None:
super().__init__(orientation=Gtk.Orientation.VERTICAL)
self.grid = Gtk.Grid(row_spacing=6, column_spacing=6)
self.grid.set_margin_top(WIDGET_SPACING // 2)
self.grid.set_halign(Gtk.Align.CENTER)
headerbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.headerbar = PreviewHeaderbar()
self.menubar = Gtk.MenuBar()
menuitem1 = Gtk.MenuItem(label=translate('File'))
menuitem1.set_submenu(self.create_menu(3, True))
self.menubar.append(menuitem1) # type: ignore[attr-defined]
menuitem2 = Gtk.MenuItem(label=translate('Edit'))
menuitem2.set_submenu(self.create_menu(6, True))
self.menubar.append(menuitem2) # type: ignore[attr-defined]
headerbox.pack_start(self.headerbar, True, True, 0)
headerbox.pack_start(self.menubar, True, True, 0) # type: ignore[arg-type]
self.label = Gtk.Label(label=translate("This is a label"))
self.sel_label = Gtk.Label(label=translate("Selected item"))
self.entry = Gtk.Entry(text=translate("Text entry")) # type: ignore[call-arg]
self.button = Gtk.Button(label=translate("Button"))
self.preview_imageboxes = {}
self.preview_imageboxes_templates = {}
self.preview_imageboxes['CHECKBOX'] = ScaledImage(width=16)
fake_checkbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
fake_checkbox.pack_start(self.preview_imageboxes['CHECKBOX'], False, False, 0)
fake_checkbox.pack_start(self.label, False, False, 0)
fake_checkbox.set_margin_left(WIDGET_SPACING // 2)
self.grid.set_margin_left(WIDGET_SPACING)
self.grid.set_margin_right(WIDGET_SPACING)
self.grid.attach(fake_checkbox, 3, 3, 1, 1)
self.grid.attach_next_to(
self.sel_label, fake_checkbox, Gtk.PositionType.BOTTOM, 1, 1
)
self.grid.attach_next_to(
self.entry, self.sel_label, Gtk.PositionType.BOTTOM, 1, 1
)
self.grid.attach_next_to(
self.button, self.entry, Gtk.PositionType.BOTTOM, 1, 1
)
self.pack_start(headerbox, True, True, 0)
self.pack_start(self.grid, True, True, 0)
def create_menu(self, n_items: int, has_submenus: bool = False) -> Gtk.Menu:
menu = Gtk.Menu()
for i in range(0, n_items):
sensitive = (i + 1) % 3 != 0
label = translate('Item {id}') if sensitive else translate('Insensitive Item {id}')
item = Gtk.MenuItem( # type: ignore[call-arg]
label=label.format(id=i + 1),
sensitive=sensitive
)
menu.append(item)
if has_submenus and (i + 1) % 2 == 0:
item.set_submenu(self.create_menu(2))
return menu
def load_imageboxes_templates(self, theme_plugin: OomoxThemePlugin) -> None:
for icon in theme_plugin.PreviewImageboxesNames:
template_path = f"{icon.value}.svg.template"
with open(
os.path.join(
theme_plugin.gtk_preview_dir, template_path
), "rb"
) as file_object:
self.preview_imageboxes_templates[icon.name] = \
file_object.read().decode(DEFAULT_ENCODING)
def update_preview_imageboxes(
self, colorscheme: ThemeT, theme_plugin: OomoxThemePlugin
) -> None:
transform_function = theme_plugin.preview_transform_function
self.load_imageboxes_templates(theme_plugin)
for icon in theme_plugin.PreviewImageboxesNames:
new_svg_image = transform_function(
self.preview_imageboxes_templates[icon.name],
colorscheme
).encode('ascii')
self.preview_imageboxes[icon.name].set_from_bytes(
new_svg_image, width=theme_plugin.preview_sizes[icon.name]
)
def _get_menu_widgets(shell: Gtk.MenuShell) -> Sequence[Gtk.Menu | Gtk.MenuShell]:
""" gets a menu shell (menu or menubar) and all its children """
children = [shell]
for child in shell: # type: ignore[attr-defined]
children.append(child)
submenu = child.get_submenu()
if submenu:
children.extend(_get_menu_widgets(submenu))
return children
class ThemePreview(Gtk.Grid):
BG = 'bg' # pylint: disable=invalid-name
FG = 'fg' # pylint: disable=invalid-name
WM_BORDER_WIDTH: int = 2
theme_plugin_name: str | None = None
css_providers: CssProviders
# widget sections:
background: Gtk.Grid
gtk_preview: PreviewWidgets
icons_preview: IconThemePreview | None = None
terminal_preview: TerminalThemePreview | None = None
def __init__(self) -> None:
super().__init__(row_spacing=6, column_spacing=6)
self.set_border_width(10)
self.css_providers = CssProviders()
self.init_widgets()
def init_widgets(self) -> None:
self.gtk_preview = PreviewWidgets()
self.background = Gtk.Grid(row_spacing=WIDGET_SPACING, column_spacing=6)
self.attach(self.background, 1, 1, 3, 1)
self.gtk_preview.set_margin_bottom(WIDGET_SPACING)
self.background.attach(self.gtk_preview, 1, 3, 1, 1)
if self.icons_preview:
self.icons_preview.destroy()
self.icons_preview = IconThemePreview()
self.background.attach_next_to(
self.icons_preview, self.gtk_preview,
Gtk.PositionType.BOTTOM, 1, 1
)
if self.terminal_preview:
self.terminal_preview.destroy()
self.terminal_preview = TerminalThemePreview()
self.terminal_preview.set_margin_bottom(WIDGET_SPACING)
self.background.attach_next_to(
self.terminal_preview, self.icons_preview,
Gtk.PositionType.BOTTOM, 1, 1
)
self.background.set_margin_bottom(WIDGET_SPACING)
self.gtk_preview.button.connect("style-updated", self._queue_resize)
def override_widget_color(
self,
widget: Gtk.Widget,
value: str,
color: Gdk.RGBA,
state: Gtk.StateFlags = Gtk.StateFlags.NORMAL
) -> None:
if value == self.BG:
widget.override_background_color(state, color) # type: ignore[arg-type]
return
if value == self.FG:
widget.override_color(state, color) # type: ignore[arg-type]
return
raise NotImplementedError()
def update_preview_carets(self, colorscheme: ThemeT) -> None:
self.css_providers.caret.load_from_data((
(Gtk.get_minor_version() >= 20 and """
* {{
caret-color: #{primary_caret_color};
-gtk-secondary-caret-color: #{secondary_caret_color};
-GtkWidget-cursor-aspect-ratio: {caret_aspect_ratio};
}}
""" or """
* {{
-GtkWidget-cursor-color: #{primary_caret_color};
-GtkWidget-secondary-cursor-color: #{secondary_caret_color};
-GtkWidget-cursor-aspect-ratio: {caret_aspect_ratio};
}}
""").format(
primary_caret_color=colorscheme['CARET1_FG'],
secondary_caret_color=colorscheme['CARET2_FG'],
caret_aspect_ratio=colorscheme['CARET_SIZE']
)
).encode('ascii'))
Gtk.StyleContext.add_provider(
self.gtk_preview.entry.get_style_context(),
self.css_providers.caret,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
)
def update_preview_gradients(self, colorscheme: ThemeT) -> None:
gradient: float = colorscheme['GRADIENT'] # type: ignore[assignment]
if gradient == 0:
self.reset_gradients()
return
for widget, color_key in zip(
[
self.gtk_preview.button,
self.gtk_preview.headerbar.button,
self.gtk_preview.entry,
self.gtk_preview.headerbar,
],
[
"BTN_BG",
"HDR_BTN_BG",
"TXT_BG",
"HDR_BG"
]
):
color = colorscheme[color_key]
css_provider_gradient = self.css_providers.gradient.get(color_key)
if not css_provider_gradient:
css_provider_gradient = \
self.css_providers.gradient[color_key] = \
Gtk.CssProvider()
css_provider_gradient.load_from_data((
"""
* {{
background-image: linear-gradient(to bottom,
shade(#{color}, {amount1}),
shade(#{color}, {amount2})
);
}}
""".format(
color=color,
amount1=1 + gradient / 2,
amount2=1 - gradient / 2,
)
).encode('ascii'))
Gtk.StyleContext.add_provider(
widget.get_style_context(),
css_provider_gradient,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
)
def reset_gradients(self) -> None:
css_provider_gradient = self.css_providers.gradient.get("reset")
if not css_provider_gradient:
css_provider_gradient = \
self.css_providers.gradient["reset"] = \
Gtk.CssProvider()
css_provider_gradient.load_from_data((
"""
* {
background-image: none;
}
"""
).encode('ascii'))
for widget in [
self.gtk_preview.button,
self.gtk_preview.headerbar.button,
self.gtk_preview.entry,
self.gtk_preview.headerbar,
]:
Gtk.StyleContext.add_provider(
widget.get_style_context(),
css_provider_gradient,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
)
def update_preview_borders(self, colorscheme: ThemeT) -> None:
for widget_name, widget, fg, bg, ratio in ( # pylint: disable=invalid-name
(
'button',
self.gtk_preview.button,
colorscheme['BTN_FG'],
colorscheme['BTN_BG'],
0.22
), (
'headerbar_button',
self.gtk_preview.headerbar.button,
colorscheme['HDR_BTN_FG'],
colorscheme['HDR_BTN_BG'],
0.22
), (
'entry',
self.gtk_preview.entry,
colorscheme['TXT_BG'],
colorscheme['TXT_FG'],
0.8 * (0.7 + (
0 if hex_lightness(colorscheme['TXT_BG']) > 0.66 else ( # type: ignore[arg-type]
0.1 if hex_lightness(colorscheme['TXT_BG']) > 0.33 else 0.3 # type: ignore[arg-type]
)
))
),
):
border_color = mix_theme_colors(fg, bg, ratio) # type: ignore[arg-type]
css_provider_border_color = self.css_providers.border.get(widget_name)
if not css_provider_border_color:
css_provider_border_color = \
self.css_providers.border[widget_name] = \
Gtk.CssProvider()
css_provider_border_color.load_from_data(
f"""
* {{
border-color: #{border_color};
border-radius: {colorscheme["ROUNDNESS"]}px;
}}
""".encode('ascii')
)
Gtk.StyleContext.add_provider(
widget.get_style_context(),
css_provider_border_color,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
)
def update_preview_colors(self, colorscheme: ThemeT) -> None:
converted = {
theme_value['key']: convert_theme_color_to_gdk(
colorscheme[theme_value['key']] # type: ignore[arg-type]
)
for section in get_theme_model().values()
for theme_value in section if (
theme_value['type'] == 'color' and
not theme_value['key'].startswith('TERMINAL_')
)
}
def mix(color_id1: str, color_id2: str, amount: float) -> Gdk.RGBA:
return mix_gdk_colors(converted[color_id2], converted[color_id1], amount)
self.override_widget_color(self.background, self.BG, converted["BG"])
self.override_widget_color(self.gtk_preview.label, self.FG, converted["FG"])
self.override_widget_color(self.gtk_preview.sel_label, self.FG, converted["SEL_FG"])
self.override_widget_color(self.gtk_preview.sel_label, self.BG, converted["SEL_BG"])
self.override_widget_color(self.gtk_preview.entry, self.FG, converted["TXT_FG"])
self.override_widget_color(self.gtk_preview.entry, self.BG, converted["TXT_BG"])
self.override_widget_color(
self.gtk_preview.entry, self.FG, converted["SEL_FG"],
state=Gtk.StateFlags.SELECTED
)
self.override_widget_color(
self.gtk_preview.entry, self.BG, converted["SEL_BG"],
state=Gtk.StateFlags.SELECTED
)
self.override_widget_color(self.gtk_preview.button, self.FG, converted["BTN_FG"])
self.override_widget_color(self.gtk_preview.button, self.BG, converted["BTN_BG"])
for item in self.gtk_preview.menubar.get_children(): # type: ignore[attr-defined]
self.override_widget_color(item, self.FG, converted["HDR_FG"])
self.override_widget_color(
item, self.BG, mix("HDR_BG", "HDR_FG", 0.21),
state=Gtk.StateFlags.PRELIGHT
)
for widget in _get_menu_widgets(item.get_submenu()):
if isinstance(widget, Gtk.MenuShell):
self.override_widget_color(widget, self.BG, converted["HDR_BG"])
self.override_widget_color(widget, self.FG, converted["HDR_FG"])
else:
if not widget.get_sensitive(): # :disabled
color = mix("HDR_FG", "HDR_BG", 0.5)
else:
color = converted["HDR_FG"]
self.override_widget_color(widget, self.FG, color)
self.override_widget_color(
widget, self.BG, converted["SEL_BG"],
state=Gtk.StateFlags.PRELIGHT
)
self.override_widget_color(
widget, self.FG, converted["SEL_FG"],
state=Gtk.StateFlags.PRELIGHT
)
self.override_widget_color(
self.gtk_preview.menubar, self.BG, converted["HDR_BG"] # type: ignore[arg-type]
)
self.override_widget_color(self.gtk_preview.headerbar, self.BG, converted["HDR_BG"])
self.override_widget_color(self.gtk_preview.headerbar.title, self.FG, converted["HDR_FG"])
self.override_widget_color(
self.gtk_preview.headerbar.button, self.FG,
converted["HDR_BTN_FG"]
)
self.override_widget_color(
self.gtk_preview.headerbar.button, self.BG,
converted["HDR_BTN_BG"]
)
if self.icons_preview:
self.override_widget_color(
self.icons_preview, self.BG,
converted["TXT_BG"]
)
self.css_providers.wm_border.load_from_data(
f"""
* {{
border-color: #{colorscheme['WM_BORDER_FOCUS']};
/*border-radius: {colorscheme['ROUNDNESS']}px;*/
border-width: {self.WM_BORDER_WIDTH}px;
border-style: solid;
}}
""".encode('ascii')
)
Gtk.StyleContext.add_provider(
self.background.get_style_context(),
self.css_providers.wm_border,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
)
def update_preview(
self,
colorscheme: ThemeT,
theme_plugin: OomoxThemePlugin | None,
icons_plugin: OomoxIconsPlugin | None,
) -> None:
colorscheme_with_fallbacks: ThemeT = {}
for section in get_theme_model().values():
for theme_value in section:
if 'key' not in theme_value:
continue
result = colorscheme.get(theme_value['key'])
if not result and theme_value['type'] == 'color':
result = FALLBACK_COLOR
colorscheme_with_fallbacks[theme_value['key']] = result # type: ignore[assignment]
if not theme_plugin:
self.gtk_preview.hide()
else:
theme_plugin.preview_before_load_callback(self, colorscheme_with_fallbacks)
self.override_css_style(colorscheme_with_fallbacks, theme_plugin)
self.update_preview_colors(colorscheme_with_fallbacks)
self.update_preview_borders(colorscheme_with_fallbacks)
self.update_preview_carets(colorscheme_with_fallbacks)
self.update_preview_gradients(colorscheme_with_fallbacks)
self.gtk_preview.update_preview_imageboxes(colorscheme_with_fallbacks, theme_plugin)
self.gtk_preview.show()
if not self.icons_preview:
raise RuntimeError("Icon preview widget failed to load")
if not icons_plugin:
self.icons_preview.hide()
else:
self.icons_preview.update_preview(colorscheme_with_fallbacks, icons_plugin)
self.icons_preview.show()
if not self.terminal_preview:
raise RuntimeError("Terminal preview widget failed to load")
self.terminal_preview.update_preview(colorscheme_with_fallbacks)
def get_theme_css_provider(self, theme_plugin: OomoxThemePlugin) -> Gtk.CssProvider:
css_dir = theme_plugin.gtk_preview_dir
_css_postfix = '20' if Gtk.get_minor_version() >= 20 else ''
css_name = f"theme{_css_postfix}.css"
css_path = os.path.join(css_dir, css_name)
if not os.path.exists(css_path):
css_path = os.path.join(css_dir, "theme.css")
css_provider = self.css_providers.theme.get(css_path)
if css_provider:
return css_provider
css_provider = Gtk.CssProvider()
try:
css_provider.load_from_path(css_path) # type: ignore[arg-type]
except GLib.Error as exc:
print(exc)
self.css_providers.theme[css_path] = css_provider
return css_provider
def override_css_style(self, colorscheme: ThemeT, theme_plugin: OomoxThemePlugin) -> None:
new_theme_plugin_name: str = colorscheme["THEME_STYLE"] # type: ignore[assignment]
if new_theme_plugin_name == self.theme_plugin_name:
return
if self.theme_plugin_name:
for child in self.get_children():
self.remove(child)
child.destroy()
self.init_widgets()
self.theme_plugin_name = new_theme_plugin_name
base_theme_css_provider = self.get_theme_css_provider(theme_plugin)
def apply_css(widget: Gtk.Widget) -> None:
widget_style_context = widget.get_style_context()
Gtk.StyleContext.add_provider(
widget_style_context,
self.css_providers.reset_style,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
)
Gtk.StyleContext.add_provider(
widget_style_context,
base_theme_css_provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
)
if isinstance(widget, Gtk.Container):
widget.forall(apply_css) # type: ignore[arg-type]
apply_css(self)
Gtk.StyleContext.add_provider(
self.gtk_preview.headerbar.get_style_context(),
self.css_providers.headerbar_border,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
)
self.show_all()
def _queue_resize(self, *_args: Any) -> None:
# print(args)
self.queue_resize()