This commit is contained in:
2026-06-03 21:26:54 +02:00
parent 05e6b8d061
commit d21e5175d5
125 changed files with 41986 additions and 0 deletions
+374
View File
@@ -0,0 +1,374 @@
import calendar
import subprocess
from datetime import datetime, timedelta
import gi
from fabric.widgets.centerbox import CenterBox
from fabric.widgets.label import Label
import modules.icons as icons
gi.require_version("Gtk", "3.0")
from gi.repository import GLib, Gtk, Gio
class Calendar(Gtk.Box):
def __init__(self, view_mode="month"):
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=8, name="calendar")
self.view_mode = view_mode
self.first_weekday = 0 # Default: Monday, will be updated async
self.set_halign(Gtk.Align.CENTER)
self.set_hexpand(False)
self.current_day_date = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
if self.view_mode == "month":
self.current_shown_date = self.current_day_date.replace(day=1)
self.current_year = self.current_shown_date.year
self.current_month = self.current_shown_date.month
self.current_day = self.current_day_date.day # Solo para resaltar en create_month_view
self.previous_key = (self.current_year, self.current_month)
elif self.view_mode == "week":
# current_shown_date es el primer día (según locale) de la semana actual
days_to_subtract = (self.current_day_date.weekday() - self.first_weekday + 7) % 7
self.current_shown_date = self.current_day_date - timedelta(days=days_to_subtract)
self.current_year = self.current_shown_date.year # Para el header
self.current_month = self.current_shown_date.month # Para el header
iso_year, iso_week, _ = self.current_shown_date.isocalendar()
self.previous_key = (iso_year, iso_week)
self.set_halign(Gtk.Align.FILL)
self.set_hexpand(True)
self.set_valign(Gtk.Align.CENTER)
self.set_vexpand(False)
self.cache_threshold = 3 # Umbral para mantener vistas en caché
self.month_views = {} # Reutilizado para vistas de semana también
self.prev_button = Gtk.Button( # Nombre genérico del botón
name="prev-month-button",
child=Label(name="month-button-label", markup=icons.chevron_left) # CSS puede ser genérico
)
self.prev_button.connect("clicked", self.on_prev_clicked)
self.month_label = Gtk.Label(name="month-label") # El nombre es histórico, pero muestra mes/año
self.next_button = Gtk.Button( # Nombre genérico del botón
name="next-month-button",
child=Label(name="month-button-label", markup=icons.chevron_right) # CSS puede ser genérico
)
self.next_button.connect("clicked", self.on_next_clicked)
self.header = CenterBox(
spacing=4,
name="header",
start_children=[self.prev_button],
center_children=[self.month_label],
end_children=[self.next_button],
)
self.add(self.header)
self.weekday_row = Gtk.Box(spacing=4, name="weekday-row")
self.pack_start(self.weekday_row, False, False, 0)
self.stack = Gtk.Stack(name="calendar-stack")
self.stack.set_transition_duration(250)
self.pack_start(self.stack, True, True, 0)
self.update_header() # Llamar antes de update_calendar para que el primer header sea correcto
self.update_calendar()
self.setup_periodic_update()
self.setup_dbus_listeners()
# Initialize locale settings asynchronously
GLib.Thread.new("calendar-locale", self._init_locale_settings_thread, None)
def _init_locale_settings_thread(self, user_data):
"""Background thread to initialize locale settings without blocking UI."""
try:
origin_date_str = subprocess.check_output(["locale", "week-1stday"], text=True).strip()
first_weekday_val = int(subprocess.check_output(["locale", "first_weekday"], text=True).strip())
origin_date = datetime.fromisoformat(origin_date_str)
# Esta lógica calcula el día de la semana (0-6, Lunes=0) que es considerado el primero
# según la configuración regional combinada de week-1stday y first_weekday.
date_of_first_day_of_week_config = origin_date + timedelta(days=first_weekday_val - 1)
new_first_weekday = date_of_first_day_of_week_config.weekday() # Lunes=0, ..., Domingo=6
# Update the first_weekday on main thread and refresh calendar if needed
GLib.idle_add(self._update_first_weekday, new_first_weekday)
except Exception as e:
print(f"Error getting locale first weekday: {e}")
# Keep default value (0 = Monday)
def _update_first_weekday(self, new_first_weekday):
"""Update first weekday setting and refresh calendar if changed."""
if self.first_weekday != new_first_weekday:
self.first_weekday = new_first_weekday
# Clear cache and refresh calendar with new locale settings
self.month_views.clear()
# Remove all current stack children to force regeneration
for child in self.stack.get_children():
self.stack.remove(child)
# Update header (which includes weekday labels) and calendar
self.update_header()
self.update_calendar()
return False # Don't repeat this idle callback
def setup_periodic_update(self):
# Check for date changes every second
GLib.timeout_add(1000, self.check_date_change)
def setup_dbus_listeners(self):
# Listen for system suspend/resume events
bus = Gio.bus_get_sync(Gio.BusType.SYSTEM, None)
bus.signal_subscribe(
None, # sender
'org.freedesktop.login1.Manager', # interface
'PrepareForSleep', # signal
'/org/freedesktop/login1', # path
None, # arg0
Gio.DBusSignalFlags.NONE,
self.on_suspend_resume, # callback
None # user_data
)
def check_date_change(self):
now = datetime.now()
current_date = now.replace(hour=0, minute=0, second=0, microsecond=0)
if current_date != self.current_day_date:
self.on_midnight()
return True # Continue the timer
def on_suspend_resume(self, connection, sender_name, object_path, interface_name, signal_name, parameters, user_data):
# Check date when resuming from suspend
self.check_date_change()
def on_midnight(self):
now = datetime.now()
self.current_day_date = now.replace(hour=0, minute=0, second=0, microsecond=0)
key_to_remove_for_today_highlight = None
if self.view_mode == "month":
# Actualizar la fecha base para la vista de mes si es necesario (aunque usualmente no cambia a medianoche)
self.current_shown_date = self.current_day_date.replace(day=1)
self.current_year = self.current_shown_date.year
self.current_month = self.current_shown_date.month
self.current_day = self.current_day_date.day # Actualizar el día actual
key_to_remove_for_today_highlight = (self.current_year, self.current_month)
elif self.view_mode == "week":
days_to_subtract = (self.current_day_date.weekday() - self.first_weekday + 7) % 7
self.current_shown_date = self.current_day_date - timedelta(days=days_to_subtract)
self.current_year = self.current_shown_date.year # Para el header
self.current_month = self.current_shown_date.month # Para el header
iso_year, iso_week, _ = self.current_shown_date.isocalendar()
key_to_remove_for_today_highlight = (iso_year, iso_week)
# Eliminar la vista actual de la caché para forzar la regeneración con el nuevo "hoy" resaltado
if key_to_remove_for_today_highlight and key_to_remove_for_today_highlight in self.month_views:
widget = self.month_views.pop(key_to_remove_for_today_highlight)
self.stack.remove(widget)
# Si la vista eliminada era la actual, previous_key podría quedar desactualizado
# pero update_calendar lo corregirá al establecer la nueva vista.
self.update_calendar() # Esto regenerará la vista si fue eliminada y actualizará el resaltado
return False # Importante para que el timeout no se repita automáticamente
def update_header(self):
# self.current_shown_date es el primer día del mes (modo mes) o el primer día de la semana (modo semana)
# El encabezado siempre muestra el mes y año de self.current_shown_date
self.month_label.set_text(
self.current_shown_date.strftime("%B %Y").capitalize()
)
for child in self.weekday_row.get_children():
self.weekday_row.remove(child)
day_initials = self.get_weekday_initials()
for day_initial in day_initials:
label = Gtk.Label(label=day_initial.upper(), name="weekday-label")
self.weekday_row.pack_start(label, True, True, 0)
self.weekday_row.show_all()
def update_calendar(self):
new_key = None
child_name = "" # Renombrado de child_name_prefix
view_widget = None
if self.view_mode == "month":
new_key = (self.current_year, self.current_month)
child_name = f"{self.current_year}_{self.current_month}"
if new_key not in self.month_views:
view_widget = self.create_month_view(self.current_year, self.current_month)
elif self.view_mode == "week":
iso_year, iso_week, _ = self.current_shown_date.isocalendar()
new_key = (iso_year, iso_week)
child_name = f"{iso_year}_w{iso_week}"
if new_key not in self.month_views:
# Pasar self.current_shown_date directamente a create_week_view
view_widget = self.create_week_view(self.current_shown_date)
if new_key is None: return
if new_key > self.previous_key:
self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT)
elif new_key < self.previous_key:
self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_RIGHT)
# else: no transition if key is the same (e.g. on_midnight for same month/week)
self.previous_key = new_key
if view_widget: # Si se creó una nueva vista
self.month_views[new_key] = view_widget
self.stack.add_titled(view_widget, child_name, child_name)
self.stack.set_visible_child_name(child_name)
# El encabezado se actualiza ANTES de llamar a update_calendar en __init__ y on_clicked,
# y también en on_midnight si es necesario.
# Pero si la vista cambia (ej. de Enero a Febrero), el encabezado debe reflejarlo.
self.update_header() # Asegurar que el header está sincronizado con la vista actual
self.stack.show_all()
self.prune_cache()
def prune_cache(self):
def get_key_index(key_tuple):
year, num = key_tuple # num es month o week_number
if self.view_mode == "month": # Asumiendo que la clave es (año, mes)
return year * 12 + (num - 1)
else: # Asumiendo que la clave es (año_iso, semana_iso)
return year * 53 + num # Usar 53 para cubrir años con 53 semanas ISO
current_index = get_key_index(self.previous_key) # previous_key es la clave de la vista actual
keys_to_remove = []
for key_iter in self.month_views:
if abs(get_key_index(key_iter) - current_index) > self.cache_threshold:
keys_to_remove.append(key_iter)
for key_to_remove in keys_to_remove:
widget = self.month_views.pop(key_to_remove)
self.stack.remove(widget)
def create_month_view(self, year, month):
grid = Gtk.Grid(column_homogeneous=True, row_homogeneous=False, name="calendar-grid")
cal = calendar.Calendar(firstweekday=self.first_weekday)
month_days = cal.monthdayscalendar(year, month)
while len(month_days) < 6: # Asegurar 6 filas para consistencia visual
month_days.append([0] * 7) # [0] representa un día vacío
for row, week in enumerate(month_days):
for col, day_num in enumerate(week):
day_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, name="day-box")
top_spacer = Gtk.Box(hexpand=True, vexpand=True)
middle_box = Gtk.Box(hexpand=True, vexpand=True)
bottom_spacer = Gtk.Box(hexpand=True, vexpand=True)
if day_num == 0:
label = Label(name="day-empty", markup=icons.dot)
else:
label = Gtk.Label(label=str(day_num), name="day-label")
day_date_obj = datetime(year, month, day_num)
if day_date_obj == self.current_day_date:
label.get_style_context().add_class("current-day")
middle_box.pack_start(Gtk.Box(hexpand=True, vexpand=True), True, True, 0)
middle_box.pack_start(label, False, False, 0)
middle_box.pack_start(Gtk.Box(hexpand=True, vexpand=True), True, True, 0)
day_box.pack_start(top_spacer, True, True, 0)
day_box.pack_start(middle_box, True, True, 0)
day_box.pack_start(bottom_spacer, True, True, 0)
grid.attach(day_box, col, row, 1, 1)
grid.show_all()
return grid
def create_week_view(self, first_day_of_week_to_display):
grid = Gtk.Grid(column_homogeneous=True, row_homogeneous=False, name="calendar-grid-week-view") # Podría tener estilo diferente
# El mes de referencia para atenuar es el mes de first_day_of_week_to_display
# que es self.current_shown_date, y su mes es self.current_month (actualizado en nav)
reference_month_for_dimming = first_day_of_week_to_display.month
for col in range(7):
current_day_in_loop = first_day_of_week_to_display + timedelta(days=col)
day_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, name="day-box") # Reusar estilo de day-box
top_spacer = Gtk.Box(hexpand=True, vexpand=True)
middle_box = Gtk.Box(hexpand=True, vexpand=True)
bottom_spacer = Gtk.Box(hexpand=True, vexpand=True)
label = Gtk.Label(label=str(current_day_in_loop.day), name="day-label")
if current_day_in_loop == self.current_day_date:
label.get_style_context().add_class("current-day")
if current_day_in_loop.month != reference_month_for_dimming:
label.get_style_context().add_class("dim-label") # Necesita CSS: .dim-label { opacity: 0.5; } o similar
middle_box.pack_start(Gtk.Box(hexpand=True, vexpand=True), True, True, 0)
middle_box.pack_start(label, False, False, 0)
middle_box.pack_start(Gtk.Box(hexpand=True, vexpand=True), True, True, 0)
day_box.pack_start(top_spacer, True, True, 0)
day_box.pack_start(middle_box, True, True, 0)
day_box.pack_start(bottom_spacer, True, True, 0)
grid.attach(day_box, col, 0, 1, 1) # Todos los días en la fila 0
# Para mantener una altura similar a la vista mensual, se podrían añadir filas vacías.
# Esto es opcional y depende del diseño deseado.
# for r_idx in range(1, 6): # Añadir 5 filas vacías
# empty_row_placeholder = Gtk.Box(name="day-empty-placeholder", hexpand=True, vexpand=True, height_request=20) # Ajustar altura
# grid.attach(empty_row_placeholder, 0, r_idx, 7, 1) # Abarca las 7 columnas
grid.show_all()
return grid
def get_weekday_initials(self):
# Genera las iniciales de los días de la semana comenzando por self.first_weekday
# datetime(2024, 1, 1) es Lunes. Su weekday() es 0.
# Si self.first_weekday es 0 (Lunes), queremos que el primer día sea Lunes.
# i=0: datetime(2024, 1, 1 + 0) -> Lunes
# Si self.first_weekday es 6 (Domingo), queremos que el primer día sea Domingo.
# i=0: datetime(2024, 1, 1 + 6) -> Domingo
# Esta lógica es correcta.
return [(datetime(2024, 1, 1) + timedelta(days=(self.first_weekday + i) % 7)).strftime("%a")[:1] for i in range(7)]
def on_prev_clicked(self, widget):
if self.view_mode == "month":
current_month_val = self.current_shown_date.month
current_year_val = self.current_shown_date.year
if current_month_val == 1:
self.current_shown_date = self.current_shown_date.replace(year=current_year_val - 1, month=12)
else:
self.current_shown_date = self.current_shown_date.replace(month=current_month_val - 1)
self.current_year = self.current_shown_date.year
self.current_month = self.current_shown_date.month
elif self.view_mode == "week":
self.current_shown_date -= timedelta(days=7)
self.current_year = self.current_shown_date.year # Actualizar para el header
self.current_month = self.current_shown_date.month # Actualizar para el header y dimming
# self.update_header() # Se llama dentro de update_calendar
self.update_calendar()
def on_next_clicked(self, widget):
if self.view_mode == "month":
current_month_val = self.current_shown_date.month
current_year_val = self.current_shown_date.year
if current_month_val == 12:
self.current_shown_date = self.current_shown_date.replace(year=current_year_val + 1, month=1)
else:
self.current_shown_date = self.current_shown_date.replace(month=current_month_val + 1)
self.current_year = self.current_shown_date.year
self.current_month = self.current_shown_date.month
elif self.view_mode == "week":
self.current_shown_date += timedelta(days=7)
self.current_year = self.current_shown_date.year # Actualizar para el header
self.current_month = self.current_shown_date.month # Actualizar para el header y dimming
# self.update_header() # Se llama dentro de update_calendar
self.update_calendar()