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