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 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
@@ -17,9 +28,9 @@
-
+ {
+ "associatedIndex": 8
+}
@@ -39,7 +50,7 @@
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
"SHARE_PROJECT_CONFIGURATION_FILES": "true",
"ai.playground.ignore.import.keys.banner.in.settings": "true",
- "git-widget-placeholder": "dev-pyside",
+ "git-widget-placeholder": "dev-tkinter-v2",
"ignore.virus.scanning.warn.message": "true",
"settings.editor.selected.configurable": "preferences.lookFeel",
"vue.rearranger.settings.migration": "true"
@@ -68,6 +79,6 @@
-
+
\ 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