diff --git a/.idea/workspace.xml b/.idea/workspace.xml index fc59fa5..5284823 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -1,7 +1,18 @@ - + + + + + + + + + + + + \ No newline at end of file diff --git a/astro_settings.json b/astro_settings.json index 6562295..756754f 100644 --- a/astro_settings.json +++ b/astro_settings.json @@ -1,13 +1,12 @@ { "cameras": [ "Canon 40D", - "Canon 400D", - "Canon 500D" + "Canon 400D" ], "lenses": [ "MTO-500A", - "Юпитер-21м", - "Tamron 18-200mm" + "Tamron 18-200mm", + "Юпитер-21м 200мм" ], "telescopes": [ "Celestron Astromaster 130 (f/5.0, F=650mm, D=130mm)" diff --git a/celestial_bodies.json b/celestial_bodies.json index c510679..bd6d355 100644 --- a/celestial_bodies.json +++ b/celestial_bodies.json @@ -10,5 +10,6 @@ "M89", "Венера", "Меркурий", - "Нептун" + "Нептун", + "Saturn" ] \ No newline at end of file diff --git a/main.py b/main.py index b079cf5..30672c9 100644 --- a/main.py +++ b/main.py @@ -1,32 +1,22 @@ """ -Astro Session Watcher - Главный входной файл -Приложение для астрофотографов с отслеживанием файлов и сортировкой по объектам +Astro Session Watcher v0.4.0 - tkinter версия """ + +import tkinter as tk +from tkinter import ttk import sys import os -from pathlib import Path # Добавляем корневую директорию в путь sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from PySide6.QtWidgets import QApplication from ui.main_window import MainWindow def main(): - """Точка входа в приложение""" - app = QApplication(sys.argv) - - # Устанавливаем стиль Fusion для современного вида - app.setStyle("Fusion") - - # Тёмная палитра - app.setPalette(app.style().standardPalette()) - - window = MainWindow() - window.show() - - sys.exit(app.exec()) + root = tk.Tk() + app = MainWindow(root) + root.mainloop() if __name__ == "__main__": diff --git a/models/__pycache__/__init__.cpython-313.pyc b/models/__pycache__/__init__.cpython-313.pyc index b164826..a502d50 100644 Binary files a/models/__pycache__/__init__.cpython-313.pyc and b/models/__pycache__/__init__.cpython-313.pyc differ diff --git a/models/__pycache__/astro_object.cpython-313.pyc b/models/__pycache__/astro_object.cpython-313.pyc index 7d6fbd6..3374dd7 100644 Binary files a/models/__pycache__/astro_object.cpython-313.pyc and b/models/__pycache__/astro_object.cpython-313.pyc differ diff --git a/models/__pycache__/session.cpython-313.pyc b/models/__pycache__/session.cpython-313.pyc index 7f6a944..15f7d11 100644 Binary files a/models/__pycache__/session.cpython-313.pyc and b/models/__pycache__/session.cpython-313.pyc differ diff --git a/services/__pycache__/__init__.cpython-313.pyc b/services/__pycache__/__init__.cpython-313.pyc index 24f8907..012c6b9 100644 Binary files a/services/__pycache__/__init__.cpython-313.pyc and b/services/__pycache__/__init__.cpython-313.pyc differ diff --git a/services/__pycache__/config_service.cpython-313.pyc b/services/__pycache__/config_service.cpython-313.pyc index 9bd6f5e..4d22e00 100644 Binary files a/services/__pycache__/config_service.cpython-313.pyc and b/services/__pycache__/config_service.cpython-313.pyc differ diff --git a/services/__pycache__/file_service.cpython-313.pyc b/services/__pycache__/file_service.cpython-313.pyc index 12d7937..d21d910 100644 Binary files a/services/__pycache__/file_service.cpython-313.pyc and b/services/__pycache__/file_service.cpython-313.pyc differ diff --git a/services/__pycache__/session_service.cpython-313.pyc b/services/__pycache__/session_service.cpython-313.pyc index 5f88702..fb65467 100644 Binary files a/services/__pycache__/session_service.cpython-313.pyc and b/services/__pycache__/session_service.cpython-313.pyc differ diff --git a/services/__pycache__/watch_service.cpython-313.pyc b/services/__pycache__/watch_service.cpython-313.pyc index 7f20f07..49d4f78 100644 Binary files a/services/__pycache__/watch_service.cpython-313.pyc and b/services/__pycache__/watch_service.cpython-313.pyc differ diff --git a/ui/__pycache__/__init__.cpython-313.pyc b/ui/__pycache__/__init__.cpython-313.pyc index ac8a3f4..3aed9d2 100644 Binary files a/ui/__pycache__/__init__.cpython-313.pyc and b/ui/__pycache__/__init__.cpython-313.pyc differ diff --git a/ui/__pycache__/main_window.cpython-313.pyc b/ui/__pycache__/main_window.cpython-313.pyc index b85de7a..5273062 100644 Binary files a/ui/__pycache__/main_window.cpython-313.pyc and b/ui/__pycache__/main_window.cpython-313.pyc differ diff --git a/ui/dialogs/__pycache__/__init__.cpython-313.pyc b/ui/dialogs/__pycache__/__init__.cpython-313.pyc index 9c7cd83..da86210 100644 Binary files a/ui/dialogs/__pycache__/__init__.cpython-313.pyc and b/ui/dialogs/__pycache__/__init__.cpython-313.pyc differ diff --git a/ui/dialogs/__pycache__/calibration_dialog.cpython-313.pyc b/ui/dialogs/__pycache__/calibration_dialog.cpython-313.pyc index 5316e23..1a0b685 100644 Binary files a/ui/dialogs/__pycache__/calibration_dialog.cpython-313.pyc and b/ui/dialogs/__pycache__/calibration_dialog.cpython-313.pyc differ diff --git a/ui/dialogs/__pycache__/calibration_type_dialog.cpython-313.pyc b/ui/dialogs/__pycache__/calibration_type_dialog.cpython-313.pyc index 764ab64..ecc1545 100644 Binary files a/ui/dialogs/__pycache__/calibration_type_dialog.cpython-313.pyc and b/ui/dialogs/__pycache__/calibration_type_dialog.cpython-313.pyc differ diff --git a/ui/dialogs/__pycache__/celestial_dialog.cpython-313.pyc b/ui/dialogs/__pycache__/celestial_dialog.cpython-313.pyc index d7018e0..d388a1a 100644 Binary files a/ui/dialogs/__pycache__/celestial_dialog.cpython-313.pyc and b/ui/dialogs/__pycache__/celestial_dialog.cpython-313.pyc differ diff --git a/ui/dialogs/__pycache__/equipment_dialog.cpython-313.pyc b/ui/dialogs/__pycache__/equipment_dialog.cpython-313.pyc index 4f0a79b..863cbb3 100644 Binary files a/ui/dialogs/__pycache__/equipment_dialog.cpython-313.pyc and b/ui/dialogs/__pycache__/equipment_dialog.cpython-313.pyc differ diff --git a/ui/dialogs/__pycache__/instructions_dialog.cpython-313.pyc b/ui/dialogs/__pycache__/instructions_dialog.cpython-313.pyc index cc50530..5837dfe 100644 Binary files a/ui/dialogs/__pycache__/instructions_dialog.cpython-313.pyc and b/ui/dialogs/__pycache__/instructions_dialog.cpython-313.pyc differ diff --git a/ui/dialogs/calibration_dialog.py b/ui/dialogs/calibration_dialog.py index adcf9fc..f7bcd2a 100644 --- a/ui/dialogs/calibration_dialog.py +++ b/ui/dialogs/calibration_dialog.py @@ -1,281 +1,180 @@ """ -CalibrationDialog - главный диалог калибровки -С выбором камеры, папки и типа кадров +CalibrationDialog - главный диалог калибровки (tkinter) """ -from PySide6.QtWidgets import ( - QDialog, QVBoxLayout, QHBoxLayout, QGridLayout, - QLabel, QComboBox, QLineEdit, QPushButton, QFrame, - QMessageBox, QFileDialog, QWidget -) -from PySide6.QtCore import Qt, QTimer -from PySide6.QtGui import QFont -from services.config_service import ConfigService +import tkinter as tk +from tkinter import ttk, messagebox, filedialog +from pathlib import Path -class CalibrationDialog(QDialog): +class CalibrationDialog(tk.Toplevel): """Главное окно калибровки""" - def __init__(self, parent, config_service: ConfigService): + def __init__(self, parent, config_service): super().__init__(parent) - + self.parent = parent self.config_service = config_service + self._blink_active = False + self._blink_after_id = None - self.setWindowTitle("🌑 Калибровочные кадры") - self.setMinimumSize(600, 450) - self.resize(650, 500) + self.title("Calibration Frames") + self.geometry("650x500") + self.minsize(600, 450) + self.transient(parent) + self.grab_set() self._create_ui() self._load_saved_settings() - - # Таймер для мигания кнопки "Обзор" - self._browse_blink_timer = None self._check_folder_path() + self._center_window() def _create_ui(self): - layout = QVBoxLayout(self) - layout.setSpacing(20) - layout.setContentsMargins(25, 25, 25, 25) + # Main frame + main_frame = ttk.Frame(self, padding="25") + main_frame.pack(fill='both', expand=True) - # Заголовок - title_label = QLabel("🌑 Калибровочные кадры") - title_font = QFont() - title_font.setPointSize(18) - title_font.setBold(True) - title_label.setFont(title_font) - layout.addWidget(title_label) + # Title + title_label = ttk.Label(main_frame, text="Calibration Frames", font=('Segoe UI', 18, 'bold')) + title_label.pack(pady=(0, 15)) - # Основная сетка - grid = QGridLayout() - grid.setVerticalSpacing(15) - grid.setHorizontalSpacing(15) + # Separator + ttk.Separator(main_frame, orient='horizontal').pack(fill='x', pady=(0, 15)) - # Строка 0: Камера - camera_label = QLabel("📷 Камера:") - camera_label.setFont(QFont("", 10, QFont.Bold)) - grid.addWidget(camera_label, 0, 0) + # Camera selection + camera_frame = ttk.Frame(main_frame) + camera_frame.pack(fill='x', pady=5) - self.camera_combo = QComboBox() - self.camera_combo.setEditable(True) - self.camera_combo.setMinimumWidth(250) - grid.addWidget(self.camera_combo, 0, 1) + ttk.Label(camera_frame, text="Camera:", font=('Segoe UI', 10, 'bold')).pack(side='left', padx=(0, 10)) - # Строка 1: Папка - folder_label = QLabel("📁 Папка:") - folder_label.setFont(QFont("", 10, QFont.Bold)) - grid.addWidget(folder_label, 1, 0) + self.camera_combo = ttk.Combobox(camera_frame, width=30) + self.camera_combo.pack(side='left', fill='x', expand=True) - folder_widget = QWidget() - folder_layout = QHBoxLayout(folder_widget) - folder_layout.setContentsMargins(0, 0, 0, 0) - folder_layout.setSpacing(10) + # Folder selection + folder_frame = ttk.Frame(main_frame) + folder_frame.pack(fill='x', pady=5) - self.folder_entry = QLineEdit() - self.folder_entry.setPlaceholderText("Выберите папку для сохранения калибровочных кадров") - folder_layout.addWidget(self.folder_entry) + ttk.Label(folder_frame, text="Folder:", font=('Segoe UI', 10, 'bold')).pack(side='left', padx=(0, 10)) - self.browse_button = QPushButton("✨ Обзор") - self.browse_button.setFixedWidth(100) - self.browse_button.clicked.connect(self._browse_folder) - folder_layout.addWidget(self.browse_button) + folder_input_frame = ttk.Frame(folder_frame) + folder_input_frame.pack(side='left', fill='x', expand=True) - grid.addWidget(folder_widget, 1, 1) + self.folder_entry = ttk.Entry(folder_input_frame) + self.folder_entry.pack(side='left', fill='x', expand=True, padx=(0, 10)) - layout.addLayout(grid) + self.browse_btn = tk.Button(folder_input_frame, text="Browse...", width=10, + bg='#3c3c3c', fg='#e0e0e0', activebackground='#4c4c4c', + relief='raised', borderwidth=1) + self.browse_btn.config(command=self._browse_folder) + self.browse_btn.pack(side='right') - # Разделитель - separator = QFrame() - separator.setFrameShape(QFrame.HLine) - separator.setStyleSheet("background-color: #333333; max-height: 1px;") - layout.addWidget(separator) + # Separator + ttk.Separator(main_frame, orient='horizontal').pack(fill='x', pady=15) - # Кнопки типов кадров - types_layout = QHBoxLayout() - types_layout.setSpacing(20) - types_layout.setAlignment(Qt.AlignCenter) + # Type buttons + types_frame = ttk.Frame(main_frame) + types_frame.pack(pady=10) - self.bias_btn = QPushButton("⚪ BIAS") - self.bias_btn.setFixedSize(120, 50) - self.bias_btn.setStyleSheet(""" - QPushButton { - background-color: #2196F3; - color: white; - font-weight: bold; - font-size: 14px; - border-radius: 6px; - } - QPushButton:hover { - background-color: #1976D2; - } - """) - self.bias_btn.clicked.connect(lambda: self._open_calibration_type('bias')) + self.bias_btn = tk.Button(types_frame, text="BIAS", font=('Segoe UI', 12, 'bold'), + bg='#2196F3', fg='white', activebackground='#1976D2', + width=12, height=2, + command=lambda: self._open_calibration_type('bias')) + self.bias_btn.pack(side='left', padx=10) - self.dark_btn = QPushButton("🌑 DARK") - self.dark_btn.setFixedSize(120, 50) - self.dark_btn.setStyleSheet(""" - QPushButton { - background-color: #9C27B0; - color: white; - font-weight: bold; - font-size: 14px; - border-radius: 6px; - } - QPushButton:hover { - background-color: #7B1FA2; - } - """) - self.dark_btn.clicked.connect(lambda: self._open_calibration_type('dark')) + self.dark_btn = tk.Button(types_frame, text="DARK", font=('Segoe UI', 12, 'bold'), + bg='#9C27B0', fg='white', activebackground='#7B1FA2', + width=12, height=2, + command=lambda: self._open_calibration_type('dark')) + self.dark_btn.pack(side='left', padx=10) - self.flat_btn = QPushButton("📖 FLAT") - self.flat_btn.setFixedSize(120, 50) - self.flat_btn.setStyleSheet(""" - QPushButton { - background-color: #4CAF50; - color: white; - font-weight: bold; - font-size: 14px; - border-radius: 6px; - } - QPushButton:hover { - background-color: #388E3C; - } - """) - self.flat_btn.clicked.connect(lambda: self._open_calibration_type('flat')) + self.flat_btn = tk.Button(types_frame, text="FLAT", font=('Segoe UI', 12, 'bold'), + bg='#4CAF50', fg='white', activebackground='#388E3C', + width=12, height=2, + command=lambda: self._open_calibration_type('flat')) + self.flat_btn.pack(side='left', padx=10) - types_layout.addWidget(self.bias_btn) - types_layout.addWidget(self.dark_btn) - types_layout.addWidget(self.flat_btn) + # Tips frame + tips_frame = tk.Frame(main_frame, bg='#2d2d2d', relief='groove', bd=1) + tips_frame.pack(fill='x', pady=15, padx=10) - layout.addLayout(types_layout) + tk.Label(tips_frame, text="Tips:", font=('Segoe UI', 10, 'bold'), + bg='#2d2d2d', fg='#FFD700').pack(anchor='w', padx=10, pady=(10, 5)) - # Совет - tips_frame = QFrame() - tips_frame.setStyleSheet(""" - QFrame { - background-color: #2d2d2d; - border-radius: 8px; - padding: 10px; - } - """) - tips_layout = QVBoxLayout(tips_frame) + self.tips_label = tk.Label(tips_frame, + text="• BIAS can be taken once a month (at home)\n• DARK must be taken on site at the same temperature\n• FLAT must be taken after session without changing focus", + bg='#2d2d2d', fg='#e0e0e0', justify='left', font=('Segoe UI', 9)) + self.tips_label.pack(anchor='w', padx=10, pady=(0, 10)) - tips_title = QLabel("💡 Совет") - tips_title.setFont(QFont("", 11, QFont.Bold)) - tips_layout.addWidget(tips_title) - - self.tips_label = QLabel( - "• BIAS снимаются один раз на месяц (можно дома)\n" - "• DARK снимаются на месте съёмки при той же температуре\n" - "• FLAT снимаются после сессии без изменения фокуса" - ) - self.tips_label.setWordWrap(True) - tips_layout.addWidget(self.tips_label) - - layout.addWidget(tips_frame) - - # Кнопки отмена/закрыть - buttons_layout = QHBoxLayout() - buttons_layout.addStretch() - - cancel_btn = QPushButton("❌ Отмена") - cancel_btn.clicked.connect(self.reject) - buttons_layout.addWidget(cancel_btn) - - layout.addLayout(buttons_layout) + # Cancel button + btn_frame = ttk.Frame(main_frame) + btn_frame.pack(pady=10) + ttk.Button(btn_frame, text="Cancel", command=self.destroy).pack() def _load_saved_settings(self): - """Загружает сохранённые камеры""" cameras = self.config_service.get_cameras() if cameras: - self.camera_combo.addItems(cameras) - + self.camera_combo['values'] = cameras last_camera = self.config_service.get_last_camera() if last_camera and last_camera in cameras: - self.camera_combo.setCurrentText(last_camera) + self.camera_combo.set(last_camera) def _browse_folder(self): - """Выбор папки для калибровочных кадров""" - folder = QFileDialog.getExistingDirectory(self, "Выберите папку для калибровочных кадров") + folder = filedialog.askdirectory(title="Select folder for calibration frames") if folder: - self.folder_entry.setText(folder) - self._stop_browse_blinking() + self.folder_entry.delete(0, tk.END) + self.folder_entry.insert(0, folder) + self._stop_blinking() + self.browse_btn.config(bg='#3c3c3c', fg='#e0e0e0') def _check_folder_path(self): - """Проверяет, заполнено ли поле пути и запускает мигание если нет""" - if not self.folder_entry.text(): - self._start_browse_blinking() + if not self.folder_entry.get(): + self._start_blinking() else: - self._stop_browse_blinking() + self._stop_blinking() - def _start_browse_blinking(self): - """Запускает мигание кнопки 'Обзор' зелёным цветом""" - self._browse_blink_timer = QTimer() - self._browse_blink_timer.timeout.connect(self._do_browse_blink) - self._browse_blink_timer.start(500) + def _start_blinking(self): + self._blink_active = True - def _do_browse_blink(self): - """Мигание кнопки""" - current_style = self.browse_button.styleSheet() - if "background-color: #4CAF50" in current_style: - self.browse_button.setStyleSheet(""" - QPushButton { - background-color: #2196F3; - color: white; - font-weight: bold; - border-radius: 4px; - } - """) - else: - self.browse_button.setStyleSheet(""" - QPushButton { - background-color: #4CAF50; - color: white; - font-weight: bold; - border-radius: 4px; - } - """) + def blink(): + if not self._blink_active: + return + if self.browse_btn.cget('bg') == '#3c3c3c': + self.browse_btn.config(bg='#f44336', fg='white') + else: + self.browse_btn.config(bg='#3c3c3c', fg='#e0e0e0') + self._blink_after_id = self.after(1500, blink) - def _stop_browse_blinking(self): - """Останавливает мигание кнопки""" - if self._browse_blink_timer: - self._browse_blink_timer.stop() - self._browse_blink_timer = None - self.browse_button.setStyleSheet(""" - QPushButton { - background-color: #2196F3; - color: white; - font-weight: bold; - border-radius: 4px; - } - """) + blink() - def _open_calibration_type(self, cal_type: str): - """Открывает дочернее окно для выбранного типа калибровки""" - if not self.folder_entry.text(): - QMessageBox.warning(self, "Внимание", "Сначала выберите папку для сохранения!") - self._start_browse_blinking() + def _stop_blinking(self): + self._blink_active = False + if self._blink_after_id: + self.after_cancel(self._blink_after_id) + self._blink_after_id = None + self.browse_btn.config(bg='#3c3c3c', fg='#e0e0e0') + + def _center_window(self): + self.update_idletasks() + x = self.parent.winfo_x() + (self.parent.winfo_width() // 2) - (self.winfo_width() // 2) + y = self.parent.winfo_y() + (self.parent.winfo_height() // 2) - (self.winfo_height() // 2) + self.geometry(f'+{x}+{y}') + + def _open_calibration_type(self, cal_type): + if not self.folder_entry.get(): + messagebox.showwarning("Warning", "Please select a folder to save calibration frames!", parent=self) + self._start_blinking() return - camera_name = self.camera_combo.currentText() + camera_name = self.camera_combo.get() if not camera_name: - QMessageBox.warning(self, "Внимание", "Введите или выберите название камеры!") + messagebox.showwarning("Warning", "Please enter or select a camera name!", parent=self) return from ui.dialogs.calibration_type_dialog import CalibrationTypeDialog dialog = CalibrationTypeDialog( self, cal_type, - self.folder_entry.text(), + self.folder_entry.get(), camera_name, self.config_service ) - - if dialog.exec(): - # После успешной съёмки - QMessageBox.information(self, "Успех", f"Съёмка {cal_type.upper()} завершена!") - - def reject(self): - """Закрытие диалога""" - if hasattr(self, '_browse_blink_timer') and self._browse_blink_timer: - self._browse_blink_timer.stop() - super().reject() \ No newline at end of file + self.wait_window(dialog) \ No newline at end of file diff --git a/ui/dialogs/calibration_type_dialog.py b/ui/dialogs/calibration_type_dialog.py index 1b3196a..d49be82 100644 --- a/ui/dialogs/calibration_type_dialog.py +++ b/ui/dialogs/calibration_type_dialog.py @@ -1,75 +1,56 @@ """ -CalibrationTypeDialog - диалог для конкретного типа калибровки -Dark / Bias / Flat с прогрессом, авто-остановкой и профилями +CalibrationTypeDialog - диалог для конкретного типа калибровки (tkinter) """ + +import tkinter as tk +from tkinter import ttk, messagebox import shutil -import os import re from pathlib import Path from datetime import datetime -from typing import Optional -from PySide6.QtWidgets import ( - QDialog, QVBoxLayout, QHBoxLayout, QGridLayout, - QLabel, QComboBox, QSpinBox, QPushButton, QFrame, - QProgressBar, QMessageBox, QGroupBox, - QInputDialog, QWidget, QFileDialog -) -from PySide6.QtCore import Qt, QTimer, QMetaObject, Q_ARG, Signal -from PySide6.QtGui import QFont - -from services.config_service import ConfigService from services.file_service import FileService from services.watch_service import WatchService -class CalibrationTypeDialog(QDialog): +class CalibrationTypeDialog(tk.Toplevel): """Диалог для съёмки калибровочных кадров определённого типа""" - # Сигнал для безопасного обновления UI из другого потока - progress_updated = Signal(int, int) # current, target - capture_completed = Signal(str) # target_folder - - def __init__(self, parent, cal_type: str, base_folder: str, - camera_name: str, config_service: ConfigService): + def __init__(self, parent, cal_type, base_folder, camera_name, config_service): super().__init__(parent) - - self.cal_type = cal_type # 'bias', 'dark', 'flat' + self.parent = parent + self.cal_type = cal_type self.base_folder = Path(base_folder) self.camera_name = camera_name self.config_service = config_service - # Состояние съёмки self.is_capturing = False self.current_count = 0 self.target_count = 0 - self._calibration_watch_service = None + self._watch_service = None - # Настройки для разных типов self.settings = self._get_default_settings() - self.setWindowTitle(self._get_title()) - self.setMinimumSize(550, 600) - self.resize(600, 650) - - # Подключаем сигналы - self.progress_updated.connect(self._on_progress_updated) - self.capture_completed.connect(self._on_capture_completed) + self.title(self._get_title()) + self.geometry("600x650") + self.minsize(550, 600) + self.transient(parent) + self.grab_set() self._create_ui() self._load_optics() self._update_recommendations() + self._center_window() - def _get_title(self) -> str: + def _get_title(self): titles = { - 'bias': '⚪ BIAS (Кадры смещения)', - 'dark': '🌑 DARK (Тёмные кадры)', - 'flat': '📖 FLAT (Плоские поля)' + 'bias': 'BIAS (Bias Frames)', + 'dark': 'DARK (Dark Frames)', + 'flat': 'FLAT (Flat Fields)' } - return titles.get(self.cal_type, 'Калибровочные кадры') + return titles.get(self.cal_type, 'Calibration Frames') - def _get_default_settings(self) -> dict: - """Возвращает настройки по умолчанию для типа калибровки""" + def _get_default_settings(self): base = { 'bias': { 'iso_values': [800, 1600, 3200], @@ -102,266 +83,152 @@ class CalibrationTypeDialog(QDialog): return base.get(self.cal_type, {}) def _create_ui(self): - layout = QVBoxLayout(self) - layout.setSpacing(15) - layout.setContentsMargins(20, 20, 20, 20) + # Main frame + main_frame = ttk.Frame(self, padding="20") + main_frame.pack(fill='both', expand=True) - # Заголовок с кнопкой справки - header_layout = QHBoxLayout() + # Title with help button + title_frame = ttk.Frame(main_frame) + title_frame.pack(fill='x', pady=(0, 10)) - title_label = QLabel(self._get_title()) - title_font = QFont() - title_font.setPointSize(16) - title_font.setBold(True) - title_label.setFont(title_font) - header_layout.addWidget(title_label) + ttk.Label(title_frame, text=self._get_title(), font=('Segoe UI', 16, 'bold')).pack(side='left') - help_btn = QPushButton("❓") - help_btn.setFixedSize(30, 30) - help_btn.setToolTip("Показать справку") - help_btn.clicked.connect(self._show_help) - header_layout.addWidget(help_btn) - header_layout.addStretch() + help_btn = ttk.Button(title_frame, text="?", width=3, command=self._show_help) + help_btn.pack(side='right') - layout.addLayout(header_layout) - - # Группа параметров - params_group = QGroupBox("⚙️ Параметры съёмки") - params_layout = QGridLayout(params_group) - params_layout.setVerticalSpacing(12) - params_layout.setHorizontalSpacing(15) - - row = 0 + # Parameters frame + params_frame = ttk.LabelFrame(main_frame, text="Camera Settings", padding="10") + params_frame.pack(fill='x', pady=10) # ISO - iso_label = QLabel("ISO:") - iso_label.setFont(QFont("", 10, QFont.Bold)) - params_layout.addWidget(iso_label, row, 0) + iso_row = ttk.Frame(params_frame) + iso_row.pack(fill='x', pady=5) + ttk.Label(iso_row, text="ISO:", width=15).pack(side='left') + self.iso_combo = ttk.Combobox(iso_row, values=self.settings['iso_values'], width=10) + self.iso_combo.set(str(self.settings['default_iso'])) + self.iso_combo.pack(side='left', padx=5) + self.iso_combo.bind('<>', lambda e: self._update_recommendations()) - self.iso_combo = QComboBox() - self.iso_combo.addItems([str(v) for v in self.settings['iso_values']]) - self.iso_combo.setCurrentText(str(self.settings['default_iso'])) - self.iso_combo.currentTextChanged.connect(self._update_recommendations) - params_layout.addWidget(self.iso_combo, row, 1) + ttk.Button(iso_row, text="Custom", width=8, command=self._add_custom_iso).pack(side='left', padx=5) - self.custom_iso_btn = QPushButton("➕ своё") - self.custom_iso_btn.setFixedWidth(60) - self.custom_iso_btn.clicked.connect(self._add_custom_iso) - params_layout.addWidget(self.custom_iso_btn, row, 2) - - row += 1 - - # Выдержка (только для DARK) + # Exposure (only for DARK) if self.cal_type == 'dark': - exposure_label = QLabel("Выдержка (сек):") - exposure_label.setFont(QFont("", 10, QFont.Bold)) - params_layout.addWidget(exposure_label, row, 0) + exp_row = ttk.Frame(params_frame) + exp_row.pack(fill='x', pady=5) + ttk.Label(exp_row, text="Exposure (sec):", width=15).pack(side='left') + self.exposure_combo = ttk.Combobox(exp_row, values=self.settings['exposure_values'], width=10) + self.exposure_combo.set(str(self.settings['default_exposure'])) + self.exposure_combo.pack(side='left', padx=5) + self.exposure_combo.bind('<>', lambda e: self._update_recommendations()) + ttk.Button(exp_row, text="Custom", width=8, command=self._add_custom_exposure).pack(side='left', padx=5) - self.exposure_combo = QComboBox() - self.exposure_combo.addItems([str(v) for v in self.settings['exposure_values']]) - self.exposure_combo.setCurrentText(str(self.settings['default_exposure'])) - self.exposure_combo.currentTextChanged.connect(self._update_recommendations) - params_layout.addWidget(self.exposure_combo, row, 1) - - self.custom_exposure_btn = QPushButton("➕ своё") - self.custom_exposure_btn.setFixedWidth(60) - self.custom_exposure_btn.clicked.connect(self._add_custom_exposure) - params_layout.addWidget(self.custom_exposure_btn, row, 2) - - row += 1 - - # Оптика (только для FLAT) + # Optics (only for FLAT) if self.cal_type == 'flat': - optics_label = QLabel("Оптика:") - optics_label.setFont(QFont("", 10, QFont.Bold)) - params_layout.addWidget(optics_label, row, 0) + optics_row = ttk.Frame(params_frame) + optics_row.pack(fill='x', pady=5) + ttk.Label(optics_row, text="Optics:", width=15).pack(side='left') + self.optics_combo = ttk.Combobox(optics_row, width=30) + self.optics_combo.pack(side='left', fill='x', expand=True, padx=5) - self.optics_combo = QComboBox() - self.optics_combo.setEditable(True) - params_layout.addWidget(self.optics_combo, row, 1, 1, 2) + aperture_row = ttk.Frame(params_frame) + aperture_row.pack(fill='x', pady=5) + ttk.Label(aperture_row, text="Aperture:", width=15).pack(side='left') + self.aperture_combo = ttk.Combobox(aperture_row, values=self.settings['aperture_values'], width=10) + self.aperture_combo.set('f/5.6') + self.aperture_combo.pack(side='left', padx=5) - row += 1 + ttk.Label(aperture_row, text="(Fixed for telescopes)", foreground='#888888').pack(side='left', padx=10) - aperture_label = QLabel("Диафрагма:") - aperture_label.setFont(QFont("", 10, QFont.Bold)) - params_layout.addWidget(aperture_label, row, 0) + # Count + count_row = ttk.Frame(params_frame) + count_row.pack(fill='x', pady=5) + ttk.Label(count_row, text="Number of frames:", width=15).pack(side='left') - self.aperture_combo = QComboBox() - self.aperture_combo.setEditable(True) - self.aperture_combo.addItems(self.settings['aperture_values']) - params_layout.addWidget(self.aperture_combo, row, 1, 1, 2) + self.count_spin = tk.Spinbox(count_row, from_=self.settings['min_count'], to=self.settings['max_count'], + width=8, font=('Segoe UI', 10)) + self.count_spin.delete(0, 'end') + self.count_spin.insert(0, str(self.settings['count'])) + self.count_spin.pack(side='left', padx=5) - row += 1 + ttk.Label(count_row, text=f"(recommended: {self.settings['recommended_count']})", foreground='#888888').pack(side='left', padx=5) - telescope_hint = QLabel("💡 Для телескопов диафрагма фиксированная и выбирается автоматически") - telescope_hint.setStyleSheet("color: #888888; font-size: 10px;") - params_layout.addWidget(telescope_hint, row, 0, 1, 3) + # Recommendations frame + tips_frame = tk.Frame(main_frame, bg='#2d2d2d', relief='groove', bd=1) + tips_frame.pack(fill='x', pady=10) - row += 1 + self.tips_text = tk.Text(tips_frame, height=12, wrap='word', bg='#2d2d2d', fg='#FFD700', + font=('Segoe UI', 9), relief='flat', padx=10, pady=10) + self.tips_text.pack(fill='both', expand=True) - # Количество кадров - count_label = QLabel("Количество кадров:") - count_label.setFont(QFont("", 10, QFont.Bold)) - params_layout.addWidget(count_label, row, 0) + # Progress frame + self.progress_frame = ttk.LabelFrame(main_frame, text="Progress", padding="10") - self.count_spin = QSpinBox() - self.count_spin.setMinimum(self.settings['min_count']) - self.count_spin.setMaximum(self.settings['max_count']) - self.count_spin.setValue(self.settings['count']) - self.count_spin.setSuffix(" кадров") - params_layout.addWidget(self.count_spin, row, 1) + self.progress_bar = ttk.Progressbar(self.progress_frame, orient='horizontal', length=400, mode='determinate') + self.progress_bar.pack(pady=5) - self.recommended_label = QLabel(f"(рекомендуется {self.settings['recommended_count']})") - self.recommended_label.setStyleSheet("color: #888888;") - params_layout.addWidget(self.recommended_label, row, 2) + self.progress_label = ttk.Label(self.progress_frame, text="Ready to shoot") + self.progress_label.pack() - layout.addWidget(params_group) + # Save path + save_frame = ttk.Frame(main_frame) + save_frame.pack(fill='x', pady=10) - # Группа рекомендаций - tips_group = QGroupBox("📖 Рекомендации") - tips_group.setStyleSheet(""" - QGroupBox { - font-weight: bold; - margin-top: 10px; - } - QGroupBox::title { - subcontrol-origin: margin; - left: 10px; - padding: 0 5px 0 5px; - } - """) - tips_layout = QVBoxLayout(tips_group) + ttk.Label(save_frame, text="Save to:", font=('Segoe UI', 9, 'bold')).pack(anchor='w') + self.save_path_label = ttk.Label(save_frame, foreground='#4CAF50') + self.save_path_label.pack(anchor='w') - self.tips_text = QLabel() - self.tips_text.setWordWrap(True) - self.tips_text.setStyleSheet("color: #FFD700; padding: 5px;") - tips_layout.addWidget(self.tips_text) + # Buttons + btn_frame = ttk.Frame(main_frame) + btn_frame.pack(pady=10) - layout.addWidget(tips_group) + self.back_btn = ttk.Button(btn_frame, text="Back", command=self._on_back) + self.back_btn.pack(side='left', padx=5) - # Группа прогресса - self.progress_group = QGroupBox("📊 Прогресс съёмки") - self.progress_group.setVisible(False) - progress_layout = QVBoxLayout(self.progress_group) + self.start_btn = ttk.Button(btn_frame, text="Start Shooting", command=self._start_capture, style='Green.TButton') + self.start_btn.pack(side='left', padx=5) - self.progress_bar = QProgressBar() - self.progress_bar.setMinimum(0) - progress_layout.addWidget(self.progress_bar) - - self.progress_status = QLabel("Готов к съёмке") - self.progress_status.setAlignment(Qt.AlignCenter) - progress_layout.addWidget(self.progress_status) - - layout.addWidget(self.progress_group) - - # Информация о сохранении - save_info = QFrame() - save_info.setStyleSheet(""" - QFrame { - background-color: #2d2d2d; - border-radius: 6px; - padding: 8px; - } - """) - save_layout = QVBoxLayout(save_info) - - save_label = QLabel("💾 Сохранить в:") - save_label.setFont(QFont("", 10, QFont.Bold)) - save_layout.addWidget(save_label) - - self.save_path_label = QLabel() - self.save_path_label.setWordWrap(True) - self.save_path_label.setStyleSheet("color: #4CAF50; font-family: monospace;") - save_layout.addWidget(self.save_path_label) - - layout.addWidget(save_info) - - # Кнопки действий - buttons_layout = QHBoxLayout() - buttons_layout.addStretch() - - self.back_btn = QPushButton("◀ Назад") - self.back_btn.clicked.connect(self._on_back_clicked) - buttons_layout.addWidget(self.back_btn) - - self.start_btn = QPushButton("▶ Начать съёмку") - self.start_btn.setStyleSheet(""" - QPushButton { - background-color: #4CAF50; - color: white; - font-weight: bold; - padding: 8px 20px; - border-radius: 4px; - } - QPushButton:hover { - background-color: #388E3C; - } - """) - self.start_btn.clicked.connect(self._start_capture) - buttons_layout.addWidget(self.start_btn) - - self.stop_btn = QPushButton("⏹️ Остановить") - self.stop_btn.setStyleSheet(""" - QPushButton { - background-color: #f44336; - color: white; - font-weight: bold; - padding: 8px 20px; - border-radius: 4px; - } - QPushButton:hover { - background-color: #d32f2f; - } - """) - self.stop_btn.clicked.connect(self._on_stop_clicked) - self.stop_btn.setVisible(False) - buttons_layout.addWidget(self.stop_btn) - - layout.addLayout(buttons_layout) + self.stop_btn = ttk.Button(btn_frame, text="Stop", command=self._on_stop, style='Red.TButton') + self.stop_btn.pack(side='left', padx=5) + self.stop_btn.config(state='disabled') self._update_save_path() def _load_optics(self): - """Загружает список оптики (объективы + телескопы) для FLAT режима""" if self.cal_type != 'flat': return lenses = self.config_service.get_lenses() telescopes = self.config_service.get_telescopes() - all_optics = [] - for lens in lenses: - all_optics.append(f"🔭 {lens}") - for telescope in telescopes: - all_optics.append(f"🪐 {telescope}") + all_optics = lenses + telescopes - self.optics_combo.addItems(all_optics) + self.optics_combo['values'] = all_optics + if all_optics: + self.optics_combo.set(all_optics[0]) - def on_optics_changed(): - current = self.optics_combo.currentText() - if current.startswith("🪐"): - self.aperture_combo.setEnabled(False) + def on_optics_change(*args): + current = self.optics_combo.get() + if 'f/' in current: + self.aperture_combo.config(state='disabled') match = re.search(r'f/(\d+\.?\d*)', current) if match: - self.aperture_combo.setCurrentText(f"f/{match.group(1)}") + self.aperture_combo.set(f"f/{match.group(1)}") else: - self.aperture_combo.setEnabled(True) + self.aperture_combo.config(state='normal') - if all_optics: - self.optics_combo.currentTextChanged.connect(on_optics_changed) + self.optics_combo.bind('<>', on_optics_change) def _update_save_path(self): - """Обновляет отображение пути сохранения""" - iso = int(self.iso_combo.currentText()) + iso = int(self.iso_combo.get()) if self.cal_type == 'bias': path = self.base_folder / "Calibration" / self.camera_name / "Bias" / f"ISO{iso}" elif self.cal_type == 'dark': - exposure = self.exposure_combo.currentText() + exposure = self.exposure_combo.get() path = self.base_folder / "Calibration" / self.camera_name / "Dark" / f"ISO{iso}_{exposure}s" elif self.cal_type == 'flat': - optics = self.optics_combo.currentText() - optics_name = optics.replace("🔭", "").replace("🪐", "").strip() + optics = self.optics_combo.get() + optics_name = optics invalid_chars = '<>:"/\\|?*' for char in invalid_chars: optics_name = optics_name.replace(char, '_') @@ -370,157 +237,127 @@ class CalibrationTypeDialog(QDialog): else: path = self.base_folder - self.save_path_label.setText(str(path)) + self.save_path_label.config(text=str(path)) return path def _update_recommendations(self): - """Обновляет рекомендации в зависимости от типа калибровки""" - if self.cal_type == 'bias': - self.tips_text.setText( - "⚪ BIAS (Кадры смещения)\n\n" - "📌 КАК СНИМАТЬ:\n" - "• Закройте крышку объектива\n" - "• Выдержка: САМАЯ КОРОТКАЯ (1/4000 или 1/8000)\n" - "• ISO: тот же, что при съёмке световых кадров\n\n" - "💡 СОВЕТ:\n" - "• Можно снять дома в любое время\n" - "• Используются для всех объективов\n" - "• 50 кадров оптимально для хорошего усреднения" - ) - elif self.cal_type == 'dark': - self.tips_text.setText( - "🌑 DARK (Тёмные кадры)\n\n" - "⚠️ ВАЖНО: Снимайте ПОСЛЕ сессии на месте!\n\n" - "📌 КАК СНИМАТЬ:\n" - "• Закройте крышку объектива\n" - "• ТЕ ЖЕ параметры ISO и выдержки, что при съёмке\n" - "• Дождитесь, пока камера прогреется до ночной температуры\n\n" - "🌡️ ТЕМПЕРАТУРА:\n" - "• Снимайте ПРИ ТОЙ ЖЕ температуре, что и Light кадры\n" - "• Разница >5°C делает кадры бесполезными!\n" - "• Лучше снять сразу после сессии, пока камера не остыла" - ) - elif self.cal_type == 'flat': - self.tips_text.setText( - "📖 FLAT (Плоские поля)\n\n" - "⚠️ ВАЖНО: НЕ меняйте фокус и зум после съёмки!\n\n" - "📌 КАК СНИМАТЬ:\n" - "• Способ 1: LED-планшет (рекомендуется)\n" - "• Способ 2: Рассвет/закат, камера в зенит\n" - "• Способ 3: Белая футболка на объектив\n\n" - "🎯 ЦЕЛЬ:\n" - "• Убрать виньетирование и пыль на оптике\n" - "• Гистограмма должна быть на 50-70%\n" - "• 30 кадров достаточно для хорошего результата" - ) + self.tips_text.config(state='normal') + self.tips_text.delete('1.0', 'end') + if self.cal_type == 'bias': + self.tips_text.insert('1.0', + "BIAS (Bias Frames)\n\n" + "HOW TO SHOOT:\n" + "• Close lens cap\n" + "• Shutter speed: FASTEST (1/4000 or 1/8000)\n" + "• ISO: same as light frames\n\n" + "TIPS:\n" + "• Can be taken at home anytime\n" + "• Works for all lenses/telescopes\n" + "• 50 frames recommended") + elif self.cal_type == 'dark': + self.tips_text.insert('1.0', + "DARK (Dark Frames)\n\n" + "IMPORTANT: Shoot AFTER session on site!\n\n" + "HOW TO SHOOT:\n" + "• Close lens cap\n" + "• SAME ISO and exposure as light frames\n" + "• Wait for camera to reach night temperature\n\n" + "TEMPERATURE:\n" + "• Shoot at the SAME temperature as light frames\n" + "• Difference >5C makes darks useless!\n" + "• Best taken immediately after session") + elif self.cal_type == 'flat': + self.tips_text.insert('1.0', + "FLAT (Flat Fields)\n\n" + "IMPORTANT: DON'T change focus or zoom!\n\n" + "HOW TO SHOOT:\n" + "• Method 1: LED panel (recommended)\n" + "• Method 2: Dawn/dusk sky, point at zenith\n" + "• Method 3: White T-shirt over lens\n\n" + "GOAL:\n" + "• Remove vignetting and dust\n" + "• Histogram at 50-70%\n" + "• 30 frames recommended") + + self.tips_text.config(state='disabled') self._update_save_path() def _start_capture(self): - """Начинает съёмку калибровочных кадров""" - self.target_count = self.count_spin.value() + self.target_count = int(self.count_spin.get()) self.current_count = 0 target_folder = self._update_save_path() - # Создаём папку с проверкой try: target_folder.mkdir(parents=True, exist_ok=True) - print(f"Папка создана: {target_folder}") except Exception as e: - QMessageBox.critical(self, "Ошибка", f"Не удалось создать папку:\n{target_folder}\n\nОшибка: {e}") + messagebox.showerror("Error", f"Failed to create folder:\n{target_folder}\n\n{str(e)}", parent=self) return - self.progress_group.setVisible(True) - self.progress_bar.setMaximum(self.target_count) - self.progress_bar.setValue(0) - self.progress_status.setText(f"0 из {self.target_count} кадров") + # Show progress frame + self.progress_frame.pack(fill='x', pady=10) + self.progress_bar['maximum'] = self.target_count + self.progress_bar['value'] = 0 + self.progress_label.config(text=f"0 / {self.target_count} frames") - # Меняем кнопки - self.start_btn.setVisible(False) - self.stop_btn.setVisible(True) - self.back_btn.setEnabled(False) - - # Блокируем изменение параметров - self.iso_combo.setEnabled(False) - self.count_spin.setEnabled(False) - if self.cal_type == 'dark': - self.exposure_combo.setEnabled(False) - if self.cal_type == 'flat': - self.optics_combo.setEnabled(False) - self.aperture_combo.setEnabled(False) + # Disable controls + self.start_btn.config(state='disabled') + self.stop_btn.config(state='normal') + self.back_btn.config(state='disabled') + self.iso_combo.config(state='disabled') + self.count_spin.config(state='disabled') + if hasattr(self, 'exposure_combo'): + self.exposure_combo.config(state='disabled') + if hasattr(self, 'optics_combo'): + self.optics_combo.config(state='disabled') + self.aperture_combo.config(state='disabled') self.is_capturing = True - # Получаем папку наблюдения + # Get watch folder from main window watch_folder = self._get_watch_folder() - print(f"Получена папка наблюдения: {watch_folder}") - if not watch_folder: - QMessageBox.critical(self, "Ошибка", - "Не удалось определить папку наблюдения!\nУбедитесь, что вы выбрали папку в главном окне.") + messagebox.showerror("Error", "Could not determine watch folder!\nPlease select a folder in the main window.", parent=self) self._stop_capture() return if not watch_folder.exists(): - QMessageBox.critical(self, "Ошибка", f"Папка наблюдения не существует:\n{watch_folder}") + messagebox.showerror("Error", f"Watch folder does not exist:\n{watch_folder}", parent=self) self._stop_capture() return - # Очищаем папку наблюдения от старых файлов FileService.clear_watch_folder(watch_folder) - # Создаём НОВЫЙ WatchService для калибровки - self._calibration_watch_service = WatchService() + self._watch_service = WatchService() - # Функция обратного вызова при получении файла (выполняется в потоке WatchService) - def on_file_received(file_path: Path): + def on_file_received(file_path): if not self.is_capturing: return - print(f"Обнаружен файл: {file_path}") if self._process_calibration_file(file_path, target_folder): - # Увеличиваем счётчик - new_count = self.current_count + 1 - # Отправляем сигнал для обновления UI в главном потоке - self.progress_updated.emit(new_count, self.target_count) + self.current_count += 1 + self.after(0, self._update_progress) - if new_count >= self.target_count: - # Отправляем сигнал о завершении - self.capture_completed.emit(str(target_folder)) - - print("Запуск WatchService для калибровки...") - success = self._calibration_watch_service.start(watch_folder, on_file_received) - print(f"Результат запуска: {success}") + if self.current_count >= self.target_count: + self.after(0, self._stop_capture) + self.after(0, lambda: messagebox.showinfo("Success", f"Capture completed!\nSaved {self.current_count} frames to:\n{target_folder}", parent=self)) + self.after(100, lambda: self.progress_frame.pack_forget()) + success = self._watch_service.start(watch_folder, on_file_received) if not success: - QMessageBox.critical(self, "Ошибка", "Не удалось запустить отслеживание папки!") + messagebox.showerror("Error", "Failed to start watching folder!", parent=self) self._stop_capture() return - self.progress_status.setText(f"Отслеживается папка: {watch_folder}\nОжидание новых файлов...") + self.progress_label.config(text=f"Watching: {watch_folder}\nWaiting for files...") - def _on_progress_updated(self, current: int, target: int): - """Обновляет прогресс (вызывается из основного потока по сигналу)""" - self.current_count = current - self.progress_bar.setValue(current) - self.progress_status.setText(f"Снято {current} из {target} кадров") - print(f"Прогресс: {current}/{target}") + def _update_progress(self): + self.progress_bar['value'] = self.current_count + self.progress_label.config(text=f"{self.current_count} / {self.target_count} frames") - def _on_capture_completed(self, target_folder: str): - """Обработчик завершения съёмки (вызывается из основного потока по сигналу)""" - if self.is_capturing: - self._stop_capture() - QMessageBox.information(self, "Успех", - f"✅ Съёмка завершена!\n" - f"Сохранено {self.current_count} кадров в:\n{target_folder}") - # Скрываем группу прогресса после сообщения - self.progress_group.setVisible(False) - - def _process_calibration_file(self, file_path: Path, target_folder: Path) -> bool: - """Обрабатывает файл из папки наблюдения""" + def _process_calibration_file(self, file_path, target_folder): if not FileService.is_photo(file_path): - print(f"Файл {file_path.name} не является фото, пропускаем") return False try: @@ -532,19 +369,19 @@ class CalibrationTypeDialog(QDialog): suffix = file_path.suffix if self.cal_type == 'bias': - iso = self.iso_combo.currentText() + iso = self.iso_combo.get() prefix = f"Bias_{self.camera_name}_ISO{iso}" elif self.cal_type == 'dark': - iso = self.iso_combo.currentText() - exposure = self.exposure_combo.currentText() + iso = self.iso_combo.get() + exposure = self.exposure_combo.get() prefix = f"Dark_{self.camera_name}_ISO{iso}_{exposure}s" elif self.cal_type == 'flat': - optics = self.optics_combo.currentText() - optics_name = optics.replace("🔭", "").replace("🪐", "").strip() + optics = self.optics_combo.get() + optics_name = optics invalid_chars = '<>:"/\\|?*' for char in invalid_chars: optics_name = optics_name.replace(char, '_') - aperture = self.aperture_combo.currentText() + aperture = self.aperture_combo.get() prefix = f"Flat_{optics_name}_{aperture}" else: prefix = "Calibration" @@ -557,174 +394,181 @@ class CalibrationTypeDialog(QDialog): target_path = FileService.resolve_conflict(target_path) shutil.move(str(file_path), str(target_path)) - print(f"Файл сохранён: {target_path}") return True except Exception as e: - print(f"Ошибка сохранения {file_path.name}: {e}") + print(f"Error saving {file_path.name}: {e}") return False def _stop_capture(self): - """Останавливает съёмку""" self.is_capturing = False - if self._calibration_watch_service: - self._calibration_watch_service.stop() - self._calibration_watch_service = None + if self._watch_service: + self._watch_service.stop() + self._watch_service = None - self.start_btn.setVisible(True) - self.stop_btn.setVisible(False) - self.back_btn.setEnabled(True) + self.start_btn.config(state='normal') + self.stop_btn.config(state='disabled') + self.back_btn.config(state='normal') + self.iso_combo.config(state='normal') + self.count_spin.config(state='normal') + if hasattr(self, 'exposure_combo'): + self.exposure_combo.config(state='normal') + if hasattr(self, 'optics_combo'): + self.optics_combo.config(state='normal') + self.aperture_combo.config(state='normal') - self.iso_combo.setEnabled(True) - self.count_spin.setEnabled(True) - if self.cal_type == 'dark': - self.exposure_combo.setEnabled(True) - if self.cal_type == 'flat': - self.optics_combo.setEnabled(True) - self.aperture_combo.setEnabled(True) + self.progress_label.config(text="Capture stopped") - self.progress_status.setText("Съёмка остановлена") - - # Скрываем группу прогресса через 2 секунды - QTimer.singleShot(2000, lambda: self.progress_group.setVisible(False)) - - def _on_back_clicked(self): - """Обработчик кнопки 'Назад'""" + def _on_back(self): if self.is_capturing: - QMessageBox.warning(self, "Внимание", "Сначала остановите съёмку!") + messagebox.showwarning("Warning", "Please stop the capture first!", parent=self) return - self.reject() + self.destroy() - def _on_stop_clicked(self): - """Обработчик кнопки 'Остановить' с подтверждением""" + def _on_stop(self): if self.current_count < self.target_count and self.current_count > 0: - reply = QMessageBox.question(self, "Прервать съёмку?", - f"Вы не закончили съёмку (снято {self.current_count} из {self.target_count} кадров).\n" - f"Вы действительно хотите прервать?", - QMessageBox.Yes | QMessageBox.No) - if reply == QMessageBox.Yes: + reply = messagebox.askyesno("Stop Capture", + f"You haven't finished capture ({self.current_count} of {self.target_count} frames).\n" + f"Are you sure you want to stop?", + parent=self) + if reply: self._stop_capture() - self.progress_group.setVisible(False) elif self.current_count == 0: - reply = QMessageBox.question(self, "Прервать съёмку?", - "Съёмка ещё не начата. Вы действительно хотите выйти?", - QMessageBox.Yes | QMessageBox.No) - if reply == QMessageBox.Yes: + reply = messagebox.askyesno("Stop Capture", + "Capture hasn't started yet. Are you sure?", + parent=self) + if reply: self._stop_capture() - self.progress_group.setVisible(False) else: self._stop_capture() - self.progress_group.setVisible(False) - - def _get_watch_folder(self) -> Optional[Path]: - """Возвращает папку наблюдения из главного окна""" - print("Поиск папки наблюдения...") - - parent = self.parent() - print(f"Родительское окно: {parent}") - - while parent and not hasattr(parent, 'folder_entry'): - parent = parent.parent() - print(f"Поднимаемся выше: {parent}") - - if parent and hasattr(parent, 'folder_entry'): - watch_folder = parent.folder_entry.text() - print(f"Нашли folder_entry, значение: {watch_folder}") - if watch_folder: - path = Path(watch_folder) - print(f"Папка наблюдения: {path}") - return path - - print("Не удалось найти папку наблюдения в родительском окне") - - folder = QFileDialog.getExistingDirectory(self, "Выберите папку наблюдения (куда камера сохраняет файлы)") - if folder: - print(f"Пользователь выбрал: {folder}") - return Path(folder) + def _get_watch_folder(self): + # Traverse to find main window's folder_entry + parent = self.parent + while parent: + if hasattr(parent, 'folder_entry'): + watch_folder = parent.folder_entry.get() + if watch_folder and watch_folder != "Select watch folder...": + return Path(watch_folder) + parent = parent.master if hasattr(parent, 'master') else parent.parent if hasattr(parent, 'parent') else None return None def _add_custom_iso(self): - custom_iso, ok = QInputDialog.getInt(self, "Свой ISO", - "Введите значение ISO:", 800, 100, 12800) - if ok and custom_iso: - iso_str = str(custom_iso) - if self.iso_combo.findText(iso_str) == -1: - self.iso_combo.addItem(iso_str) - self.iso_combo.setCurrentText(iso_str) + dialog = tk.Toplevel(self) + dialog.title("Custom ISO") + dialog.geometry("300x100") + dialog.transient(self) + dialog.grab_set() + + ttk.Label(dialog, text="Enter ISO value:").pack(pady=10) + entry = ttk.Entry(dialog) + entry.pack(pady=5) + entry.focus() + + def save(): + try: + value = int(entry.get()) + if 100 <= value <= 12800: + iso_str = str(value) + values = list(self.iso_combo['values']) + if iso_str not in values: + values.append(iso_str) + values.sort(key=int) + self.iso_combo['values'] = values + self.iso_combo.set(iso_str) + dialog.destroy() + else: + messagebox.showerror("Error", "ISO must be between 100 and 12800", parent=dialog) + except ValueError: + messagebox.showerror("Error", "Please enter a valid number", parent=dialog) + + ttk.Button(dialog, text="OK", command=save).pack(pady=10) + dialog.bind('', lambda e: save()) def _add_custom_exposure(self): - custom_exp, ok = QInputDialog.getInt(self, "Своя выдержка", - "Введите выдержку (секунд):", 120, 1, 3600) - if ok and custom_exp: - exp_str = str(custom_exp) - if self.exposure_combo.findText(exp_str) == -1: - self.exposure_combo.addItem(exp_str) - self.exposure_combo.setCurrentText(exp_str) + dialog = tk.Toplevel(self) + dialog.title("Custom Exposure") + dialog.geometry("300x100") + dialog.transient(self) + dialog.grab_set() + + ttk.Label(dialog, text="Enter exposure (seconds):").pack(pady=10) + entry = ttk.Entry(dialog) + entry.pack(pady=5) + entry.focus() + + def save(): + try: + value = int(entry.get()) + if 1 <= value <= 3600: + exp_str = str(value) + values = list(self.exposure_combo['values']) + if exp_str not in values: + values.append(exp_str) + values.sort(key=int) + self.exposure_combo['values'] = values + self.exposure_combo.set(exp_str) + dialog.destroy() + else: + messagebox.showerror("Error", "Exposure must be between 1 and 3600 seconds", parent=dialog) + except ValueError: + messagebox.showerror("Error", "Please enter a valid number", parent=dialog) + + ttk.Button(dialog, text="OK", command=save).pack(pady=10) + dialog.bind('', lambda e: save()) def _show_help(self): if self.cal_type == 'bias': help_text = ( - "Что такое BIAS?\n\n" - "Bias (кадры смещения) — это снимки с закрытой крышкой\n" - "на минимально возможной выдержке.\n\n" - "Зачем нужны:\n" - "• Убирают read noise (шум считывания)\n" - "• Корректируют смещение чёрного уровня\n\n" - "Сколько снимать:\n" - "• 50 кадров для хорошего усреднения\n" - "• Можно использовать весь месяц\n\n" - "Когда снимать:\n" - "• Дома в любое время\n" - "• Температура не важна" + "What are BIAS frames?\n\n" + "Bias frames are shots with the lens cap closed\n" + "at the shortest possible shutter speed.\n\n" + "Why they are needed:\n" + "• Remove read noise\n" + "• Correct black level offset\n\n" + "How many to take:\n" + "• 50 frames for good averaging\n" + "• Can be used for a whole month\n\n" + "When to take:\n" + "• At home anytime\n" + "• Temperature doesn't matter" ) elif self.cal_type == 'dark': help_text = ( - "Что такое DARK?\n\n" - "Dark (тёмные кадры) — это снимки с закрытой крышкой\n" - "с ТЕМИ ЖЕ параметрами ISO и выдержки, что и световые кадры.\n\n" - "Зачем нужны:\n" - "• Убирают тепловой шум матрицы\n" - "• Убирают горячие пиксели\n\n" - "Сколько снимать:\n" - "• 20-30 кадров для хорошего результата\n\n" - "⚠️ ВАЖНО про температуру:\n" - "• Снимайте ПОСЛЕ сессии на месте!\n" - "• Камера должна быть при той же температуре\n" - "• Разница >5°C делает кадры бесполезными!" + "What are DARK frames?\n\n" + "Dark frames are shots with the lens cap closed\n" + "with the SAME ISO and exposure as light frames.\n\n" + "Why they are needed:\n" + "• Remove thermal noise\n" + "• Remove hot pixels\n\n" + "IMPORTANT about temperature:\n" + "• Take AFTER the session on site!\n" + "• Camera must be at the same temperature\n" + "• Difference >5C makes darks useless!" ) else: help_text = ( - "Что такое FLAT?\n\n" - "Flat (плоские поля) — это снимки равномерно освещённой\n" - "поверхности с ТЕМИ ЖЕ фокусом и зумом.\n\n" - "Зачем нужны:\n" - "• Убирают виньетирование объектива\n" - "• Убирают пыль на матрице и оптике\n\n" - "Как снимать:\n" - "1. LED-планшет (лучший вариант)\n" - "2. Рассвет/закат, камера в зенит\n" - "3. Белая футболка на объектив\n\n" - "Сколько снимать:\n" - "• 30 кадров для хорошего усреднения\n\n" - "⚠️ ВАЖНО:\n" - "• НЕ меняйте фокус!\n" - "• НЕ меняйте зум!\n" - "• Снимайте в конце сессии" + "What are FLAT frames?\n\n" + "Flat frames are shots of an evenly lit surface\n" + "with the SAME focus and zoom.\n\n" + "Why they are needed:\n" + "• Remove lens vignetting\n" + "• Remove dust on sensor/lens\n\n" + "How to shoot:\n" + "1. LED panel (best option)\n" + "2. Dawn/dusk sky, point at zenith\n" + "3. White T-shirt over lens\n\n" + "IMPORTANT:\n" + "• DON'T change focus!\n" + "• DON'T change zoom!\n" + "• Take at the end of the session" ) - QMessageBox.information(self, "Справка", help_text) + messagebox.showinfo("Help", help_text, parent=self) - def closeEvent(self, event): - if self.is_capturing: - reply = QMessageBox.question(self, "Прервать съёмку?", - "Съёмка активна. Вы действительно хотите закрыть окно?", - QMessageBox.Yes | QMessageBox.No) - if reply == QMessageBox.Yes: - self._stop_capture() - event.accept() - else: - event.ignore() - else: - event.accept() \ No newline at end of file + def _center_window(self): + self.update_idletasks() + x = self.parent.winfo_x() + (self.parent.winfo_width() // 2) - (self.winfo_width() // 2) + y = self.parent.winfo_y() + (self.parent.winfo_height() // 2) - (self.winfo_height() // 2) + self.geometry(f'+{x}+{y}') \ No newline at end of file diff --git a/ui/dialogs/celestial_dialog.py b/ui/dialogs/celestial_dialog.py index 4798cf5..d9f120f 100644 --- a/ui/dialogs/celestial_dialog.py +++ b/ui/dialogs/celestial_dialog.py @@ -1,160 +1,153 @@ """ -CelestialDialog - диалог управления небесными телами -Аналог CelestialBodiesDialogController из JavaFX версии +CelestialDialog - диалог управления небесными телами (tkinter) """ -from PySide6.QtWidgets import ( - QDialog, QVBoxLayout, QHBoxLayout, QLabel, QListWidget, - QPushButton, QLineEdit, QInputDialog, QMessageBox -) -from PySide6.QtCore import Qt -from PySide6.QtGui import QFont -from services.config_service import ConfigService +import tkinter as tk +from tkinter import ttk, messagebox -class CelestialDialog(QDialog): +class CelestialDialog(tk.Toplevel): """Диалог для управления списком небесных тел""" - def __init__(self, parent, config_service: ConfigService): + def __init__(self, parent, config_service): super().__init__(parent) - + self.parent = parent self.config_service = config_service - self.setWindowTitle("Небесные тела") - self.setMinimumSize(400, 500) - self.resize(450, 550) - - # Загружаем текущий список self.celestial_bodies = self.config_service.get_celestial_bodies() + self.selected_body = None + + self.title("Celestial Bodies") + self.geometry("500x550") + self.minsize(450, 500) + self.transient(parent) + self.grab_set() self._create_ui() - self._update_list() + self._center_window() def _create_ui(self): - """Создаёт интерфейс диалога""" - layout = QVBoxLayout(self) - layout.setSpacing(15) - layout.setContentsMargins(20, 20, 20, 20) + # Main frame + main_frame = ttk.Frame(self, padding="20") + main_frame.pack(fill='both', expand=True) - # Заголовок - title_label = QLabel("Управление небесными телами") - title_font = QFont() - title_font.setPointSize(16) - title_font.setBold(True) - title_label.setFont(title_font) - title_label.setAlignment(Qt.AlignCenter) - layout.addWidget(title_label) + # Title + ttk.Label(main_frame, text="Celestial Bodies", font=('Segoe UI', 14, 'bold')).pack(pady=(0, 10)) + ttk.Label(main_frame, text="List of observation targets", font=('Segoe UI', 10)).pack(pady=(0, 15)) - # Подпись - subtitle_label = QLabel("Список объектов для наблюдения") - subtitle_font = QFont() - subtitle_font.setPointSize(11) - subtitle_font.setBold(True) - subtitle_label.setFont(subtitle_font) - layout.addWidget(subtitle_label) + # Listbox with scrollbar + list_frame = ttk.Frame(main_frame) + list_frame.pack(fill='both', expand=True, pady=(0, 10)) - # Список небесных тел - self.bodies_list = QListWidget() - self.bodies_list.itemClicked.connect(lambda item: self._select_body(item.text())) - layout.addWidget(self.bodies_list) + scrollbar = ttk.Scrollbar(list_frame) + scrollbar.pack(side='right', fill='y') - # Поле для добавления нового - add_layout = QHBoxLayout() + self.bodies_listbox = tk.Listbox(list_frame, yscrollcommand=scrollbar.set, height=15, + bg='#2d2d2d', fg='#e0e0e0', + selectbackground='#4CAF50', selectforeground='white', + font=('Segoe UI', 10)) + self.bodies_listbox.pack(fill='both', expand=True) + scrollbar.config(command=self.bodies_listbox.yview) - self.new_body_entry = QLineEdit() - self.new_body_entry.setPlaceholderText("Название объекта (например: M31, NGC 224)") - self.new_body_entry.returnPressed.connect(self._add_celestial_body) - add_layout.addWidget(self.new_body_entry) - - add_btn = QPushButton("➕ Добавить") - add_btn.clicked.connect(self._add_celestial_body) - add_layout.addWidget(add_btn) - - layout.addLayout(add_layout) - - # Кнопки удаления и редактирования - buttons_layout = QHBoxLayout() - - self.remove_btn = QPushButton("❌ Удалить выбранный") - self.remove_btn.setEnabled(False) - self.remove_btn.clicked.connect(self._remove_celestial_body) - buttons_layout.addWidget(self.remove_btn) - - self.edit_btn = QPushButton("✏ Редактировать") - self.edit_btn.setEnabled(False) - self.edit_btn.clicked.connect(self._edit_celestial_body) - buttons_layout.addWidget(self.edit_btn) - - layout.addLayout(buttons_layout) - - # Кнопка закрытия - close_btn = QPushButton("Закрыть") - close_btn.clicked.connect(self.accept) - close_layout = QHBoxLayout() - close_layout.addStretch() - close_layout.addWidget(close_btn) - layout.addLayout(close_layout) - - def _update_list(self): - """Обновляет отображение списка небесных тел""" - self.bodies_list.clear() for body in self.celestial_bodies: - self.bodies_list.addItem(body) - self._selected_body = None - self.remove_btn.setEnabled(False) - self.edit_btn.setEnabled(False) + self.bodies_listbox.insert('end', body) - def _select_body(self, body: str): - """Выделяет объект в списке""" - self._selected_body = body - self.remove_btn.setEnabled(True) - self.edit_btn.setEnabled(True) + self.bodies_listbox.bind('<>', self._on_body_select) + + # Add new body + add_frame = ttk.Frame(main_frame) + add_frame.pack(fill='x', pady=(0, 10)) + + self.new_body_entry = ttk.Entry(add_frame) + self.new_body_entry.pack(side='left', fill='x', expand=True, padx=(0, 10)) + ttk.Button(add_frame, text="Add", command=self._add_celestial_body).pack(side='right') + + # Buttons + btn_frame = ttk.Frame(main_frame) + btn_frame.pack(pady=(0, 10)) + + self.remove_btn = ttk.Button(btn_frame, text="Remove Selected", command=self._remove_celestial_body, state='disabled') + self.remove_btn.pack(side='left', padx=5) + + self.edit_btn = ttk.Button(btn_frame, text="Edit Selected", command=self._edit_celestial_body, state='disabled') + self.edit_btn.pack(side='left', padx=5) + + # Close button + ttk.Button(main_frame, text="Close", command=self.destroy).pack(pady=10) + + def _center_window(self): + self.update_idletasks() + x = self.parent.winfo_x() + (self.parent.winfo_width() // 2) - (self.winfo_width() // 2) + y = self.parent.winfo_y() + (self.parent.winfo_height() // 2) - (self.winfo_height() // 2) + self.geometry(f'+{x}+{y}') + + def _on_body_select(self, event): + selection = self.bodies_listbox.curselection() + if selection: + self.selected_body = self.bodies_listbox.get(selection[0]) + self.remove_btn.config(state='normal') + self.edit_btn.config(state='normal') def _add_celestial_body(self): - """Добавляет новое небесное тело""" - new_body = self.new_body_entry.text() - if not new_body or not new_body.strip(): - QMessageBox.warning(self, "Ошибка", "Введите название объекта") + new_body = self.new_body_entry.get().strip() + if not new_body: + messagebox.showwarning("Warning", "Please enter object name!", parent=self) return - new_name = new_body.strip() - if new_name in self.celestial_bodies: - QMessageBox.warning(self, "Ошибка", f"Объект '{new_name}' уже существует!") + if new_body in self.celestial_bodies: + messagebox.showwarning("Warning", f"Object '{new_body}' already exists!", parent=self) return - self.celestial_bodies.append(new_name) - self.config_service.add_celestial_body(new_name) - self._update_list() - self.new_body_entry.clear() - QMessageBox.information(self, "Успех", f"Объект '{new_name}' добавлен") + self.celestial_bodies.append(new_body) + self.config_service.add_celestial_body(new_body) + self.bodies_listbox.insert('end', new_body) + self.new_body_entry.delete(0, 'end') + messagebox.showinfo("Success", f"Object '{new_body}' added", parent=self) def _remove_celestial_body(self): - """Удаляет выбранное небесное тело""" - if hasattr(self, '_selected_body') and self._selected_body: - reply = QMessageBox.question(self, "Подтверждение", - f"Удалить объект '{self._selected_body}'?", - QMessageBox.Yes | QMessageBox.No) - if reply == QMessageBox.Yes: - self.celestial_bodies.remove(self._selected_body) - self.config_service.remove_celestial_body(self._selected_body) - self._update_list() - QMessageBox.information(self, "Успех", f"Объект '{self._selected_body}' удалён") + if self.selected_body: + reply = messagebox.askyesno("Remove Object", f"Remove '{self.selected_body}'?", parent=self) + if reply: + self.celestial_bodies.remove(self.selected_body) + self.config_service.remove_celestial_body(self.selected_body) + self.bodies_listbox.delete(0, 'end') + for body in self.celestial_bodies: + self.bodies_listbox.insert('end', body) + self.selected_body = None + self.remove_btn.config(state='disabled') + self.edit_btn.config(state='disabled') def _edit_celestial_body(self): - """Редактирует выбранное небесное тело""" - if hasattr(self, '_selected_body') and self._selected_body: - new_name, ok = QInputDialog.getText(self, "Редактировать", - f"Изменить '{self._selected_body}' на:", - text=self._selected_body) - if ok and new_name and new_name.strip(): - new_name = new_name.strip() - if new_name != self._selected_body: + if self.selected_body: + dialog = tk.Toplevel(self) + dialog.title("Edit Celestial Body") + dialog.geometry("350x120") + dialog.transient(self) + dialog.grab_set() + + ttk.Label(dialog, text="New name:", font=('Segoe UI', 10)).pack(pady=15) + entry = ttk.Entry(dialog, width=40) + entry.insert(0, self.selected_body) + entry.pack(pady=5) + entry.focus() + + def save(): + new_name = entry.get().strip() + if new_name and new_name != self.selected_body: if new_name in self.celestial_bodies: - QMessageBox.warning(self, "Ошибка", f"Объект '{new_name}' уже существует!") + messagebox.showerror("Error", f"Object '{new_name}' already exists!", parent=dialog) return - idx = self.celestial_bodies.index(self._selected_body) - old_name = self.celestial_bodies[idx] + idx = self.celestial_bodies.index(self.selected_body) self.celestial_bodies[idx] = new_name - self.config_service.update_celestial_body(old_name, new_name) - self._update_list() - QMessageBox.information(self, "Успех", f"Объект переименован в '{new_name}'") \ No newline at end of file + self.config_service.update_celestial_body(self.selected_body, new_name) + self.bodies_listbox.delete(0, 'end') + for body in self.celestial_bodies: + self.bodies_listbox.insert('end', body) + dialog.destroy() + elif new_name == self.selected_body: + dialog.destroy() + else: + messagebox.showwarning("Warning", "Please enter a name!", parent=dialog) + + ttk.Button(dialog, text="Save", command=save).pack(pady=10) + dialog.bind('', lambda e: save()) \ No newline at end of file diff --git a/ui/dialogs/equipment_dialog.py b/ui/dialogs/equipment_dialog.py index 0508566..655cdd3 100644 --- a/ui/dialogs/equipment_dialog.py +++ b/ui/dialogs/equipment_dialog.py @@ -1,361 +1,469 @@ """ -EquipmentDialog - диалог управления оборудованием -Камеры, объективы и телескопы +EquipmentDialog - диалог управления оборудованием (tkinter) """ -from PySide6.QtWidgets import ( - QDialog, QVBoxLayout, QHBoxLayout, QLabel, QListWidget, - QPushButton, QInputDialog, QMessageBox, QWidget, QTabWidget, - QFormLayout, QDoubleSpinBox, QSpinBox, QLineEdit -) -from PySide6.QtCore import Qt -from PySide6.QtGui import QFont -from services.config_service import ConfigService +import tkinter as tk +from tkinter import ttk, messagebox +from threading import Thread -class EquipmentDialog(QDialog): +class EquipmentDialog(tk.Toplevel): """Диалог для управления оборудованием""" - def __init__(self, parent, config_service: ConfigService): + def __init__(self, parent, config_service): super().__init__(parent) - + self.parent = parent self.config_service = config_service - self.setWindowTitle("Управление оборудованием") - self.setMinimumSize(700, 500) - self.resize(800, 550) - # Загружаем данные self.cameras = self.config_service.get_cameras() self.lenses = self.config_service.get_lenses() self.telescopes = self.config_service.get_telescopes() + self.selected_camera = None + self.selected_lens = None + self.selected_telescope = None + + self.title("Manage Equipment") + self.geometry("750x500") + self.minsize(700, 450) + self.transient(parent) + self.grab_set() + self._create_ui() - self._update_cameras_list() - self._update_lenses_list() - self._update_telescopes_list() + self._center_window() def _create_ui(self): - layout = QVBoxLayout(self) - layout.setSpacing(15) - layout.setContentsMargins(20, 20, 20, 20) + # Notebook for tabs + notebook = ttk.Notebook(self) + notebook.pack(fill='both', expand=True, padx=10, pady=10) - # Заголовок - title_label = QLabel("Управление оборудованием") - title_font = QFont() - title_font.setPointSize(16) - title_font.setBold(True) - title_label.setFont(title_font) - title_label.setAlignment(Qt.AlignCenter) - layout.addWidget(title_label) + # Tab 1: Cameras + cameras_frame = ttk.Frame(notebook) + notebook.add(cameras_frame, text="Cameras") + self._create_cameras_tab(cameras_frame) - # Используем QTabWidget для трёх вкладок - tab_widget = QTabWidget() + # Tab 2: Lenses + lenses_frame = ttk.Frame(notebook) + notebook.add(lenses_frame, text="Lenses") + self._create_lenses_tab(lenses_frame) - # Вкладка: Камеры - cameras_tab = self._create_cameras_tab() - tab_widget.addTab(cameras_tab, "📷 Камеры") + # Tab 3: Telescopes + telescopes_frame = ttk.Frame(notebook) + notebook.add(telescopes_frame, text="Telescopes") + self._create_telescopes_tab(telescopes_frame) - # Вкладка: Объективы - lenses_tab = self._create_lenses_tab() - tab_widget.addTab(lenses_tab, "🔭 Объективы") + # Close button + btn_frame = ttk.Frame(self) + btn_frame.pack(pady=10) + ttk.Button(btn_frame, text="Close", command=self.destroy).pack() - # Вкладка: Телескопы - telescopes_tab = self._create_telescopes_tab() - tab_widget.addTab(telescopes_tab, "🪐 Телескопы") + def _center_window(self): + self.update_idletasks() + x = self.parent.winfo_x() + (self.parent.winfo_width() // 2) - (self.winfo_width() // 2) + y = self.parent.winfo_y() + (self.parent.winfo_height() // 2) - (self.winfo_height() // 2) + self.geometry(f'+{x}+{y}') - layout.addWidget(tab_widget) + def _create_cameras_tab(self, parent): + # Listbox + list_frame = ttk.Frame(parent) + list_frame.pack(fill='both', expand=True, padx=10, pady=10) - # Кнопка закрытия - close_btn = QPushButton("Закрыть") - close_btn.clicked.connect(self.accept) - close_layout = QHBoxLayout() - close_layout.addStretch() - close_layout.addWidget(close_btn) - layout.addLayout(close_layout) + scrollbar = ttk.Scrollbar(list_frame) + scrollbar.pack(side='right', fill='y') - def _create_cameras_tab(self) -> QWidget: - """Создаёт вкладку с камерами""" - tab = QWidget() - layout = QVBoxLayout(tab) + self.cameras_listbox = tk.Listbox(list_frame, yscrollcommand=scrollbar.set, + bg='#2d2d2d', fg='#e0e0e0', + selectbackground='#4CAF50', selectforeground='white', + font=('Segoe UI', 10)) + self.cameras_listbox.pack(fill='both', expand=True) + scrollbar.config(command=self.cameras_listbox.yview) - # Список камер - self.cameras_list = QListWidget() - self.cameras_list.itemClicked.connect(lambda item: self._select_camera(item.text())) - layout.addWidget(self.cameras_list) - - # Кнопки - buttons_layout = QHBoxLayout() - - add_btn = QPushButton("➕ Добавить камеру") - add_btn.clicked.connect(self._add_camera) - buttons_layout.addWidget(add_btn) - - self.remove_camera_btn = QPushButton("❌ Удалить") - self.remove_camera_btn.setEnabled(False) - self.remove_camera_btn.clicked.connect(self._remove_camera) - buttons_layout.addWidget(self.remove_camera_btn) - - layout.addLayout(buttons_layout) - - return tab - - def _create_lenses_tab(self) -> QWidget: - """Создаёт вкладку с объективами""" - tab = QWidget() - layout = QVBoxLayout(tab) - - # Список объективов - self.lenses_list = QListWidget() - self.lenses_list.itemClicked.connect(lambda item: self._select_lens(item.text())) - layout.addWidget(self.lenses_list) - - # Кнопки - buttons_layout = QHBoxLayout() - - add_btn = QPushButton("➕ Добавить объектив") - add_btn.clicked.connect(self._add_lens) - buttons_layout.addWidget(add_btn) - - self.remove_lens_btn = QPushButton("❌ Удалить") - self.remove_lens_btn.setEnabled(False) - self.remove_lens_btn.clicked.connect(self._remove_lens) - buttons_layout.addWidget(self.remove_lens_btn) - - edit_btn = QPushButton("✏ Редактировать") - edit_btn.clicked.connect(self._edit_lens) - buttons_layout.addWidget(edit_btn) - - layout.addLayout(buttons_layout) - - return tab - - def _create_telescopes_tab(self) -> QWidget: - """Создаёт вкладку с телескопами""" - tab = QWidget() - layout = QVBoxLayout(tab) - - # Список телескопов - self.telescopes_list = QListWidget() - self.telescopes_list.itemClicked.connect(lambda item: self._select_telescope(item.text())) - layout.addWidget(self.telescopes_list) - - # Кнопки - buttons_layout = QHBoxLayout() - - add_btn = QPushButton("➕ Добавить телескоп") - add_btn.clicked.connect(self._add_telescope) - buttons_layout.addWidget(add_btn) - - self.remove_telescope_btn = QPushButton("❌ Удалить") - self.remove_telescope_btn.setEnabled(False) - self.remove_telescope_btn.clicked.connect(self._remove_telescope) - buttons_layout.addWidget(self.remove_telescope_btn) - - edit_btn = QPushButton("✏ Редактировать") - edit_btn.clicked.connect(self._edit_telescope) - buttons_layout.addWidget(edit_btn) - - layout.addLayout(buttons_layout) - - return tab - - # ===== Методы для камер ===== - - def _update_cameras_list(self): - self.cameras_list.clear() for camera in self.cameras: - self.cameras_list.addItem(camera) - self._selected_camera = None - self.remove_camera_btn.setEnabled(False) + self.cameras_listbox.insert('end', camera) - def _select_camera(self, camera: str): - self._selected_camera = camera - self.remove_camera_btn.setEnabled(True) - self.lenses_list.clearSelection() - self.telescopes_list.clearSelection() - self._selected_lens = None - self._selected_telescope = None - self.remove_lens_btn.setEnabled(False) - self.remove_telescope_btn.setEnabled(False) + self.cameras_listbox.bind('<>', self._on_camera_select) + + # Buttons + btn_frame = ttk.Frame(parent) + btn_frame.pack(pady=10) + + ttk.Button(btn_frame, text="Add Camera", command=self._add_camera).pack(side='left', padx=5) + self.remove_camera_btn = ttk.Button(btn_frame, text="Remove", command=self._remove_camera, state='disabled') + self.remove_camera_btn.pack(side='left', padx=5) + self.edit_camera_btn = ttk.Button(btn_frame, text="Edit", command=self._edit_camera, state='disabled') + self.edit_camera_btn.pack(side='left', padx=5) + + def _on_camera_select(self, event): + selection = self.cameras_listbox.curselection() + if selection: + self.selected_camera = self.cameras_listbox.get(selection[0]) + self.remove_camera_btn.config(state='normal') + self.edit_camera_btn.config(state='normal') def _add_camera(self): - name, ok = QInputDialog.getText(self, "Добавить камеру", "Введите название камеры:") - if ok and name and name.strip(): - new_name = name.strip() - if new_name in self.cameras: - QMessageBox.warning(self, "Ошибка", "Такая камера уже существует!") - return - self.cameras.append(new_name) - self.config_service.add_camera(new_name) - self._update_cameras_list() - QMessageBox.information(self, "Успех", f"Камера '{new_name}' добавлена") + dialog = tk.Toplevel(self) + dialog.title("Add Camera") + dialog.geometry("350x120") + dialog.transient(self) + dialog.grab_set() + + ttk.Label(dialog, text="Camera name:", font=('Segoe UI', 10)).pack(pady=15) + entry = ttk.Entry(dialog, width=40) + entry.pack(pady=5) + entry.focus() + + def save(): + new_camera = entry.get().strip() + if new_camera: + if new_camera in self.cameras: + messagebox.showerror("Error", "Camera already exists!", parent=dialog) + return + self.cameras.append(new_camera) + self.config_service.add_camera(new_camera) + self.cameras_listbox.insert('end', new_camera) + dialog.destroy() + else: + messagebox.showwarning("Warning", "Please enter camera name!", parent=dialog) + + ttk.Button(dialog, text="OK", command=save).pack(pady=10) + dialog.bind('', lambda e: save()) def _remove_camera(self): - if hasattr(self, '_selected_camera') and self._selected_camera: - reply = QMessageBox.question(self, "Подтверждение", - f"Удалить камеру '{self._selected_camera}'?", - QMessageBox.Yes | QMessageBox.No) - if reply == QMessageBox.Yes: - self.cameras.remove(self._selected_camera) - self.config_service.remove_camera(self._selected_camera) - self._update_cameras_list() + if self.selected_camera: + reply = messagebox.askyesno("Remove Camera", f"Remove '{self.selected_camera}'?", parent=self) + if reply: + self.cameras.remove(self.selected_camera) + self.config_service.remove_camera(self.selected_camera) + self.cameras_listbox.delete(0, 'end') + for camera in self.cameras: + self.cameras_listbox.insert('end', camera) + self.selected_camera = None + self.remove_camera_btn.config(state='disabled') + self.edit_camera_btn.config(state='disabled') - # ===== Методы для объективов ===== + def _edit_camera(self): + if self.selected_camera: + dialog = tk.Toplevel(self) + dialog.title("Edit Camera") + dialog.geometry("350x120") + dialog.transient(self) + dialog.grab_set() + + ttk.Label(dialog, text="New camera name:", font=('Segoe UI', 10)).pack(pady=15) + entry = ttk.Entry(dialog, width=40) + entry.insert(0, self.selected_camera) + entry.pack(pady=5) + entry.focus() + + def save(): + new_name = entry.get().strip() + if new_name and new_name != self.selected_camera: + if new_name in self.cameras: + messagebox.showerror("Error", "Camera already exists!", parent=dialog) + return + idx = self.cameras.index(self.selected_camera) + self.cameras[idx] = new_name + self.config_service.remove_camera(self.selected_camera) + self.config_service.add_camera(new_name) + self.cameras_listbox.delete(0, 'end') + for camera in self.cameras: + self.cameras_listbox.insert('end', camera) + self.selected_camera = new_name + dialog.destroy() + elif new_name == self.selected_camera: + dialog.destroy() + else: + messagebox.showwarning("Warning", "Please enter a name!", parent=dialog) + + ttk.Button(dialog, text="Save", command=save).pack(pady=10) + dialog.bind('', lambda e: save()) + + def _create_lenses_tab(self, parent): + # Listbox + list_frame = ttk.Frame(parent) + list_frame.pack(fill='both', expand=True, padx=10, pady=10) + + scrollbar = ttk.Scrollbar(list_frame) + scrollbar.pack(side='right', fill='y') + + self.lenses_listbox = tk.Listbox(list_frame, yscrollcommand=scrollbar.set, + bg='#2d2d2d', fg='#e0e0e0', + selectbackground='#4CAF50', selectforeground='white', + font=('Segoe UI', 10)) + self.lenses_listbox.pack(fill='both', expand=True) + scrollbar.config(command=self.lenses_listbox.yview) - def _update_lenses_list(self): - self.lenses_list.clear() for lens in self.lenses: - self.lenses_list.addItem(lens) - self._selected_lens = None - self.remove_lens_btn.setEnabled(False) + self.lenses_listbox.insert('end', lens) - def _select_lens(self, lens: str): - self._selected_lens = lens - self.remove_lens_btn.setEnabled(True) - self.cameras_list.clearSelection() - self.telescopes_list.clearSelection() - self._selected_camera = None - self._selected_telescope = None - self.remove_camera_btn.setEnabled(False) - self.remove_telescope_btn.setEnabled(False) + self.lenses_listbox.bind('<>', self._on_lens_select) + + # Buttons + btn_frame = ttk.Frame(parent) + btn_frame.pack(pady=10) + + ttk.Button(btn_frame, text="Add Lens", command=self._add_lens).pack(side='left', padx=5) + self.remove_lens_btn = ttk.Button(btn_frame, text="Remove", command=self._remove_lens, state='disabled') + self.remove_lens_btn.pack(side='left', padx=5) + self.edit_lens_btn = ttk.Button(btn_frame, text="Edit", command=self._edit_lens, state='disabled') + self.edit_lens_btn.pack(side='left', padx=5) + + def _on_lens_select(self, event): + selection = self.lenses_listbox.curselection() + if selection: + self.selected_lens = self.lenses_listbox.get(selection[0]) + self.remove_lens_btn.config(state='normal') + self.edit_lens_btn.config(state='normal') def _add_lens(self): - name, ok = QInputDialog.getText(self, "Добавить объектив", "Введите название объектива:") - if ok and name and name.strip(): - new_name = name.strip() - if new_name in self.lenses: - QMessageBox.warning(self, "Ошибка", "Такой объектив уже существует!") - return - self.lenses.append(new_name) - self.config_service.add_lens(new_name) - self._update_lenses_list() - QMessageBox.information(self, "Успех", f"Объектив '{new_name}' добавлен") + dialog = tk.Toplevel(self) + dialog.title("Add Lens") + dialog.geometry("350x120") + dialog.transient(self) + dialog.grab_set() + + ttk.Label(dialog, text="Lens name:", font=('Segoe UI', 10)).pack(pady=15) + entry = ttk.Entry(dialog, width=40) + entry.pack(pady=5) + entry.focus() + + def save(): + new_lens = entry.get().strip() + if new_lens: + if new_lens in self.lenses: + messagebox.showerror("Error", "Lens already exists!", parent=dialog) + return + self.lenses.append(new_lens) + self.config_service.add_lens(new_lens) + self.lenses_listbox.insert('end', new_lens) + dialog.destroy() + else: + messagebox.showwarning("Warning", "Please enter lens name!", parent=dialog) + + ttk.Button(dialog, text="OK", command=save).pack(pady=10) + dialog.bind('', lambda e: save()) def _remove_lens(self): - if hasattr(self, '_selected_lens') and self._selected_lens: - reply = QMessageBox.question(self, "Подтверждение", - f"Удалить объектив '{self._selected_lens}'?", - QMessageBox.Yes | QMessageBox.No) - if reply == QMessageBox.Yes: - self.lenses.remove(self._selected_lens) - self.config_service.remove_lens(self._selected_lens) - self._update_lenses_list() + if self.selected_lens: + reply = messagebox.askyesno("Remove Lens", f"Remove '{self.selected_lens}'?", parent=self) + if reply: + self.lenses.remove(self.selected_lens) + self.config_service.remove_lens(self.selected_lens) + self.lenses_listbox.delete(0, 'end') + for lens in self.lenses: + self.lenses_listbox.insert('end', lens) + self.selected_lens = None + self.remove_lens_btn.config(state='disabled') + self.edit_lens_btn.config(state='disabled') def _edit_lens(self): - if hasattr(self, '_selected_lens') and self._selected_lens: - new_name, ok = QInputDialog.getText(self, "Редактировать объектив", - f"Изменить '{self._selected_lens}' на:", - text=self._selected_lens) - if ok and new_name and new_name.strip(): - new_name = new_name.strip() - if new_name != self._selected_lens: + if self.selected_lens: + dialog = tk.Toplevel(self) + dialog.title("Edit Lens") + dialog.geometry("350x120") + dialog.transient(self) + dialog.grab_set() + + ttk.Label(dialog, text="New lens name:", font=('Segoe UI', 10)).pack(pady=15) + entry = ttk.Entry(dialog, width=40) + entry.insert(0, self.selected_lens) + entry.pack(pady=5) + entry.focus() + + def save(): + new_name = entry.get().strip() + if new_name and new_name != self.selected_lens: if new_name in self.lenses: - QMessageBox.warning(self, "Ошибка", "Такой объектив уже существует!") + messagebox.showerror("Error", "Lens already exists!", parent=dialog) return - idx = self.lenses.index(self._selected_lens) + idx = self.lenses.index(self.selected_lens) self.lenses[idx] = new_name - # Обновляем в конфиге (пока просто удаляем старый и добавляем новый) - self.config_service.remove_lens(self._selected_lens) + self.config_service.remove_lens(self.selected_lens) self.config_service.add_lens(new_name) - self._update_lenses_list() + self.lenses_listbox.delete(0, 'end') + for lens in self.lenses: + self.lenses_listbox.insert('end', lens) + self.selected_lens = new_name + dialog.destroy() + elif new_name == self.selected_lens: + dialog.destroy() + else: + messagebox.showwarning("Warning", "Please enter a name!", parent=dialog) - # ===== Методы для телескопов ===== + ttk.Button(dialog, text="Save", command=save).pack(pady=10) + dialog.bind('', lambda e: save()) + + def _create_telescopes_tab(self, parent): + # Listbox + list_frame = ttk.Frame(parent) + list_frame.pack(fill='both', expand=True, padx=10, pady=10) + + scrollbar = ttk.Scrollbar(list_frame) + scrollbar.pack(side='right', fill='y') + + self.telescopes_listbox = tk.Listbox(list_frame, yscrollcommand=scrollbar.set, + bg='#2d2d2d', fg='#e0e0e0', + selectbackground='#4CAF50', selectforeground='white', + font=('Segoe UI', 10)) + self.telescopes_listbox.pack(fill='both', expand=True) + scrollbar.config(command=self.telescopes_listbox.yview) - def _update_telescopes_list(self): - self.telescopes_list.clear() for telescope in self.telescopes: - self.telescopes_list.addItem(telescope) - self._selected_telescope = None - self.remove_telescope_btn.setEnabled(False) + self.telescopes_listbox.insert('end', telescope) - def _select_telescope(self, telescope: str): - self._selected_telescope = telescope - self.remove_telescope_btn.setEnabled(True) - self.cameras_list.clearSelection() - self.lenses_list.clearSelection() - self._selected_camera = None - self._selected_lens = None - self.remove_camera_btn.setEnabled(False) - self.remove_lens_btn.setEnabled(False) + self.telescopes_listbox.bind('<>', self._on_telescope_select) + + # Buttons + btn_frame = ttk.Frame(parent) + btn_frame.pack(pady=10) + + ttk.Button(btn_frame, text="Add Telescope", command=self._add_telescope).pack(side='left', padx=5) + self.remove_telescope_btn = ttk.Button(btn_frame, text="Remove", command=self._remove_telescope, state='disabled') + self.remove_telescope_btn.pack(side='left', padx=5) + self.edit_telescope_btn = ttk.Button(btn_frame, text="Edit", command=self._edit_telescope, state='disabled') + self.edit_telescope_btn.pack(side='left', padx=5) + + def _on_telescope_select(self, event): + selection = self.telescopes_listbox.curselection() + if selection: + self.selected_telescope = self.telescopes_listbox.get(selection[0]) + self.remove_telescope_btn.config(state='normal') + self.edit_telescope_btn.config(state='normal') def _add_telescope(self): - """Добавляет телескоп с указанием диафрагмы (фиксированной)""" - dialog = QDialog(self) - dialog.setWindowTitle("Добавить телескоп") - dialog.setMinimumWidth(400) + dialog = tk.Toplevel(self) + dialog.title("Add Telescope") + dialog.geometry("400x320") + dialog.transient(self) + dialog.grab_set() - layout = QVBoxLayout(dialog) + ttk.Label(dialog, text="Telescope name:").pack(pady=(15, 5)) + name_entry = ttk.Entry(dialog, width=40) + name_entry.pack() - form_layout = QFormLayout() + ttk.Label(dialog, text="Aperture (f/):").pack(pady=(10, 5)) + aperture_entry = ttk.Entry(dialog, width=20) + aperture_entry.insert(0, "5.0") + aperture_entry.pack() - name_edit = QLineEdit() - name_edit.setPlaceholderText("例如: Celestron 8\"") - form_layout.addRow("Название:", name_edit) + ttk.Label(dialog, text="Focal length (mm):").pack(pady=(10, 5)) + focal_entry = ttk.Entry(dialog, width=20) + focal_entry.insert(0, "1000") + focal_entry.pack() - aperture_spin = QDoubleSpinBox() - aperture_spin.setRange(0.5, 20.0) - aperture_spin.setSingleStep(0.1) - aperture_spin.setValue(5.0) - aperture_spin.setSuffix(" (f/)") - form_layout.addRow("Диафрагма (f/):", aperture_spin) + ttk.Label(dialog, text="Diameter (mm):").pack(pady=(10, 5)) + diameter_entry = ttk.Entry(dialog, width=20) + diameter_entry.insert(0, "200") + diameter_entry.pack() - focal_spin = QSpinBox() - focal_spin.setRange(100, 5000) - focal_spin.setSingleStep(50) - focal_spin.setSuffix(" мм") - form_layout.addRow("Фокусное расстояние:", focal_spin) + def save(): + name = name_entry.get().strip() + if not name: + messagebox.showerror("Error", "Please enter telescope name!", parent=dialog) + return - diameter_spin = QSpinBox() - diameter_spin.setRange(50, 500) - diameter_spin.setSingleStep(10) - diameter_spin.setSuffix(" мм") - form_layout.addRow("Диаметр объектива:", diameter_spin) + try: + aperture = float(aperture_entry.get()) + focal = int(focal_entry.get()) + diameter = int(diameter_entry.get()) + except ValueError: + messagebox.showerror("Error", "Invalid numeric values!", parent=dialog) + return - layout.addLayout(form_layout) + telescope_info = f"{name} (f/{aperture}, F={focal}mm, D={diameter}mm)" + if telescope_info in self.telescopes: + messagebox.showerror("Error", "Telescope already exists!", parent=dialog) + return - buttons_layout = QHBoxLayout() - ok_btn = QPushButton("OK") - cancel_btn = QPushButton("Отмена") - buttons_layout.addWidget(ok_btn) - buttons_layout.addWidget(cancel_btn) - layout.addLayout(buttons_layout) + self.telescopes.append(telescope_info) + self.config_service.add_telescope(telescope_info) + self.telescopes_listbox.insert('end', telescope_info) + dialog.destroy() - ok_btn.clicked.connect(dialog.accept) - cancel_btn.clicked.connect(dialog.reject) - - if dialog.exec(): - name = name_edit.text().strip() - if name: - telescope_info = f"{name} (f/{aperture_spin.value()}, F={focal_spin.value()}mm, D={diameter_spin.value()}mm)" - if telescope_info not in self.telescopes: - self.telescopes.append(telescope_info) - self.config_service.add_telescope(telescope_info) - self._update_telescopes_list() - QMessageBox.information(self, "Успех", f"Телескоп '{name}' добавлен") + btn_frame = ttk.Frame(dialog) + btn_frame.pack(pady=20) + ttk.Button(btn_frame, text="OK", command=save).pack(side='left', padx=10) + ttk.Button(btn_frame, text="Cancel", command=dialog.destroy).pack(side='left', padx=10) def _remove_telescope(self): - if hasattr(self, '_selected_telescope') and self._selected_telescope: - reply = QMessageBox.question(self, "Подтверждение", - f"Удалить телескоп '{self._selected_telescope}'?", - QMessageBox.Yes | QMessageBox.No) - if reply == QMessageBox.Yes: - self.telescopes.remove(self._selected_telescope) - self.config_service.remove_telescope(self._selected_telescope) - self._update_telescopes_list() + if self.selected_telescope: + reply = messagebox.askyesno("Remove Telescope", f"Remove '{self.selected_telescope}'?", parent=self) + if reply: + self.telescopes.remove(self.selected_telescope) + self.config_service.remove_telescope(self.selected_telescope) + self.telescopes_listbox.delete(0, 'end') + for telescope in self.telescopes: + self.telescopes_listbox.insert('end', telescope) + self.selected_telescope = None + self.remove_telescope_btn.config(state='disabled') + self.edit_telescope_btn.config(state='disabled') def _edit_telescope(self): - if hasattr(self, '_selected_telescope') and self._selected_telescope: - new_name, ok = QInputDialog.getText(self, "Редактировать телескоп", - f"Изменить '{self._selected_telescope}' на:", - text=self._selected_telescope) - if ok and new_name and new_name.strip(): - new_name = new_name.strip() - if new_name != self._selected_telescope: - if new_name in self.telescopes: - QMessageBox.warning(self, "Ошибка", "Такой телескоп уже существует!") - return - idx = self.telescopes.index(self._selected_telescope) - self.telescopes[idx] = new_name - self.config_service.remove_telescope(self._selected_telescope) - self.config_service.add_telescope(new_name) - self._update_telescopes_list() \ No newline at end of file + if self.selected_telescope: + import re + match = re.search(r'(.+?) \(f/([\d\.]+), F=(\d+)mm, D=(\d+)mm\)', self.selected_telescope) + if match: + old_name = match.group(1) + old_aperture = match.group(2) + old_focal = match.group(3) + old_diameter = match.group(4) + else: + old_name = self.selected_telescope + old_aperture = "5.0" + old_focal = "1000" + old_diameter = "200" + + dialog = tk.Toplevel(self) + dialog.title("Edit Telescope") + dialog.geometry("400x320") + dialog.transient(self) + dialog.grab_set() + + ttk.Label(dialog, text="Telescope name:").pack(pady=(15, 5)) + name_entry = ttk.Entry(dialog, width=40) + name_entry.insert(0, old_name) + name_entry.pack() + + ttk.Label(dialog, text="Aperture (f/):").pack(pady=(10, 5)) + aperture_entry = ttk.Entry(dialog, width=20) + aperture_entry.insert(0, old_aperture) + aperture_entry.pack() + + ttk.Label(dialog, text="Focal length (mm):").pack(pady=(10, 5)) + focal_entry = ttk.Entry(dialog, width=20) + focal_entry.insert(0, old_focal) + focal_entry.pack() + + ttk.Label(dialog, text="Diameter (mm):").pack(pady=(10, 5)) + diameter_entry = ttk.Entry(dialog, width=20) + diameter_entry.insert(0, old_diameter) + diameter_entry.pack() + + def save(): + new_name = name_entry.get().strip() + try: + aperture = float(aperture_entry.get()) + focal = int(focal_entry.get()) + diameter = int(diameter_entry.get()) + except ValueError: + messagebox.showerror("Error", "Invalid numeric values!", parent=dialog) + return + + new_info = f"{new_name} (f/{aperture}, F={focal}mm, D={diameter}mm)" + if new_info != self.selected_telescope and new_info in self.telescopes: + messagebox.showerror("Error", "Telescope already exists!", parent=dialog) + return + + idx = self.telescopes.index(self.selected_telescope) + self.telescopes[idx] = new_info + self.config_service.remove_telescope(self.selected_telescope) + self.config_service.add_telescope(new_info) + self.telescopes_listbox.delete(0, 'end') + for telescope in self.telescopes: + self.telescopes_listbox.insert('end', telescope) + dialog.destroy() + + btn_frame = ttk.Frame(dialog) + btn_frame.pack(pady=20) + ttk.Button(btn_frame, text="Save", command=save).pack(side='left', padx=10) + ttk.Button(btn_frame, text="Cancel", command=dialog.destroy).pack(side='left', padx=10) \ No newline at end of file diff --git a/ui/dialogs/instructions_dialog.py b/ui/dialogs/instructions_dialog.py index 64b8b28..4ac959f 100644 --- a/ui/dialogs/instructions_dialog.py +++ b/ui/dialogs/instructions_dialog.py @@ -1,164 +1,152 @@ """ -InstructionsDialog - диалог с инструкцией по использованию +InstructionsDialog - диалог с инструкцией на английском (tkinter) """ -from PySide6.QtWidgets import ( - QDialog, QVBoxLayout, QHBoxLayout, QLabel, QTextEdit, - QPushButton, QScrollArea -) -from PySide6.QtCore import Qt -from PySide6.QtGui import QFont + +import tkinter as tk +from tkinter import ttk -class InstructionsDialog(QDialog): - """Диалог с подробной инструкцией пользователя""" +class InstructionsDialog(tk.Toplevel): + """Диалог с подробной инструкцией пользователя на английском""" def __init__(self, parent): super().__init__(parent) + self.parent = parent - self.setWindowTitle("Инструкция по использованию") - self.setMinimumSize(700, 500) - self.resize(750, 550) + self.title("Instructions") + self.geometry("750x600") + self.minsize(700, 500) + self.transient(parent) + self.grab_set() self._create_ui() + self._center_window() def _create_ui(self): - """Создаёт интерфейс диалога""" - layout = QVBoxLayout(self) - layout.setSpacing(10) - layout.setContentsMargins(15, 15, 15, 15) + # Main frame + main_frame = ttk.Frame(self, padding="15") + main_frame.pack(fill='both', expand=True) - # Заголовок - title_label = QLabel("Astro Session Watcher - Руководство пользователя") - title_font = QFont() - title_font.setPointSize(16) - title_font.setBold(True) - title_label.setFont(title_font) - title_label.setAlignment(Qt.AlignCenter) - layout.addWidget(title_label) + # Title + ttk.Label(main_frame, text="Astro Session Watcher - User Guide", font=('Segoe UI', 16, 'bold')).pack(pady=(0, 10)) - # Текст инструкции - text_edit = QTextEdit() - text_edit.setReadOnly(True) - text_edit.setFont(QFont("Consolas", 10)) + # Text with scrollbar + text_frame = ttk.Frame(main_frame) + text_frame.pack(fill='both', expand=True) - instructions = """ -======================= ASTRO SESSION WATCHER ======================= + scrollbar = ttk.Scrollbar(text_frame) + scrollbar.pack(side='right', fill='y') -Приложение автоматически отслеживает появление новых фотографий в указанной папке, -сортирует их по объектам съемки и ведет подробный лог всего процесса. + self.text_widget = tk.Text(text_frame, yscrollcommand=scrollbar.set, wrap='word', + bg='#1e1e1e', fg='#e0e0e0', font=('Consolas', 10)) + self.text_widget.pack(fill='both', expand=True) + scrollbar.config(command=self.text_widget.yview) -📸 ДЛЯ ЧЕГО ЭТО НУЖНО? -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + # Instructions text + instructions = """======================= ASTRO SESSION WATCHER ======================= -Когда вы снимаете астрономические объекты через EOS Utility или аналогичное ПО, -все фотографии сохраняются в одну папку. Astro Session Watcher помогает: +The application automatically tracks new photos in the selected folder, +sorts them by observation targets and maintains detailed logs. -• Автоматически распределять снимки по папкам объектов -• Вести лог каждой сессии -• Не пропустить ни одного кадра при смене объекта -• Хранить историю оборудования и небесных тел +📸 WHAT IS IT FOR? +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -🚀 КАК ЭТО РАБОТАЕТ? -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +• Automatically distribute shots into target folders +• Keep a log of each session +• Don't miss a single frame when changing targets +• Store equipment and celestial bodies history -1. Вы выбираете папку, куда камера сохраняет снимки -2. Приложение создает папку сессии: "AstroSession_ГГГГ-ММ-ДД" -3. Каждый объект съемки получает свою подпапку внутри папки сессии -4. Когда вы меняете объект (нажимаете "Новая цель"), все накопленные - в папке наблюдения файлы автоматически ПЕРЕМЕЩАЮТСЯ в папку предыдущего объекта -5. При завершении сессии оставшиеся файлы также перемещаются в папку последнего объекта +🚀 HOW IT WORKS? +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -📝 ПОШАГОВАЯ ИНСТРУКЦИЯ -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +1. Select the folder where your camera saves photos +2. The app creates a session folder: "AstroSession_YYYY-MM-DD" +3. Each target gets its own subfolder inside the session folder +4. When you change target (press "New Target"), all accumulated files are moved +5. When you end the session, remaining files are moved to the last target -█ 1. ПЕРВЫЙ ЗАПУСК (НАСТРОЙКА) -─────────────────────────────────────────────────────────────────────────────── +📝 STEP-BY-STEP GUIDE +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -• Откройте меню "Файл" → "Оборудование" и добавьте ваши камеры и объективы -• Откройте меню "Файл" → "Небесные тела" и добавьте объекты для наблюдения -• Все данные сохраняются автоматически в файлах настроек +█ 1. FIRST LAUNCH (SETUP) +─────────────────────────────────────────────────────────────────── -█ 2. ЗАПУСК СЕССИИ -─────────────────────────────────────────────────────────────────────────────── +• Go to "File" → "Equipment" and add your cameras and lenses/telescopes +• Go to "File" → "Celestial Bodies" and add your observation targets +• All data is saved automatically in config files -1. Нажмите "Обзор" и выберите папку, куда камера сохраняет снимки -2. Выберите камеру и объектив из выпадающих списков -3. Введите название цели (или выберите из списка небесных тел) -4. Нажмите ▶ "Начать отслеживание" +█ 2. STARTING A SESSION +─────────────────────────────────────────────────────────────────── -✅ После запуска: - • Статус изменится на "● ON AIR" с мигающим красным текстом - • Кнопка "Новая цель" начнет мигать красным контуром - • В папке наблюдения создастся папка "AstroSession_дата" - • Внутри - папка с вашей первой целью +1. Click "Browse" and select the folder where your camera saves photos +2. Select camera and lens/telescope from dropdowns +3. Enter target name (or select from celestial bodies list) +4. Click "▶ Start Tracking" -█ 3. СМЕНА ОБЪЕКТА ВО ВРЕМЯ СЕССИИ -─────────────────────────────────────────────────────────────────────────────── +✅ After launch: + • Status changes to "● ON AIR" with blinking + • "New Target" button becomes active + • Session folder "AstroSession_date" is created + • Inside - folder with your first target -Когда вы заканчиваете снимать один объект и переходите к другому: +█ 3. CHANGING TARGET DURING SESSION +─────────────────────────────────────────────────────────────────── -1. Нажмите кнопку "Новая цель" (или Ctrl+Shift+N) -2. Введите название нового объекта -3. Приложение автоматически: - • Переместит все накопленные файлы в папку предыдущего объекта - • Создаст новую папку для следующего объекта - • Сбросит счетчик файлов - • Продолжит отслеживание +1. Click "New Target" button (or Ctrl+Shift+N) +2. Enter new target name +3. The app automatically moves all accumulated files to the previous target +4. Creates new folder for the next target +5. Resets file counter +6. Continues tracking -💡 ВАЖНО: Если перед сменой объекта в папке наблюдения уже есть файлы, - они НЕ ПОТЕРЯЮТСЯ - все будут перемещены в папку текущего объекта! +💡 IMPORTANT: If there are files in the watch folder before changing target, + they will NOT be lost - all will be moved! -█ 4. ЗАВЕРШЕНИЕ СЕССИИ -─────────────────────────────────────────────────────────────────────────────── +█ 4. ENDING A SESSION +─────────────────────────────────────────────────────────────────── -1. Нажмите ■ "Остановить" (или Ctrl+X) -2. Приложение: - • Переместит все оставшиеся файлы в папку последнего объекта - • Запишет итоговый лог сессии - • Покажет диалог с предложением открыть папку сессии - • Восстановит интерфейс для новой сессии +1. Click "■ Stop" (or Ctrl+X) +2. The app moves all remaining files to the last target +3. Writes final session log +4. Shows dialog with option to open session folder +5. Restores interface for new session -⌨️ ГОРЯЧИЕ КЛАВИШИ -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +⌨️ HOTKEYS +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -Ctrl + O → Выбрать папку наблюдения -Ctrl + E → Управление оборудованием -Ctrl + B → Управление небесными телами -Ctrl + S → Начать сессию -Ctrl + X → Остановить сессию -Ctrl + F → Открыть папку текущей сессии -Ctrl + Shift+N → Создать новый объект -F1 → О программе -F2 → Эта инструкция +Ctrl + O → Select watch folder +Ctrl + E → Equipment management +Ctrl + B → Celestial bodies management +Ctrl + S → Start session +Ctrl + X → Stop session +Ctrl + F → Open current session folder +Ctrl + Shift+N → Create new target +F1 → About +F2 → This instruction -🔧 ФАЙЛЫ НАСТРОЕК -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +🔧 CONFIG FILES +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -📄 astro_settings.json ← камеры, объективы, последняя папка -📄 celestial_bodies.json ← список небесных тел +📄 astro_settings.json ← cameras, lenses, last folder +📄 celestial_bodies.json ← list of celestial bodies -Все файлы хранятся в папке с программой. Вы можете редактировать их вручную. +All files are stored in the program folder. You can edit them manually. -📧 ТЕХНИЧЕСКАЯ ПОДДЕРЖКА -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -Разработчик: Vic Sergeev -Версия: 0.3.0-alpha - -При обнаружении ошибок или для предложений по улучшению: -• Сообщите разработчику -• Приложите файлы логов (SessionLog.txt, ObjectLog.txt) +📧 TECHNICAL SUPPORT +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Text me: norvicdev@gmail.com +Developer: Vic Sergeev +Version: 0.4.0-alpha """ - text_edit.setText(instructions) - layout.addWidget(text_edit) + self.text_widget.insert('1.0', instructions) + self.text_widget.config(state='disabled') - # Кнопка закрытия - close_layout = QHBoxLayout() - close_layout.addStretch() + # Close button + ttk.Button(main_frame, text="Close", command=self.destroy).pack(pady=10) - close_btn = QPushButton("Закрыть") - close_btn.clicked.connect(self.accept) - close_layout.addWidget(close_btn) - - layout.addLayout(close_layout) \ No newline at end of file + def _center_window(self): + self.update_idletasks() + x = self.parent.winfo_x() + (self.parent.winfo_width() // 2) - (self.winfo_width() // 2) + y = self.parent.winfo_y() + (self.parent.winfo_height() // 2) - (self.winfo_height() // 2) + self.geometry(f'+{x}+{y}') \ No newline at end of file diff --git a/ui/main_window.py b/ui/main_window.py index edb8183..2b7cee6 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -1,20 +1,14 @@ """ -MainWindow - главное окно приложения на PySide6 +MainWindow - главное окно приложения на tkinter """ -import sys + +import tkinter as tk +from tkinter import ttk, messagebox, filedialog, simpledialog +from pathlib import Path +from threading import Thread import subprocess import platform -from pathlib import Path from datetime import datetime -from typing import Optional - -from PySide6.QtWidgets import ( - QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, - QLabel, QLineEdit, QComboBox, QPushButton, QMenuBar, QMenu, - QMessageBox, QFileDialog, QInputDialog, QFrame, QApplication -) -from PySide6.QtCore import Qt, QTimer, Signal -from PySide6.QtGui import QFont, QIcon, QAction from services.config_service import ConfigService from services.session_service import SessionService @@ -22,11 +16,15 @@ from services.watch_service import WatchService from services.file_service import FileService -class MainWindow(QMainWindow): +class MainWindow: """Главное окно приложения""" - def __init__(self): - super().__init__() + def __init__(self, root): + self.root = root + self.root.title("Astro Session Watcher v0.4.0") + self.root.geometry("800x550") + self.root.minsize(700, 500) + self.center_window() # Сервисы self.config_service = ConfigService() @@ -36,483 +34,377 @@ class MainWindow(QMainWindow): # Переменные состояния self.running = False self.file_count = 0 - self._blink_timer = None - self._new_object_blink_timer = None + self.current_target = "" + self.current_session_folder = "" + self._blink_active = False - # Настройка окна - self.setWindowTitle("Astro Session Watcher v0.3.0") - self.setMinimumSize(700, 500) - self.resize(800, 550) + # Стили + self._setup_styles() - self.center_window() + # Создаём интерфейс self._create_menu_bar() self._create_main_content() self._load_saved_settings() self._setup_hotkeys() + + # Обновление счётчика self._update_file_count_display() - self.setAttribute(Qt.WA_DeleteOnClose) + # Обработчик закрытия + self.root.protocol("WM_DELETE_WINDOW", self._on_closing) def center_window(self): - screen = QApplication.primaryScreen().availableGeometry() - self.setGeometry( - (screen.width() - self.width()) // 2, - (screen.height() - self.height()) // 2, - self.width(), - self.height() - ) + self.root.update_idletasks() + x = (self.root.winfo_screenwidth() // 2) - (self.root.winfo_width() // 2) + y = (self.root.winfo_screenheight() // 2) - (self.root.winfo_height() // 2) + self.root.geometry(f'+{x}+{y}') + + def _setup_styles(self): + style = ttk.Style() + style.theme_use('clam') + + # Тёмная тема + style.configure('.', background='#1e1e1e', foreground='#e0e0e0') + style.configure('TLabel', background='#1e1e1e', foreground='#e0e0e0') + style.configure('TFrame', background='#1e1e1e') + style.configure('TLabelframe', background='#1e1e1e', foreground='#e0e0e0') + style.configure('TLabelframe.Label', background='#1e1e1e', foreground='#e0e0e0') + + # Кнопки + style.configure('TButton', background='#3c3c3c', foreground='#e0e0e0', borderwidth=1) + style.map('TButton', + background=[('active', '#4c4c4c')], + foreground=[('active', '#ffffff')]) + + # Поля ввода + style.configure('TEntry', fieldbackground='#3c3c3c', foreground='#e0e0e0') + style.configure('TCombobox', fieldbackground='#3c3c3c', foreground='#e0e0e0') + + # Специальные кнопки + style.configure('Green.TButton', background='#4CAF50', foreground='white') + style.map('Green.TButton', background=[('active', '#45a049')]) + + style.configure('Red.TButton', background='#f44336', foreground='black') + style.map('Red.TButton', background=[('active', '#d32f2f')]) + + self.root.configure(bg='#1e1e1e') def _create_menu_bar(self): - menubar = self.menuBar() + menubar = tk.Menu(self.root) + self.root.config(menu=menubar) - # Меню Файл - file_menu = menubar.addMenu("Файл") + # File menu + file_menu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label="File", menu=file_menu) + file_menu.add_command(label="Select Folder...", command=self.select_folder, accelerator="Ctrl+O") + file_menu.add_separator() + file_menu.add_command(label="Equipment...", command=self.open_equipment_dialog, accelerator="Ctrl+E") + file_menu.add_command(label="Celestial Bodies...", command=self.open_celestial_dialog, accelerator="Ctrl+B") + file_menu.add_separator() + file_menu.add_command(label="Exit", command=self._on_closing, accelerator="Ctrl+Q") - select_folder_action = QAction("Выбрать папку...", self) - select_folder_action.setShortcut("Ctrl+O") - select_folder_action.triggered.connect(self.select_folder) - file_menu.addAction(select_folder_action) + # Session menu + session_menu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label="Session", menu=session_menu) + session_menu.add_command(label="Start Tracking", command=self.start, accelerator="Ctrl+S") + session_menu.add_command(label="Stop", command=self.stop, accelerator="Ctrl+X") + session_menu.add_separator() + session_menu.add_command(label="Open Session Folder", command=self.open_session_folder, accelerator="Ctrl+F") + session_menu.add_separator() + session_menu.add_command(label="New Target...", command=self.set_new_object, accelerator="Ctrl+Shift+N") + session_menu.add_separator() + session_menu.add_command(label="Calibration Frames...", command=self.open_calibration_dialog, accelerator="Ctrl+K") - file_menu.addSeparator() - - equipment_action = QAction("Оборудование...", self) - equipment_action.setShortcut("Ctrl+E") - equipment_action.triggered.connect(self.open_equipment_dialog) - file_menu.addAction(equipment_action) - - celestial_action = QAction("Небесные тела...", self) - celestial_action.setShortcut("Ctrl+B") - celestial_action.triggered.connect(self.open_celestial_dialog) - file_menu.addAction(celestial_action) - - file_menu.addSeparator() - - exit_action = QAction("Выход", self) - exit_action.setShortcut("Ctrl+Q") - exit_action.triggered.connect(self.close) - file_menu.addAction(exit_action) - - # Меню Сессия - session_menu = menubar.addMenu("Сессия") - - start_action = QAction("Начать наблюдение", self) - start_action.setShortcut("Ctrl+S") - start_action.triggered.connect(self.start) - session_menu.addAction(start_action) - - stop_action = QAction("Остановить наблюдение", self) - stop_action.setShortcut("Ctrl+X") - stop_action.triggered.connect(self.stop) - session_menu.addAction(stop_action) - - session_menu.addSeparator() - - open_folder_action = QAction("Открыть папку сессии", self) - open_folder_action.setShortcut("Ctrl+F") - open_folder_action.triggered.connect(self.open_session_folder) - session_menu.addAction(open_folder_action) - - session_menu.addSeparator() - - new_object_action = QAction("Новая цель...", self) - new_object_action.setShortcut("Ctrl+Shift+N") - new_object_action.triggered.connect(self.set_new_object) - session_menu.addAction(new_object_action) - - session_menu.addSeparator() - - calibration_action = QAction("🌑 Калибровочные кадры...", self) - calibration_action.setShortcut("Ctrl+K") - calibration_action.triggered.connect(self.open_calibration_dialog) - session_menu.addAction(calibration_action) - - # Меню Помощь - help_menu = menubar.addMenu("Помощь") - - instructions_action = QAction("Инструкция", self) - instructions_action.setShortcut("F2") - instructions_action.triggered.connect(self.show_instructions) - help_menu.addAction(instructions_action) - - help_menu.addSeparator() - - about_action = QAction("О программе", self) - about_action.setShortcut("F1") - about_action.triggered.connect(self.show_info) - help_menu.addAction(about_action) - - def open_calibration_dialog(self): - """Открывает диалог калибровочных кадров""" - from ui.dialogs.calibration_dialog import CalibrationDialog - dialog = CalibrationDialog(self, self.config_service) - dialog.exec() + # Help menu + help_menu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label="Help", menu=help_menu) + help_menu.add_command(label="Instructions", command=self.show_instructions, accelerator="F2") + help_menu.add_separator() + help_menu.add_command(label="About", command=self.show_info, accelerator="F1") def _create_main_content(self): - central_widget = QWidget() - self.setCentralWidget(central_widget) + # Main frame + main_frame = ttk.Frame(self.root, padding="20") + main_frame.pack(fill="both", expand=True) - main_layout = QVBoxLayout(central_widget) - main_layout.setContentsMargins(20, 20, 20, 20) - main_layout.setSpacing(15) + # Grid layout + main_frame.grid_columnconfigure(0, weight=0, minsize=100) + main_frame.grid_columnconfigure(1, weight=1) - grid_layout = QGridLayout() - grid_layout.setVerticalSpacing(12) - grid_layout.setHorizontalSpacing(15) + # Row 0: Folder + ttk.Label(main_frame, text="Folder:", font=('Segoe UI', 10, 'bold')).grid(row=0, column=0, sticky='w', pady=5) + folder_frame = ttk.Frame(main_frame) + folder_frame.grid(row=0, column=1, sticky='ew', pady=5) + folder_frame.grid_columnconfigure(0, weight=1) - # Row 0: Папка - folder_label = QLabel("Папка:") - folder_label.setFont(QFont("", 10, QFont.Bold)) - grid_layout.addWidget(folder_label, 0, 0, Qt.AlignRight | Qt.AlignVCenter) + self.folder_entry = ttk.Entry(folder_frame) + self.folder_entry.grid(row=0, column=0, sticky='ew', padx=(0, 10)) + self.folder_entry.insert(0, "Select watch folder...") + self.folder_entry.bind('', lambda e: self._clear_placeholder(self.folder_entry, "Select watch folder...")) + self.folder_entry.bind('', lambda e: self._restore_placeholder(self.folder_entry, "Select watch folder...")) - folder_widget = QWidget() - folder_layout = QHBoxLayout(folder_widget) - folder_layout.setContentsMargins(0, 0, 0, 0) - folder_layout.setSpacing(10) + self.browse_btn = ttk.Button(folder_frame, text="Browse...", width=10, command=self.select_folder) + self.browse_btn.grid(row=0, column=1) - self.folder_entry = QLineEdit() - self.folder_entry.setPlaceholderText("Выберите папку для отслеживания") - folder_layout.addWidget(self.folder_entry) + # Row 1: Equipment + ttk.Label(main_frame, text="Equipment:", font=('Segoe UI', 10, 'bold')).grid(row=1, column=0, sticky='w', pady=5) + equipment_frame = ttk.Frame(main_frame) + equipment_frame.grid(row=1, column=1, sticky='ew', pady=5) + equipment_frame.grid_columnconfigure(0, weight=1) + equipment_frame.grid_columnconfigure(1, weight=1) - self.folder_button = QPushButton("Обзор...") - self.folder_button.setFixedWidth(80) - self.folder_button.clicked.connect(self.select_folder) - folder_layout.addWidget(self.folder_button) + self.camera_combo = ttk.Combobox(equipment_frame, values=[]) + self.camera_combo.grid(row=0, column=0, sticky='ew', padx=(0, 10)) + self.camera_combo.set("Select or enter camera...") + self.camera_combo.bind('', lambda e: self._clear_combo(self.camera_combo, "Select or enter camera...")) + self.camera_combo.bind('', lambda e: self._restore_combo(self.camera_combo, "Select or enter camera...")) - grid_layout.addWidget(folder_widget, 0, 1) + self.lens_combo = ttk.Combobox(equipment_frame, values=[]) + self.lens_combo.grid(row=0, column=1, sticky='ew') + self.lens_combo.set("Select or enter lens/telescope...") + self.lens_combo.bind('', lambda e: self._clear_combo(self.lens_combo, "Select or enter lens/telescope...")) + self.lens_combo.bind('', lambda e: self._restore_combo(self.lens_combo, "Select or enter lens/telescope...")) - # Row 1: Оборудование - equipment_label = QLabel("Оборудование:") - equipment_label.setFont(QFont("", 10, QFont.Bold)) - grid_layout.addWidget(equipment_label, 1, 0, Qt.AlignRight | Qt.AlignVCenter) + # Row 2: Target + ttk.Label(main_frame, text="Target:", font=('Segoe UI', 10, 'bold')).grid(row=2, column=0, sticky='w', pady=5) + target_frame = ttk.Frame(main_frame) + target_frame.grid(row=2, column=1, sticky='ew', pady=5) + target_frame.grid_columnconfigure(0, weight=1) - equipment_widget = QWidget() - equipment_layout = QHBoxLayout(equipment_widget) - equipment_layout.setContentsMargins(0, 0, 0, 0) - equipment_layout.setSpacing(10) + self.target_combo = ttk.Combobox(target_frame, values=[]) + self.target_combo.grid(row=0, column=0, sticky='ew', padx=(0, 10)) + self.target_combo.set("Enter target name...") + self.target_combo.bind('', lambda e: self._clear_combo(self.target_combo, "Enter target name...")) + self.target_combo.bind('', lambda e: self._restore_combo(self.target_combo, "Enter target name...")) - self.camera_combo = QComboBox() - self.camera_combo.setEditable(False) - equipment_layout.addWidget(self.camera_combo) + self.new_target_btn = ttk.Button(target_frame, text="New Target", width=12, command=self.set_new_object) + self.new_target_btn.grid(row=0, column=1) + self.new_target_btn.configure(state='disabled') - self.lens_combo = QComboBox() - self.lens_combo.setEditable(False) - equipment_layout.addWidget(self.lens_combo) + # Row 3: Statistics + ttk.Label(main_frame, text="Statistics:", font=('Segoe UI', 10, 'bold')).grid(row=3, column=0, sticky='w', pady=5) + self.stats_label = ttk.Label(main_frame, text="Files received: 0", font=('Segoe UI', 11)) + self.stats_label.grid(row=3, column=1, sticky='w', pady=5) - grid_layout.addWidget(equipment_widget, 1, 1) + # Row 4: Status + ttk.Label(main_frame, text="Status:", font=('Segoe UI', 10, 'bold')).grid(row=4, column=0, sticky='w', pady=5) + self.status_label = ttk.Label(main_frame, text="IDLE", font=('Segoe UI', 12, 'bold'), foreground='#666666') + self.status_label.grid(row=4, column=1, sticky='w', pady=5) - # Row 2: Цель - target_label = QLabel("Цель:") - target_label.setFont(QFont("", 10, QFont.Bold)) - grid_layout.addWidget(target_label, 2, 0, Qt.AlignRight | Qt.AlignVCenter) + # Separator + separator = ttk.Separator(main_frame, orient='horizontal') + separator.grid(row=5, column=0, columnspan=2, sticky='ew', pady=15) - target_widget = QWidget() - target_layout = QHBoxLayout(target_widget) - target_layout.setContentsMargins(0, 0, 0, 0) - target_layout.setSpacing(10) + # Buttons + buttons_frame = ttk.Frame(main_frame) + buttons_frame.grid(row=6, column=0, columnspan=2, pady=10) - self.object_combo = QComboBox() - self.object_combo.setEditable(True) - self.object_combo.setInsertPolicy(QComboBox.NoInsert) - # Настройка плейсхолдера - self.object_combo.lineEdit().setPlaceholderText("Введите название цели") - # Автодополнение при вводе - self.object_combo.lineEdit().textChanged.connect(self._on_object_text_changed) - target_layout.addWidget(self.object_combo) + self.start_btn = ttk.Button(buttons_frame, text="▶ Start Tracking", width=18, command=self.start, style='Green.TButton') + self.start_btn.pack(side='left', padx=10) - self.new_object_button = QPushButton("Новая цель") - self.new_object_button.setFixedWidth(100) - self.new_object_button.setEnabled(False) - self.new_object_button.clicked.connect(self.set_new_object) - target_layout.addWidget(self.new_object_button) + self.stop_btn = ttk.Button(buttons_frame, text="■ Stop", width=12, command=self.stop, style='Red.TButton') + self.stop_btn.pack(side='left', padx=10) + self.stop_btn.configure(state='disabled') - grid_layout.addWidget(target_widget, 2, 1) + # Footer + footer_frame = ttk.Frame(self.root) + footer_frame.pack(side='bottom', fill='x', padx=20, pady=(0, 10)) - # Row 3: Статистика - stats_label = QLabel("Статистика:") - stats_label.setFont(QFont("", 10, QFont.Bold)) - grid_layout.addWidget(stats_label, 3, 0, Qt.AlignRight | Qt.AlignVCenter) + ttk.Label(footer_frame, text="v0.4.0-alpha", foreground='#666666').pack(side='left') + ttk.Label(footer_frame, text="Made by Vic Sergeev 2026", foreground='#666666').pack(side='right') - self.file_count_label = QLabel("Файлов получено: 0") - self.file_count_label.setFont(QFont("", 11)) - grid_layout.addWidget(self.file_count_label, 3, 1, Qt.AlignLeft) + def _clear_placeholder(self, entry, placeholder): + if entry.get() == placeholder: + entry.delete(0, 'end') - # Row 4: Статус - status_label = QLabel("Статус:") - status_label.setFont(QFont("", 10, QFont.Bold)) - grid_layout.addWidget(status_label, 4, 0, Qt.AlignRight | Qt.AlignVCenter) + def _restore_placeholder(self, entry, placeholder): + if entry.get() == '': + entry.insert(0, placeholder) - self.status_label = QLabel("IDLE") - self.status_label.setFont(QFont("", 12, QFont.Bold)) - self.status_label.setStyleSheet("color: #666666;") - grid_layout.addWidget(self.status_label, 4, 1, Qt.AlignLeft) + def _clear_combo(self, combo, placeholder): + if combo.get() == placeholder: + combo.set('') - main_layout.addLayout(grid_layout) - - separator = QFrame() - separator.setFrameShape(QFrame.HLine) - separator.setStyleSheet("background-color: #333333; max-height: 1px;") - main_layout.addWidget(separator) - - buttons_layout = QHBoxLayout() - buttons_layout.setSpacing(15) - buttons_layout.setAlignment(Qt.AlignCenter) - - self.start_button = QPushButton("▶ Начать отслеживание") - self.start_button.setFixedSize(180, 35) - self.start_button.setStyleSheet(""" - QPushButton { - background-color: #4CAF50; - color: white; - font-weight: bold; - border-radius: 4px; - } - QPushButton:hover { - background-color: #45a049; - } - """) - self.start_button.clicked.connect(self.start) - buttons_layout.addWidget(self.start_button) - - self.stop_button = QPushButton("■ Остановить") - self.stop_button.setFixedSize(180, 35) - self.stop_button.setEnabled(False) - self.stop_button.setStyleSheet(""" - QPushButton { - background-color: #f44336; - color: white; - font-weight: bold; - border-radius: 4px; - } - QPushButton:hover { - background-color: #d32f2f; - } - """) - self.stop_button.clicked.connect(self.stop) - buttons_layout.addWidget(self.stop_button) - - main_layout.addLayout(buttons_layout) - - footer_layout = QHBoxLayout() - - version_label = QLabel("v0.3.0-alpha") - version_label.setStyleSheet("color: #666666; font-size: 11px;") - footer_layout.addWidget(version_label) - - footer_layout.addStretch() - - copyright_label = QLabel("Made by Vic Sergeev 2026") - copyright_label.setStyleSheet("color: #666666; font-size: 11px;") - footer_layout.addWidget(copyright_label) - - main_layout.addLayout(footer_layout) - - def _on_object_text_changed(self, text): - """Автодополнение при вводе названия цели""" - if not text: - return - - # Поиск совпадений в списке небесных тел - celestial_bodies = self.config_service.get_celestial_bodies() - matches = [body for body in celestial_bodies if body.lower().startswith(text.lower())] - - if matches and matches[0] != text: - # Временно отключаем сигнал, чтобы избежать рекурсии - self.object_combo.lineEdit().blockSignals(True) - self.object_combo.lineEdit().setText(matches[0]) - self.object_combo.lineEdit().setSelection(len(text), len(matches[0])) - self.object_combo.lineEdit().blockSignals(False) + def _restore_combo(self, combo, placeholder): + if combo.get() == '': + combo.set(placeholder) def _load_saved_settings(self): - """Загружает сохранённые настройки""" cameras = self.config_service.get_cameras() lenses = self.config_service.get_lenses() - telescopes = self.config_service.get_telescopes() # <-- добавить + telescopes = self.config_service.get_telescopes() celestial_bodies = self.config_service.get_celestial_bodies() - # Объединяем объективы и телескопы для выбора оптики all_optics = [] for lens in lenses: - all_optics.append(f"🔭 {lens}") + all_optics.append(lens) for telescope in telescopes: - all_optics.append(f"🪐 {telescope}") + all_optics.append(telescope) if cameras: - self.camera_combo.addItems(cameras) + self.camera_combo['values'] = cameras last_camera = self.config_service.get_last_camera() if last_camera and last_camera in cameras: - self.camera_combo.setCurrentText(last_camera) + self.camera_combo.set(last_camera) if all_optics: - self.lens_combo.addItems(all_optics) + self.lens_combo['values'] = all_optics last_lens = self.config_service.get_last_lens() - if last_lens: - # Ищем последнюю использованную оптику - for opt in all_optics: - if last_lens in opt: - self.lens_combo.setCurrentText(opt) - break + if last_lens and last_lens in all_optics: + self.lens_combo.set(last_lens) if celestial_bodies: - self.object_combo.addItems(celestial_bodies) + self.target_combo['values'] = celestial_bodies last_folder = self.config_service.get_last_watch_folder() if last_folder: - self.folder_entry.setText(last_folder) + self.folder_entry.delete(0, 'end') + self.folder_entry.insert(0, last_folder) def _setup_hotkeys(self): - pass + def on_key(event): + if event.state & 0x4: # Ctrl + if event.keysym == 'o': + self.select_folder() + elif event.keysym == 'e': + self.open_equipment_dialog() + elif event.keysym == 'b': + self.open_celestial_dialog() + elif event.keysym == 's': + self.start() + elif event.keysym == 'x': + self.stop() + elif event.keysym == 'f': + self.open_session_folder() + elif event.keysym == 'k': + self.open_calibration_dialog() + elif event.state & 0x6: # Ctrl+Shift + if event.keysym == 'N': + self.set_new_object() + elif event.keysym == 'F1': + self.show_info() + elif event.keysym == 'F2': + self.show_instructions() - def _set_running_state(self, state: bool): + self.root.bind_all('', on_key) + + def _set_running_state(self, state): self.running = state if state: - self.start_button.setEnabled(False) - self.stop_button.setEnabled(True) - self.new_object_button.setEnabled(True) - self.status_label.setText("● ON AIR") - self.status_label.setStyleSheet("color: #ff0000; font-weight: bold;") + self.start_btn.configure(state='disabled') + self.stop_btn.configure(state='normal') + self.new_target_btn.configure(state='normal') + self.status_label.configure(text="● ON AIR", foreground='#ff0000') self._start_blinking() - self._start_new_object_blinking() else: - self.start_button.setEnabled(True) - self.stop_button.setEnabled(False) - self.new_object_button.setEnabled(False) - self.status_label.setText("IDLE") - self.status_label.setStyleSheet("color: #666666; font-weight: bold;") + self.start_btn.configure(state='normal') + self.stop_btn.configure(state='disabled') + self.new_target_btn.configure(state='disabled') + self.status_label.configure(text="IDLE", foreground='#666666') self._stop_blinking() - self._stop_new_object_blinking() def _start_blinking(self): - self._blink_timer = QTimer() - self._blink_timer.timeout.connect(self._do_blink) - self._blink_timer.start(500) + self._blink_active = True + self._do_blink() def _do_blink(self): - if not self.running: + if not self._blink_active or not self.running: return - current_style = self.status_label.styleSheet() - if "color: #ff0000" in current_style: - self.status_label.setStyleSheet("color: #ffffff; font-weight: bold;") - else: - self.status_label.setStyleSheet("color: #ff0000; font-weight: bold;") + current = self.status_label.cget('foreground') + new_color = '#ffffff' if current == '#ff0000' else '#ff0000' + self.status_label.configure(foreground=new_color) + self.root.after(500, self._do_blink) def _stop_blinking(self): - if self._blink_timer: - self._blink_timer.stop() - self._blink_timer = None - self.status_label.setStyleSheet("color: #666666; font-weight: bold;") - - def _start_new_object_blinking(self): - self._new_object_blink_timer = QTimer() - self._new_object_blink_timer.timeout.connect(self._do_new_object_blink) - self._new_object_blink_timer.start(500) - - def _do_new_object_blink(self): - if not self.running: - return - current_style = self.new_object_button.styleSheet() - if "border: 2px solid red" in current_style: - self.new_object_button.setStyleSheet("") - else: - self.new_object_button.setStyleSheet("border: 2px solid red; border-radius: 4px;") - - def _stop_new_object_blinking(self): - if self._new_object_blink_timer: - self._new_object_blink_timer.stop() - self._new_object_blink_timer = None - self.new_object_button.setStyleSheet("") + self._blink_active = False + self.status_label.configure(foreground='#666666') def _update_file_count_display(self): if self.running and self.session_service.get_current_object(): current_obj = self.session_service.get_current_object() self.file_count = current_obj.photo_count - self.file_count_label.setText(f"Файлов получено: {self.file_count}") - QTimer.singleShot(1000, self._update_file_count_display) + self.stats_label.configure(text=f"Files received: {self.file_count}") + self.root.after(1000, self._update_file_count_display) def _on_file_received(self, file_path: Path): - """Обработчик получения нового файла""" - print(f"Обнаружен файл: {file_path}") if self.session_service.handle_file(file_path): self.file_count += 1 - self.file_count_label.setText(f"Файлов получено: {self.file_count}") - print(f"Файл обработан: {file_path.name}") - else: - print(f"Не удалось обработать файл: {file_path}") + self.stats_label.configure(text=f"Files received: {self.file_count}") + print(f"File processed: {file_path.name}") def select_folder(self): - folder = QFileDialog.getExistingDirectory(self, "Выберите папку для отслеживания") + folder = filedialog.askdirectory(title="Select watch folder") if folder: - self.folder_entry.setText(folder) + self.folder_entry.delete(0, 'end') + self.folder_entry.insert(0, folder) self.config_service.set_last_watch_folder(folder) def start(self): - watch_folder = self.folder_entry.text() - object_name = self.object_combo.currentText() + watch_folder = self.folder_entry.get() + target_name = self.target_combo.get() + camera = self.camera_combo.get() + lens = self.lens_combo.get() + + # Skip placeholders + if watch_folder == "Select watch folder...": + watch_folder = "" + if target_name == "Enter target name...": + target_name = "" + if camera == "Select or enter camera...": + camera = "" + if lens == "Select or enter lens/telescope...": + lens = "" if not watch_folder: - QMessageBox.critical(self, "Ошибка", "Папка для отслеживания не выбрана") + messagebox.showerror("Error", "Please select a folder to watch!", parent=self.root) return - if not object_name: - QMessageBox.critical(self, "Ошибка", "Цель не указана") + if not target_name: + messagebox.showerror("Error", "Please enter a target name!", parent=self.root) return - # Проверка, существует ли объект в списке небесных тел celestial_bodies = self.config_service.get_celestial_bodies() - if object_name not in celestial_bodies: - reply = QMessageBox.question(self, "Новый объект", - f"Объект '{object_name}' не найден в списке.\nДобавить его в список?", - QMessageBox.Yes | QMessageBox.No) - if reply == QMessageBox.Yes: - self.config_service.add_celestial_body(object_name) - self.object_combo.addItem(object_name) + if target_name not in celestial_bodies: + reply = messagebox.askyesno("New Target", + f"Target '{target_name}' not found in list.\nAdd it to the list?", + parent=self.root) + if reply: + self.config_service.add_celestial_body(target_name) + self.target_combo['values'] = self.config_service.get_celestial_bodies() + self.target_combo.set(target_name) else: return - camera = self.camera_combo.currentText() - lens = self.lens_combo.currentText() - if not camera or not lens: - reply = QMessageBox.question(self, "Предупреждение", - "Камера или объектив не выбраны. Продолжить?", - QMessageBox.Yes | QMessageBox.No) - if reply == QMessageBox.No: + reply = messagebox.askyesno("Warning", "Camera or lens not selected. Continue?", + parent=self.root) + if not reply: return try: watch_path = Path(watch_folder) - - # Очищаем папку наблюдения от старых файлов FileService.clear_watch_folder(watch_path) camera_val = camera if camera else "Unknown" lens_val = lens if lens else "Unknown" - optics_value = lens - if optics_value.startswith("🔭 "): - optics_value = optics_value[2:] - elif optics_value.startswith("🪐 "): - optics_value = optics_value[2:] - self.config_service.set_last_lens(optics_value) - - self.session_service.start_session(watch_path, object_name, camera_val, lens_val) + self.session_service.start_session(watch_path, target_name, camera_val, lens_val) + self.current_target = target_name + self.current_session_folder = str(self.session_service.get_current_session().session_folder) self.config_service.set_last_camera(camera_val) self.config_service.set_last_lens(lens_val) - # Запускаем отслеживание success = self.watch_service.start(watch_path, self._on_file_received) if not success: - QMessageBox.critical(self, "Ошибка", "Не удалось запустить отслеживание папки") + messagebox.showerror("Error", "Failed to start watching folder!", parent=self.root) return self._set_running_state(True) - print(f"Отслеживание начато! Папка наблюдения: {watch_path}") - print(f"Папка сессии: {self.session_service.get_current_session().session_folder}") - except Exception as e: - QMessageBox.critical(self, "Ошибка", f"Не удалось начать сессию: {e}") + messagebox.showerror("Error", f"Failed to start session: {e}", parent=self.root) import traceback traceback.print_exc() @@ -521,114 +413,122 @@ class MainWindow(QMainWindow): return try: - watch_folder = Path(self.folder_entry.text()) - - print(f"Остановка сессии. Перемещаем файлы из {watch_folder}") - - # Перемещаем все оставшиеся файлы - moved_count = self.watch_service.move_all_existing_files(watch_folder, self._on_file_received) - print(f"Перемещено файлов: {moved_count}") - - # Останавливаем отслеживание + watch_folder = Path(self.folder_entry.get()) + self.watch_service.move_all_existing_files(watch_folder, self._on_file_received) self.watch_service.stop() - - # Завершаем сессию session = self.session_service.finish_session() - self._set_running_state(False) - - # Показываем диалог завершения self._show_session_end_dialog(session) - except Exception as e: - QMessageBox.critical(self, "Ошибка", f"Ошибка при завершении сессии: {e}") - import traceback - traceback.print_exc() + messagebox.showerror("Error", f"Error stopping session: {e}", parent=self.root) def set_new_object(self): if not self.running: - QMessageBox.critical(self, "Ошибка", "Сессия не активна") + messagebox.showerror("Error", "Session is not active!", parent=self.root) return - # Перемещаем все накопленные файлы в папку текущего объекта - watch_folder = Path(self.folder_entry.text()) - moved_count = self.watch_service.move_all_existing_files(watch_folder, self._on_file_received) + watch_folder = Path(self.folder_entry.get()) + self.watch_service.move_all_existing_files(watch_folder, self._on_file_received) - if moved_count > 0: - print(f"Перемещено файлов перед сменой объекта: {moved_count}") + dialog = tk.Toplevel(self.root) + dialog.title("New Target") + dialog.geometry("400x150") + dialog.transient(self.root) + dialog.grab_set() - new_object, ok = QInputDialog.getText(self, "Новый объект", "Введите название объекта:") + ttk.Label(dialog, text="Enter new target name:", font=('Segoe UI', 11)).pack(pady=20) - if ok and new_object and new_object.strip(): - new_name = new_object.strip() + entry = ttk.Entry(dialog, width=40) + entry.pack(pady=10) + entry.focus() - # Проверка, существует ли объект в списке - celestial_bodies = self.config_service.get_celestial_bodies() - if new_name not in celestial_bodies: - reply = QMessageBox.question(self, "Новый объект", - f"Объект '{new_name}' не найден в списке.\nДобавить его в список?", - QMessageBox.Yes | QMessageBox.No) - if reply == QMessageBox.Yes: - self.config_service.add_celestial_body(new_name) - self.object_combo.addItem(new_name) - else: - return + def confirm(): + new_target = entry.get().strip() + if new_target: + dialog.destroy() + self._create_new_target(new_target) + else: + messagebox.showwarning("Warning", "Please enter a target name!", parent=dialog) - self.session_service.create_new_object(new_name) - self.object_combo.setCurrentText(new_name) - QMessageBox.information(self, "Успех", f"Объект изменён на: {new_name}") + ttk.Button(dialog, text="OK", command=confirm).pack(pady=10) + dialog.bind('', lambda e: confirm()) + + self.root.wait_window(dialog) + + def _create_new_target(self, new_name): + celestial_bodies = self.config_service.get_celestial_bodies() + if new_name not in celestial_bodies: + reply = messagebox.askyesno("New Target", + f"Target '{new_name}' not found in list.\nAdd it to the list?", + parent=self.root) + if reply: + self.config_service.add_celestial_body(new_name) + self.target_combo['values'] = self.config_service.get_celestial_bodies() + else: + return + + self.session_service.create_new_object(new_name) + self.target_combo.set(new_name) + self.file_count = 0 + self.stats_label.configure(text="Files received: 0") def open_equipment_dialog(self): from ui.dialogs.equipment_dialog import EquipmentDialog - dialog = EquipmentDialog(self, self.config_service) - dialog.exec() + dialog = EquipmentDialog(self.root, self.config_service) + self.root.wait_window(dialog) - self.camera_combo.clear() - self.lens_combo.clear() - self.camera_combo.addItems(self.config_service.get_cameras()) - self.lens_combo.addItems(self.config_service.get_lenses()) + # Refresh comboboxes + cameras = self.config_service.get_cameras() + lenses = self.config_service.get_lenses() + telescopes = self.config_service.get_telescopes() + + all_optics = lenses + telescopes + + self.camera_combo['values'] = cameras + self.lens_combo['values'] = all_optics def open_celestial_dialog(self): from ui.dialogs.celestial_dialog import CelestialDialog - dialog = CelestialDialog(self, self.config_service) - dialog.exec() + dialog = CelestialDialog(self.root, self.config_service) + self.root.wait_window(dialog) - self.object_combo.clear() - self.object_combo.addItems(self.config_service.get_celestial_bodies()) + celestial_bodies = self.config_service.get_celestial_bodies() + self.target_combo['values'] = celestial_bodies + + def open_calibration_dialog(self): + from ui.dialogs.calibration_dialog import CalibrationDialog + dialog = CalibrationDialog(self.root, self.config_service) + self.root.wait_window(dialog) def open_session_folder(self): - if self.running and self.session_service.get_current_session(): - folder = self.session_service.get_current_session().session_folder - if folder and folder.exists(): - try: - if platform.system() == "Windows": - subprocess.Popen(['explorer', str(folder)]) - elif platform.system() == "Darwin": - subprocess.Popen(['open', str(folder)]) - else: - subprocess.Popen(['xdg-open', str(folder)]) - except Exception as e: - QMessageBox.critical(self, "Ошибка", f"Не удалось открыть папку: {e}") - else: - QMessageBox.critical(self, "Ошибка", "Папка сессии не найдена") + if self.running and self.current_session_folder: + try: + if platform.system() == "Windows": + subprocess.Popen(['explorer', self.current_session_folder]) + elif platform.system() == "Darwin": + subprocess.Popen(['open', self.current_session_folder]) + else: + subprocess.Popen(['xdg-open', self.current_session_folder]) + except Exception as e: + messagebox.showerror("Error", f"Failed to open folder: {e}", parent=self.root) else: - QMessageBox.information(self, "Информация", "Нет активной сессии") + messagebox.showinfo("Info", "No active session", parent=self.root) def show_instructions(self): from ui.dialogs.instructions_dialog import InstructionsDialog - dialog = InstructionsDialog(self) - dialog.exec() + InstructionsDialog(self.root) def show_info(self): - QMessageBox.about(self, "О программе", - "Astro Session Watcher\nВерсия: 0.3.0-alpha\n\n" - "Приложение для автоматической сортировки астрофотографий\n\n" - "Особенности:\n" - "• Автоматическое отслеживание новых файлов\n" - "• Сортировка по объектам съёмки\n" - "• Ведение детальных логов\n" - "• Сохранение истории оборудования\n\n" - "Разработчик: Vic Sergeev\n2026") + messagebox.showinfo("About", + "Astro Session Watcher v0.4.0\n\n" + "Application for astrophotographers\n\n" + "Features:\n" + "• Automatic file tracking\n" + "• Sorting by targets\n" + "• Session logging\n" + "• Equipment management\n\n" + "Made by Vic Sergeev\n2026", + parent=self.root) def _show_session_end_dialog(self, session): current_object = session.get_current_object() @@ -636,17 +536,18 @@ class MainWindow(QMainWindow): photo_count = current_object.photo_count if current_object else 0 session_folder = session.session_folder - msg_box = QMessageBox(self) - msg_box.setWindowTitle("Сессия завершена") - msg_box.setIcon(QMessageBox.Information) - msg_box.setText(f"Наблюдение остановлено\n\nСессия для объекта '{object_name}' завершена.\nПолучено файлов: {photo_count}") - msg_box.setInformativeText(f"Папка с данными:\n{session_folder}") + dialog = tk.Toplevel(self.root) + dialog.title("Session Completed") + dialog.geometry("500x250") + dialog.transient(self.root) + dialog.grab_set() - open_folder_btn = msg_box.addButton("📁 Открыть папку", QMessageBox.AcceptRole) - close_btn = msg_box.addButton("Закрыть", QMessageBox.RejectRole) + ttk.Label(dialog, text="✅ Session finished!", font=('Segoe UI', 14, 'bold')).pack(pady=15) + ttk.Label(dialog, text=f"Target: {object_name}").pack() + ttk.Label(dialog, text=f"Files received: {photo_count}").pack() + ttk.Label(dialog, text=f"Saved to: {session_folder}", wraplength=450).pack(pady=10) - msg_box.exec() - if msg_box.clickedButton() == open_folder_btn: + def open_folder(): if session_folder and session_folder.exists(): try: if platform.system() == "Windows": @@ -656,20 +557,23 @@ class MainWindow(QMainWindow): else: subprocess.Popen(['xdg-open', str(session_folder)]) except Exception as e: - QMessageBox.critical(self, "Ошибка", f"Не удалось открыть папку: {e}") + messagebox.showerror("Error", f"Failed to open folder: {e}", parent=dialog) + dialog.destroy() - def closeEvent(self, event): + def close(): + dialog.destroy() + + btn_frame = ttk.Frame(dialog) + btn_frame.pack(pady=20) + ttk.Button(btn_frame, text="Open Folder", command=open_folder).pack(side='left', padx=10) + ttk.Button(btn_frame, text="Close", command=close).pack(side='left', padx=10) + + def _on_closing(self): if self.running: - reply = QMessageBox.question(self, "Выход", - "Сессия активна. Остановить сессию и выйти?", - QMessageBox.Yes | QMessageBox.No) - if reply == QMessageBox.Yes: - try: - self.stop() - except: - pass - event.accept() - else: - event.ignore() + reply = messagebox.askyesno("Exit", "Session is active. Stop session and exit?", + parent=self.root) + if reply: + self.stop() + self.root.destroy() else: - event.accept() \ No newline at end of file + self.root.destroy() \ No newline at end of file