add: инициализация виджета textual при выбора всего...

This commit is contained in:
Sergei Erjemin 2025-06-13 00:57:37 +03:00
parent 505c5f3421
commit 1b70c30eaf

View File

@ -14,7 +14,8 @@ from textual.widgets import Button, Header, Footer, Static, Log, Label, ListView
# --- Настройки и инициирование логирования модуля --- # --- Настройки и инициирование логирования модуля ---
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Кастомный обработчик логирования для Textual
# --- Кастомный обработчик логирования (для Textual) ---
class TextualLogHandler(logging.Handler): class TextualLogHandler(logging.Handler):
""" """
Обработчик логирования, который направляет записи в виджет Textual Log. Обработчик логирования, который направляет записи в виджет Textual Log.
@ -37,56 +38,88 @@ class TextualLogHandler(logging.Handler):
self.handleError(record) self.handleError(record)
# --- Верхнее меню приложения (для Textual) ---
class MainMenu(Static): class MainMenu(Static):
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Horizontal( yield Horizontal(
Button(label="Backup (DB -> Dump)", id="backup"), Button(label="Backup (DB\u2192Dump)", id="backup"),
Button(label="Restore (Dump -> DB)", id="restore"), Button(label="Restore (Dump\u2192DB)", id="restore"),
Button(label="Copy (DB -> DB)", id="copy", variant="error"), Button(label="Copy (DB\u2192DB)", id="copy", variant="error"),
Button(label="Service", id="service"),
Button(label="Quit", id="quit", variant="error"), Button(label="Quit", id="quit", variant="error"),
id="menu", id="menu",
) )
# Новый простой экран для Backup # --- Виджет для "встроенного" выбора ---
class SimpleBackupScreen(Screen): class SelectionWidget(Static): # Наследуем от Static
""" """
Простой экран для демонстрации перехода при выборе Backup. Виджет для отображения опций выбора бэкапа внутри основного экрана.
""" """
def __init__(self,
BINDINGS = [ app_config: Optional[Dict[str, Any]] = None,
("escape", "pop_screen", "Назад"), # Позволяет вернуться на главный экран по Escape action_type=None,
] **kwargs):
super().__init__(**kwargs)
self.app_config = app_config if app_config is not None else {}
self.action_type = action_type # Сохраняем тип действия
# SelectionWidget(Static)
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Header(name="Экран Бэкапа") # Заголовок для нового экрана if self.action_type == "backup":
yield Label("Привет, мир! Это экран для бэкапа.", classes="greeting-label") yield Label("Выберите BD-сервер для бэкапа:")
yield Footer() if "servers" in self.app_config:
logger.info(f"TUI: Обнаружено {len(self.app_config['servers'])} серверов в конфигурации.")
# ... тут будет список серверов
elif self.action_type == "restore":
yield Label("Выберите том для восстановления:")
if "servers" in self.app_config:
logger.info(f"TUI: Обнаружено {len(self.app_config['targets'])} томов в конфигурации.")
# ... тут будет список томов
elif self.action_type == "copy":
yield Label("Выберите BD-сервер (откуда копировать):")
# ... тут будет список серверов
elif self.action_type == "service":
yield Label("Служебные функции:")
# ... тут будут служебные функции
yield Static("Тут будет список чего-то...", classes="placeholder-text") # Временная заглушка
yield Button("Отмена", id="cancel_backup_selection", variant="default")
def on_mount(self) -> None: def on_mount(self) -> None:
logger.info(f"Экран '{self.id}' (SimpleBackupScreen) смонтирован.") logger.info(f"Виджет '{self.id}' (SelectionWidget) смонтирован.")
# Можно сразу установить фокус на первый элемент, если он есть
# self.query_one(OptionList).focus() или self.query_one(Button).focus()
async def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "cancel_backup_selection":
logger.info("Нажата отмена в SelectionWidget.")
await self.remove() # Удаляем сам виджет
self.app.selection_widget_active = False # <-- Сброс флага что виджет активен
# Можно также послать сообщение приложению, чтобы оно обновило состояние, если нужно
# self.app.post_message(BackupSelectionCancelled())
class PGanecApp(App): class PGanecApp(App):
CSS = """ CSS = """
Screen { Screen { layout: vertical; } /* Вертикальная компоновка для всего приложения */
layout: vertical; /* Используем вертикальный layout для всего экрана */
}
#main_content_area { /* Новый контейнер для всего, что НЕ прибито к краям */ #main_content_area { /* Новый контейнер для всего, что НЕ прибито к краям */
/* Этот контейнер будет занимать все доступное пространство /* Этот контейнер будет занимать все доступное пространство между Header и Log/Footer */
между Header и Log/Footer */
height: 1fr; /* 1fr означает "одна доля доступного пространства" */ height: 1fr; /* 1fr означает "одна доля доступного пространства" */
overflow-y: auto; /* Если контент не влезет, появится прокрутка */ overflow-y: auto; /* Если контент не влезет, появится прокрутка */
} }
#title { width: 100%; text-align: left; background: purple; padding: 0 1; } #title { width: 100%; text-align: left; background: purple; padding: 0 1; }
#menu { width: 100%; height: 3; align: left top; padding: 0; } #menu { width: 100%; height: 3; align: left top; padding: 0; }
Button { #menu Button {
align: center middle; align: center middle;
width: 25%; width: 20%;
color: orange; color: orange;
background: transparent; background: transparent;
margin: 0; padding: 0; margin: 0; padding: 0;
} }
Button#backup, Button#restore, Button#quit { color: orange; border: round orange; } Button#backup, Button#restore, Button#service, Button#quit { color: orange; border: round orange; }
Button#copy { color: grey; border: round orange; } Button#copy { color: grey; border: round orange; }
@ -95,19 +128,31 @@ class PGanecApp(App):
Log#app_log_viewer { Log#app_log_viewer {
dock: bottom; dock: bottom;
height: 10; height: 10;
margin-top: 1;
border-top: double grey; border-top: double grey;
background: 15%; background: 15%;
} }
Log#app_log_viewer:focus { height: 50%; border-top: solid green; background: 45%; } Log#app_log_viewer:focus { height: 50%; border-top: solid green; background: 45%; }
/* Стили для нового экрана */ /* Стили для встроенного виджета выбора */
SimpleBackupScreen .greeting-label { SelectionWidget {
width: 100%; padding: 1;
text-align: center; margin-top: 0;
padding: 2 0; /* Немного отступов сверху и снизу */ background: $panel; /* Используем системный цвет панели */
/* height: auto; /* Высота будет по содержимому */
}
SelectionWidget .selection-widget-label {
margin-bottom: 1;
text-style: bold; text-style: bold;
} }
SelectionWidget .placeholder-text {
padding: 1;
color: $text-muted;
}
SelectionWidget Button { /* Стили для кнопок внутри виджета */
width: auto; /* Чтобы кнопка была по размеру текста */
background: 15%;
margin-top: 1;
}
Footer { dock: bottom; height: 1; } Footer { dock: bottom; height: 1; }
Header { dock: top; height: 1; } Header { dock: top; height: 1; }
@ -118,12 +163,16 @@ class PGanecApp(App):
("b", "backup", "Backup"), ("b", "backup", "Backup"),
("r", "restore", "Restore"), ("r", "restore", "Restore"),
("c", "copy", "Copy"), ("c", "copy", "Copy"),
("s", "service", "Service"),
("q", "quit", "Quit"), ("q", "quit", "Quit"),
("right", "focus_next"), ("down", "focus_next"), # Стрелочки для навигации (вниз/вправо -- вперед) ("right", "focus_next"), ("down", "focus_next"), # Стрелочки для навигации (вниз/вправо -- вперед)
("left", "focus_previous"), ("up", "focus_previous"), # Стрелочки для навигации (вверх/влево -- назад) ("left", "focus_previous"), ("up", "focus_previous"), # Стрелочки для навигации (вверх/влево -- назад)
("enter", "activate"), ("enter", "activate"),
] ]
# Атрибут для хранения ссылки на виджет выбора, если он отображен
selection_widget_instance: Optional[SelectionWidget] = None
def __init__(self, def __init__(self,
log_level_int: int = logging.INFO, log_level_int: int = logging.INFO,
app_config: Dict[str, Any] = None, app_config: Dict[str, Any] = None,
@ -137,6 +186,7 @@ class PGanecApp(App):
self.log_level_int = log_level_int self.log_level_int = log_level_int
self.app_config = app_config if app_config is not None else {} # Сохраняем конфигурацию self.app_config = app_config if app_config is not None else {} # Сохраняем конфигурацию
self.early_log_handler = early_log_handler # <--- Сохраняем self.early_log_handler = early_log_handler # <--- Сохраняем
self.selection_widget_instance = None # Инициализируем атрибут для виджета выбора
logger.debug( logger.debug(
f"TUI инициализирован. Log Level: `{logging.getLevelName(self.log_level_int)}`; Config Keys: {list(self.app_config.keys())}" f"TUI инициализирован. Log Level: `{logging.getLevelName(self.log_level_int)}`; Config Keys: {list(self.app_config.keys())}"
) )
@ -148,19 +198,62 @@ class PGanecApp(App):
yield Static("PGanec TUI", id="title") yield Static("PGanec TUI", id="title")
yield MainMenu() yield MainMenu()
# Другие элементы основного интерфейса могут быть здесь # Другие элементы основного интерфейса могут быть здесь
# Место для нашего виджета выбора будет здесь.
# Мы не добавляем его сразу, а будем делать это динамически.
# Можно добавить "якорь" - пустой контейнер, если нужно точное позиционирование
yield Container(id="selection_widget_placeholder")
yield Log(id="app_log_viewer", highlight=True) # Виджет лога yield Log(id="app_log_viewer", highlight=True) # Виджет лога
yield Footer() yield Footer()
async def show_selection_widget(self, action_type: str) -> None:
"""Показывает виджет выбора для ."""
# Сначала удаляем старый виджет, если он есть, чтобы не было дублей
if self.selection_widget_instance:
try:
await self.selection_widget_instance.remove()
except Exception as e: # DOMObjectMissingError может возникнуть, если уже удален
logger.debug(f"Ошибка при удалении старого selection_widget_instance (возможно, уже удален): {e}")
self.selection_widget_instance = None
# Создаем новый экземпляр виджета
self.selection_widget_instance = SelectionWidget(
app_config=self.app_config,
action_type=action_type,
id="select_any" # Даем ему id для возможного поиска
)
self.selection_widget_active = True # Выставляем флаг, что виджет активен
# Находим контейнер, куда его добавить.
# Будем добавлять после MainMenu внутри #main_content_area
main_content_area = self.query_one("#main_content_area", Vertical)
main_menu = self.query_one(MainMenu)
# Добавляем виджет в DOM после MainMenu
# `mount` добавляет виджет и вызывает его on_mount
await main_content_area.mount(self.selection_widget_instance, after=main_menu)
self.selection_widget_instance.focus() # Пытаемся установить фокус
logger.info("SelectionWidget добавлен в DOM.")
async def on_button_pressed(self, event: Button.Pressed) -> None: async def on_button_pressed(self, event: Button.Pressed) -> None:
"""
Обработчик нажатия (Enter или Click) кнопки в верхнем меню.
:param event:
:return:
"""
button_id = event.button.id button_id = event.button.id
if button_id == "backup": if button_id == "backup":
await self.push_screen(SimpleBackupScreen()) logger.debug("TUI: Enter или Click на кнопку 'Backup' -- Инициализируем виджет выбора.")
await self.show_selection_widget("backup")
elif button_id == "restore": elif button_id == "restore":
logger.info("Действие 'Restore' пока не реализовано с новым экраном.") logger.debug("TUI: Enter или Click на кнопку 'Restore' -- Инициализируем виджет выбора.")
self.app.bell() # Сигнал, что действие не выполнено await self.show_selection_widget("restore")
elif button_id == "copy": elif button_id == "copy":
logger.warning("Copy action is currently a placeholder for quit.") logger.debug("TUI: Enter или Click на кнопку 'Copy' -- Инициализируем виджет выбора.")
await self.action_quit() await self.show_selection_widget("copy")
elif button_id == "service":
logger.debug("TUI: Enter или Click на кнопку 'Service' -- Инициализируем виджет выбора.")
await self.show_selection_widget("service")
elif button_id == "quit": elif button_id == "quit":
await self.action_quit() await self.action_quit()
@ -186,36 +279,90 @@ class PGanecApp(App):
target_logger = logging.getLogger() target_logger = logging.getLogger()
target_logger.addHandler(textual_handler) # Добавляем наш обработчик target_logger.addHandler(textual_handler) # Добавляем наш обработчик
# --- Перенос и сброс ранних логов из MemoryHandler --- # --- -- Перенос и сброс ранних логов из MemoryHandler -- ---
if self.early_log_handler: if self.early_log_handler:
# Устанавливаем наш textual_handler как цель для MemoryHandler # Устанавливаем наш textual_handler как цель для MemoryHandler
self.early_log_handler.setTarget(textual_handler) self.early_log_handler.setTarget(textual_handler)
# Сбрасываем все накопленные записи # Сбрасываем все накопленные записи
self.early_log_handler.flush() self.early_log_handler.flush()
# Закрываем и удаляем MemoryHandler из корневого логгера, т.к. он больше не нужен # Закрываем и удаляем MemoryHandler из корневого логера, так как он больше не нужен
self.early_log_handler.close() self.early_log_handler.close()
logger.debug("TUI: Перенесли и закрыли ранние логи…") logger.debug("TUI: Перенесли и закрыли ранние логи…")
target_logger.removeHandler(self.early_log_handler) target_logger.removeHandler(self.early_log_handler)
logger.debug("TUI: Удалили ранний MemoryHandler из корневого логгера…") logger.debug("TUI: Удалили ранний MemoryHandler из корневого логгера…")
self.early_log_handler = None # Очищаем ссылку self.early_log_handler = None # Очищаем ссылку
# --- Конец переноса и сброса --- # --- -- Конец переноса и сброса -- ---
# --- Конец обжужукивания логирования ---
logger.info("TUI-логгер инициализирован") logger.info("TUI-логгер инициализирован")
logger.debug("TUI: DEBUG сообщение для проверки") logger.debug("test TUI-logger: DEBUG")
logger.warning("TUI: WARNING для проверки") logger.warning("test TUI-logger: WARNING")
logger.error("TUI: ERROR тоже для проверки") logger.error("test TUI-logger: ERROR")
async def action_activate(self) -> None: async def action_activate(self) -> None:
"""
Обработчик нажатия клавиши Enter.
:return:
"""
focused = self.focused focused = self.focused
if not focused:
logger.debug("TUI: action_activate вызван, но нет сфокусированного элемента.")
return
else:
if isinstance(focused, Button): if isinstance(focused, Button):
# Проверяем, не является ли кнопка частью нашего виджета выбора
if not isinstance(focused, Button): # Если не кнопка - ничего не делаем
logger.debug("TUI: Что-то нажато (Enter), но это не кнопка.")
return
if focused.parent and isinstance(focused.parent, SelectionWidget):
await focused.parent.on_button_pressed(Button.Pressed(focused))
else:
await self.on_button_pressed(Button.Pressed(focused)) await self.on_button_pressed(Button.Pressed(focused))
# Заглушки для других экранов: # --- Обработчики быстрых клавиш ---
class BackupScreen(Static): # Для 'q' (Quit) уже есть в BINDINGS срабатывающий метод action_quit, он сработает автоматически (не ясно почему).
def on_mount(self) -> None: async def action_backup(self):
self.update("🛠 Здесь будет экран выбора сервера для бэкапа.") """
Обработчик нажатия быстрой клавиши 'b' для инициализации виджета создания бэкапа.
:return:
"""
if getattr(self, "selection_widget_active", False): # Проверяем, активен ли виджет выбора
return # Если активен, ничего не делаем (отменяем нажатие клавиши)
self.query_one("#backup", Button).focus() # подсветим кнопку
logger.debug("TUI: Быстрая клавиша 'b' -- Инициализируем виджет.")
await self.show_selection_widget("backup")
class RestoreScreen(Static): async def action_restore(self):
def on_mount(self) -> None: """
self.update("♻️ Здесь будет экран выбора сервера и бэкапа для восстановления.") Обработчик нажатия быстрой клавиши 'r' для восстановления.
:return:
"""
if getattr(self, "selection_widget_active", False): # Проверяем, активен ли виджет выбора
return
self.query_one("#restore", Button).focus() # подсветим кнопку
logger.debug("TUI: Быстрая клавиша 'r' -- Инициализируем виджет.")
await self.show_selection_widget("restore")
async def action_copy(self):
"""
Обработчик нажатия быстрой клавиши 'c' для копирования.
:return:
"""
if getattr(self, "selection_widget_active", False): # Проверяем, активен ли виджет выбора
return
self.query_one("#copy", Button).focus()
logger.debug("TUI: Быстрая клавиша 'c' -- Инициализируем виджет.")
await self.show_selection_widget("copy")
async def action_service(self):
"""
Обработчик нажатия быстрой клавиши 's' для сервисных действий.
:return:
"""
if getattr(self, "selection_widget_active", False): # Проверяем, активен ли виджет выбора
return
self.query_one("#service", Button).focus()
logger.debug("TUI: Быстрая клавиша 's' -- Инициализируем виджет.")
await self.show_selection_widget("service")