feat: UI-Polish und Konfiguration
Wallpaper-Support via moongreet.toml, Astronauten-SVG als Default-Avatar mit dynamischer Theme-Farbe, CSS auf GTK-Theme-Farben umgestellt, konsistentes Widget-Spacing, Custom-Icon-Registrierung.
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
# ABOUTME: Configuration loading from moongreet.toml.
|
||||
# ABOUTME: Parses appearance and behavior settings for the greeter.
|
||||
|
||||
import tomllib
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
DEFAULT_CONFIG_PATHS = [
|
||||
Path("/etc/moongreet/moongreet.toml"),
|
||||
Path(__file__).parent.parent.parent / "config" / "moongreet.toml",
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
"""Greeter configuration loaded from moongreet.toml."""
|
||||
|
||||
background: Path | None = None
|
||||
|
||||
|
||||
def load_config(config_path: Path | None = None) -> Config:
|
||||
"""Load configuration from a TOML file.
|
||||
|
||||
Relative paths in the config are resolved against the config file's directory.
|
||||
"""
|
||||
if config_path is None:
|
||||
for path in DEFAULT_CONFIG_PATHS:
|
||||
if path.exists():
|
||||
config_path = path
|
||||
break
|
||||
if config_path is None:
|
||||
return Config()
|
||||
|
||||
if not config_path.exists():
|
||||
return Config()
|
||||
|
||||
with open(config_path, "rb") as f:
|
||||
data = tomllib.load(f)
|
||||
|
||||
config = Config()
|
||||
appearance = data.get("appearance", {})
|
||||
|
||||
bg = appearance.get("background")
|
||||
if bg:
|
||||
bg_path = Path(bg)
|
||||
if not bg_path.is_absolute():
|
||||
bg_path = config_path.parent / bg_path
|
||||
config.background = bg_path
|
||||
|
||||
return config
|
||||
+63
-16
@@ -10,6 +10,7 @@ gi.require_version("Gtk", "4.0")
|
||||
gi.require_version("Gdk", "4.0")
|
||||
from gi.repository import Gtk, Gdk, GLib, Gio, GdkPixbuf
|
||||
|
||||
from moongreet.config import load_config
|
||||
from moongreet.ipc import create_session, post_auth_response, start_session, cancel_session
|
||||
from moongreet.users import User, get_users, get_avatar_path, get_user_gtk_theme
|
||||
from moongreet.sessions import Session, get_sessions
|
||||
@@ -17,6 +18,7 @@ from moongreet.power import reboot, shutdown
|
||||
|
||||
LAST_USER_PATH = Path("/var/cache/moongreet/last-user")
|
||||
DEFAULT_AVATAR_PATH = Path(__file__).parent.parent.parent / "data" / "default-avatar.svg"
|
||||
AVATAR_SIZE = 128
|
||||
|
||||
|
||||
class GreeterWindow(Gtk.ApplicationWindow):
|
||||
@@ -27,6 +29,7 @@ class GreeterWindow(Gtk.ApplicationWindow):
|
||||
self.add_css_class("greeter")
|
||||
self.set_default_size(1920, 1080)
|
||||
|
||||
self._config = load_config()
|
||||
self._users = get_users()
|
||||
self._sessions = get_sessions()
|
||||
self._selected_user: User | None = None
|
||||
@@ -42,10 +45,17 @@ class GreeterWindow(Gtk.ApplicationWindow):
|
||||
overlay = Gtk.Overlay()
|
||||
self.set_child(overlay)
|
||||
|
||||
# Background fills the whole window
|
||||
background = Gtk.Box()
|
||||
background.set_hexpand(True)
|
||||
background.set_vexpand(True)
|
||||
# Background wallpaper (blurred and darkened)
|
||||
if self._config.background and self._config.background.exists():
|
||||
background = Gtk.Picture()
|
||||
background.set_filename(str(self._config.background))
|
||||
background.set_content_fit(Gtk.ContentFit.COVER)
|
||||
background.set_hexpand(True)
|
||||
background.set_vexpand(True)
|
||||
else:
|
||||
background = Gtk.Box()
|
||||
background.set_hexpand(True)
|
||||
background.set_vexpand(True)
|
||||
overlay.set_child(background)
|
||||
|
||||
# Main layout: 3 rows (top spacer, center login, bottom bar)
|
||||
@@ -80,19 +90,16 @@ class GreeterWindow(Gtk.ApplicationWindow):
|
||||
box.add_css_class("login-box")
|
||||
box.set_halign(Gtk.Align.CENTER)
|
||||
box.set_valign(Gtk.Align.CENTER)
|
||||
box.set_spacing(4)
|
||||
box.set_spacing(12)
|
||||
|
||||
# Avatar — wrapped in a fixed-size frame to constrain the Picture
|
||||
# Avatar — wrapped in a clipping frame for round shape
|
||||
avatar_frame = Gtk.Box()
|
||||
avatar_frame.set_size_request(96, 96)
|
||||
avatar_frame.set_size_request(AVATAR_SIZE, AVATAR_SIZE)
|
||||
avatar_frame.set_halign(Gtk.Align.CENTER)
|
||||
avatar_frame.set_overflow(Gtk.Overflow.HIDDEN)
|
||||
avatar_frame.add_css_class("avatar")
|
||||
self._avatar_image = Gtk.Picture()
|
||||
self._avatar_image.set_size_request(96, 96)
|
||||
self._avatar_image.set_content_fit(Gtk.ContentFit.COVER)
|
||||
self._avatar_image.set_hexpand(False)
|
||||
self._avatar_image.set_vexpand(False)
|
||||
self._avatar_image = Gtk.Image()
|
||||
self._avatar_image.set_pixel_size(AVATAR_SIZE)
|
||||
avatar_frame.append(self._avatar_image)
|
||||
box.append(avatar_frame)
|
||||
|
||||
@@ -104,7 +111,7 @@ class GreeterWindow(Gtk.ApplicationWindow):
|
||||
# Session dropdown
|
||||
self._session_dropdown = Gtk.DropDown()
|
||||
self._session_dropdown.add_css_class("session-dropdown")
|
||||
self._session_dropdown.set_halign(Gtk.Align.CENTER)
|
||||
self._session_dropdown.set_hexpand(True)
|
||||
if self._sessions:
|
||||
session_names = [s.name for s in self._sessions]
|
||||
string_list = Gtk.StringList.new(session_names)
|
||||
@@ -113,10 +120,10 @@ class GreeterWindow(Gtk.ApplicationWindow):
|
||||
|
||||
# Password entry
|
||||
self._password_entry = Gtk.PasswordEntry()
|
||||
self._password_entry.set_hexpand(True)
|
||||
self._password_entry.set_property("placeholder-text", "Passwort")
|
||||
self._password_entry.set_property("show-peek-icon", True)
|
||||
self._password_entry.add_css_class("password-entry")
|
||||
self._password_entry.set_halign(Gtk.Align.CENTER)
|
||||
self._password_entry.connect("activate", self._on_login_activate)
|
||||
box.append(self._password_entry)
|
||||
|
||||
@@ -211,9 +218,11 @@ class GreeterWindow(Gtk.ApplicationWindow):
|
||||
user.username, home_dir=user.home
|
||||
)
|
||||
if avatar_path and avatar_path.exists():
|
||||
self._avatar_image.set_filename(str(avatar_path))
|
||||
self._set_avatar_from_file(avatar_path)
|
||||
elif DEFAULT_AVATAR_PATH.exists():
|
||||
self._avatar_image.set_filename(str(DEFAULT_AVATAR_PATH))
|
||||
self._set_default_avatar()
|
||||
else:
|
||||
self._avatar_image.set_from_icon_name("avatar-default-symbolic")
|
||||
|
||||
# Apply user's GTK theme if available
|
||||
self._apply_user_theme(user)
|
||||
@@ -235,6 +244,44 @@ class GreeterWindow(Gtk.ApplicationWindow):
|
||||
else:
|
||||
settings.reset_property("gtk-theme-name")
|
||||
|
||||
def _get_foreground_color(self) -> str:
|
||||
"""Get the current GTK theme foreground color as a hex string."""
|
||||
color = self.get_style_context().lookup_color("theme_fg_color")
|
||||
if color[0]:
|
||||
rgba = color[1]
|
||||
r = int(rgba.red * 255)
|
||||
g = int(rgba.green * 255)
|
||||
b = int(rgba.blue * 255)
|
||||
return f"#{r:02x}{g:02x}{b:02x}"
|
||||
return "#ffffff"
|
||||
|
||||
def _set_default_avatar(self) -> None:
|
||||
"""Load the default avatar SVG, tinted with the GTK foreground color."""
|
||||
try:
|
||||
svg_text = DEFAULT_AVATAR_PATH.read_text()
|
||||
fg_color = self._get_foreground_color()
|
||||
svg_text = svg_text.replace("#PLACEHOLDER", fg_color)
|
||||
svg_bytes = svg_text.encode("utf-8")
|
||||
loader = GdkPixbuf.PixbufLoader.new_with_type("svg")
|
||||
loader.set_size(AVATAR_SIZE, AVATAR_SIZE)
|
||||
loader.write(svg_bytes)
|
||||
loader.close()
|
||||
pixbuf = loader.get_pixbuf()
|
||||
if pixbuf:
|
||||
self._avatar_image.set_from_pixbuf(pixbuf)
|
||||
except (GLib.Error, OSError):
|
||||
self._avatar_image.set_from_icon_name("avatar-default-symbolic")
|
||||
|
||||
def _set_avatar_from_file(self, path: Path) -> None:
|
||||
"""Load an image file and set it as the avatar, scaled to AVATAR_SIZE."""
|
||||
try:
|
||||
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
|
||||
str(path), AVATAR_SIZE, AVATAR_SIZE, True
|
||||
)
|
||||
self._avatar_image.set_from_pixbuf(pixbuf)
|
||||
except GLib.Error:
|
||||
self._avatar_image.set_from_icon_name("avatar-default-symbolic")
|
||||
|
||||
def _setup_keyboard_navigation(self) -> None:
|
||||
"""Set up keyboard shortcuts."""
|
||||
controller = Gtk.EventControllerKey()
|
||||
|
||||
@@ -28,6 +28,7 @@ class MoongreetApp(Gtk.Application):
|
||||
|
||||
def do_activate(self) -> None:
|
||||
"""Create and present the greeter window."""
|
||||
self._register_icons()
|
||||
self._load_css()
|
||||
window = GreeterWindow(application=self)
|
||||
|
||||
@@ -36,6 +37,12 @@ class MoongreetApp(Gtk.Application):
|
||||
|
||||
window.present()
|
||||
|
||||
def _register_icons(self) -> None:
|
||||
"""Register custom icons from the data/icons directory."""
|
||||
icons_dir = os.path.join(os.path.dirname(__file__), "..", "..", "data", "icons")
|
||||
icon_theme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default())
|
||||
icon_theme.add_search_path(os.path.realpath(icons_dir))
|
||||
|
||||
def _load_css(self) -> None:
|
||||
"""Load the CSS stylesheet for the greeter."""
|
||||
css_provider = Gtk.CssProvider()
|
||||
|
||||
+8
-15
@@ -12,15 +12,17 @@ window.greeter {
|
||||
.login-box {
|
||||
padding: 40px;
|
||||
border-radius: 12px;
|
||||
background-color: alpha(@window_bg_color, 0.7);
|
||||
background-color: alpha(@theme_bg_color, 0.7);
|
||||
}
|
||||
|
||||
/* Round avatar image */
|
||||
.avatar {
|
||||
border-radius: 50%;
|
||||
min-width: 96px;
|
||||
min-height: 96px;
|
||||
background-color: #3a3a5c;
|
||||
min-width: 128px;
|
||||
min-height: 128px;
|
||||
max-width: 128px;
|
||||
max-height: 128px;
|
||||
background-color: @theme_selected_bg_color;
|
||||
border: 3px solid alpha(white, 0.3);
|
||||
}
|
||||
|
||||
@@ -30,32 +32,23 @@ window.greeter {
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
/* Session dropdown */
|
||||
.session-dropdown {
|
||||
min-width: 200px;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 6px;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
/* Password entry field */
|
||||
.password-entry {
|
||||
min-width: 280px;
|
||||
min-height: 40px;
|
||||
border-radius: 20px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
font-size: 16px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* Error message label */
|
||||
.error-label {
|
||||
color: #ff6b6b;
|
||||
font-size: 14px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* User list on the bottom left */
|
||||
|
||||
Reference in New Issue
Block a user