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__)
|
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")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user