add: инициализация виджета textual при выбора всего...
This commit is contained in:
parent
505c5f3421
commit
1b70c30eaf
@ -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 not focused:
|
||||
logger.debug("TUI: action_activate вызван, но нет сфокусированного элемента.")
|
||||
return
|
||||
else:
|
||||
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))
|
||||
|
||||
# Заглушки для других экранов:
|
||||
class BackupScreen(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")
|
||||
|
||||
class RestoreScreen(Static):
|
||||
def on_mount(self) -> None:
|
||||
self.update("♻️ Здесь будет экран выбора сервера и бэкапа для восстановления.")
|
||||
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")
|
||||
|
Loading…
x
Reference in New Issue
Block a user