working logic+working watching files+added calibration feature+instructions
This commit is contained in:
parent
09d181eba8
commit
97ed8217bf
25 changed files with 1743 additions and 192 deletions
|
|
@ -1,5 +1,8 @@
|
|||
from ui.dialogs.equipment_dialog import EquipmentDialog
|
||||
from ui.dialogs.celestial_dialog import CelestialDialog
|
||||
from ui.dialogs.instructions_dialog import InstructionsDialog
|
||||
from ui.dialogs.calibration_dialog import CalibrationDialog
|
||||
from ui.dialogs.calibration_type_dialog import CalibrationTypeDialog
|
||||
|
||||
__all__ = ['EquipmentDialog', 'CelestialDialog', 'InstructionsDialog']
|
||||
__all__ = ['EquipmentDialog', 'CelestialDialog', 'InstructionsDialog',
|
||||
'CalibrationDialog', 'CalibrationTypeDialog']
|
||||
Binary file not shown.
BIN
ui/dialogs/__pycache__/calibration_dialog.cpython-313.pyc
Normal file
BIN
ui/dialogs/__pycache__/calibration_dialog.cpython-313.pyc
Normal file
Binary file not shown.
BIN
ui/dialogs/__pycache__/calibration_type_dialog.cpython-313.pyc
Normal file
BIN
ui/dialogs/__pycache__/calibration_type_dialog.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
281
ui/dialogs/calibration_dialog.py
Normal file
281
ui/dialogs/calibration_dialog.py
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
"""
|
||||
CalibrationDialog - главный диалог калибровки
|
||||
С выбором камеры, папки и типа кадров
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
class CalibrationDialog(QDialog):
|
||||
"""Главное окно калибровки"""
|
||||
|
||||
def __init__(self, parent, config_service: ConfigService):
|
||||
super().__init__(parent)
|
||||
|
||||
self.config_service = config_service
|
||||
|
||||
self.setWindowTitle("🌑 Калибровочные кадры")
|
||||
self.setMinimumSize(600, 450)
|
||||
self.resize(650, 500)
|
||||
|
||||
self._create_ui()
|
||||
self._load_saved_settings()
|
||||
|
||||
# Таймер для мигания кнопки "Обзор"
|
||||
self._browse_blink_timer = None
|
||||
self._check_folder_path()
|
||||
|
||||
def _create_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(20)
|
||||
layout.setContentsMargins(25, 25, 25, 25)
|
||||
|
||||
# Заголовок
|
||||
title_label = QLabel("🌑 Калибровочные кадры")
|
||||
title_font = QFont()
|
||||
title_font.setPointSize(18)
|
||||
title_font.setBold(True)
|
||||
title_label.setFont(title_font)
|
||||
layout.addWidget(title_label)
|
||||
|
||||
# Основная сетка
|
||||
grid = QGridLayout()
|
||||
grid.setVerticalSpacing(15)
|
||||
grid.setHorizontalSpacing(15)
|
||||
|
||||
# Строка 0: Камера
|
||||
camera_label = QLabel("📷 Камера:")
|
||||
camera_label.setFont(QFont("", 10, QFont.Bold))
|
||||
grid.addWidget(camera_label, 0, 0)
|
||||
|
||||
self.camera_combo = QComboBox()
|
||||
self.camera_combo.setEditable(True)
|
||||
self.camera_combo.setMinimumWidth(250)
|
||||
grid.addWidget(self.camera_combo, 0, 1)
|
||||
|
||||
# Строка 1: Папка
|
||||
folder_label = QLabel("📁 Папка:")
|
||||
folder_label.setFont(QFont("", 10, QFont.Bold))
|
||||
grid.addWidget(folder_label, 1, 0)
|
||||
|
||||
folder_widget = QWidget()
|
||||
folder_layout = QHBoxLayout(folder_widget)
|
||||
folder_layout.setContentsMargins(0, 0, 0, 0)
|
||||
folder_layout.setSpacing(10)
|
||||
|
||||
self.folder_entry = QLineEdit()
|
||||
self.folder_entry.setPlaceholderText("Выберите папку для сохранения калибровочных кадров")
|
||||
folder_layout.addWidget(self.folder_entry)
|
||||
|
||||
self.browse_button = QPushButton("✨ Обзор")
|
||||
self.browse_button.setFixedWidth(100)
|
||||
self.browse_button.clicked.connect(self._browse_folder)
|
||||
folder_layout.addWidget(self.browse_button)
|
||||
|
||||
grid.addWidget(folder_widget, 1, 1)
|
||||
|
||||
layout.addLayout(grid)
|
||||
|
||||
# Разделитель
|
||||
separator = QFrame()
|
||||
separator.setFrameShape(QFrame.HLine)
|
||||
separator.setStyleSheet("background-color: #333333; max-height: 1px;")
|
||||
layout.addWidget(separator)
|
||||
|
||||
# Кнопки типов кадров
|
||||
types_layout = QHBoxLayout()
|
||||
types_layout.setSpacing(20)
|
||||
types_layout.setAlignment(Qt.AlignCenter)
|
||||
|
||||
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.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.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'))
|
||||
|
||||
types_layout.addWidget(self.bias_btn)
|
||||
types_layout.addWidget(self.dark_btn)
|
||||
types_layout.addWidget(self.flat_btn)
|
||||
|
||||
layout.addLayout(types_layout)
|
||||
|
||||
# Совет
|
||||
tips_frame = QFrame()
|
||||
tips_frame.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #2d2d2d;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
}
|
||||
""")
|
||||
tips_layout = QVBoxLayout(tips_frame)
|
||||
|
||||
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)
|
||||
|
||||
def _load_saved_settings(self):
|
||||
"""Загружает сохранённые камеры"""
|
||||
cameras = self.config_service.get_cameras()
|
||||
if cameras:
|
||||
self.camera_combo.addItems(cameras)
|
||||
|
||||
last_camera = self.config_service.get_last_camera()
|
||||
if last_camera and last_camera in cameras:
|
||||
self.camera_combo.setCurrentText(last_camera)
|
||||
|
||||
def _browse_folder(self):
|
||||
"""Выбор папки для калибровочных кадров"""
|
||||
folder = QFileDialog.getExistingDirectory(self, "Выберите папку для калибровочных кадров")
|
||||
if folder:
|
||||
self.folder_entry.setText(folder)
|
||||
self._stop_browse_blinking()
|
||||
|
||||
def _check_folder_path(self):
|
||||
"""Проверяет, заполнено ли поле пути и запускает мигание если нет"""
|
||||
if not self.folder_entry.text():
|
||||
self._start_browse_blinking()
|
||||
else:
|
||||
self._stop_browse_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 _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 _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;
|
||||
}
|
||||
""")
|
||||
|
||||
def _open_calibration_type(self, cal_type: str):
|
||||
"""Открывает дочернее окно для выбранного типа калибровки"""
|
||||
if not self.folder_entry.text():
|
||||
QMessageBox.warning(self, "Внимание", "Сначала выберите папку для сохранения!")
|
||||
self._start_browse_blinking()
|
||||
return
|
||||
|
||||
camera_name = self.camera_combo.currentText()
|
||||
if not camera_name:
|
||||
QMessageBox.warning(self, "Внимание", "Введите или выберите название камеры!")
|
||||
return
|
||||
|
||||
from ui.dialogs.calibration_type_dialog import CalibrationTypeDialog
|
||||
dialog = CalibrationTypeDialog(
|
||||
self,
|
||||
cal_type,
|
||||
self.folder_entry.text(),
|
||||
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()
|
||||
730
ui/dialogs/calibration_type_dialog.py
Normal file
730
ui/dialogs/calibration_type_dialog.py
Normal file
|
|
@ -0,0 +1,730 @@
|
|||
"""
|
||||
CalibrationTypeDialog - диалог для конкретного типа калибровки
|
||||
Dark / Bias / Flat с прогрессом, авто-остановкой и профилями
|
||||
"""
|
||||
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):
|
||||
"""Диалог для съёмки калибровочных кадров определённого типа"""
|
||||
|
||||
# Сигнал для безопасного обновления 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):
|
||||
super().__init__(parent)
|
||||
|
||||
self.cal_type = cal_type # 'bias', 'dark', 'flat'
|
||||
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.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._create_ui()
|
||||
self._load_optics()
|
||||
self._update_recommendations()
|
||||
|
||||
def _get_title(self) -> str:
|
||||
titles = {
|
||||
'bias': '⚪ BIAS (Кадры смещения)',
|
||||
'dark': '🌑 DARK (Тёмные кадры)',
|
||||
'flat': '📖 FLAT (Плоские поля)'
|
||||
}
|
||||
return titles.get(self.cal_type, 'Калибровочные кадры')
|
||||
|
||||
def _get_default_settings(self) -> dict:
|
||||
"""Возвращает настройки по умолчанию для типа калибровки"""
|
||||
base = {
|
||||
'bias': {
|
||||
'iso_values': [800, 1600, 3200],
|
||||
'default_iso': 800,
|
||||
'count': 50,
|
||||
'min_count': 30,
|
||||
'max_count': 100,
|
||||
'recommended_count': 50,
|
||||
},
|
||||
'dark': {
|
||||
'iso_values': [800, 1600, 3200],
|
||||
'default_iso': 800,
|
||||
'exposure_values': [30, 60, 120, 180, 300],
|
||||
'default_exposure': 120,
|
||||
'count': 20,
|
||||
'min_count': 10,
|
||||
'max_count': 50,
|
||||
'recommended_count': 20,
|
||||
},
|
||||
'flat': {
|
||||
'iso_values': [800, 1600, 3200],
|
||||
'default_iso': 800,
|
||||
'aperture_values': ['f/2.8', 'f/4', 'f/5.6', 'f/8'],
|
||||
'count': 30,
|
||||
'min_count': 20,
|
||||
'max_count': 60,
|
||||
'recommended_count': 30,
|
||||
}
|
||||
}
|
||||
return base.get(self.cal_type, {})
|
||||
|
||||
def _create_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(15)
|
||||
layout.setContentsMargins(20, 20, 20, 20)
|
||||
|
||||
# Заголовок с кнопкой справки
|
||||
header_layout = QHBoxLayout()
|
||||
|
||||
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)
|
||||
|
||||
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()
|
||||
|
||||
layout.addLayout(header_layout)
|
||||
|
||||
# Группа параметров
|
||||
params_group = QGroupBox("⚙️ Параметры съёмки")
|
||||
params_layout = QGridLayout(params_group)
|
||||
params_layout.setVerticalSpacing(12)
|
||||
params_layout.setHorizontalSpacing(15)
|
||||
|
||||
row = 0
|
||||
|
||||
# ISO
|
||||
iso_label = QLabel("ISO:")
|
||||
iso_label.setFont(QFont("", 10, QFont.Bold))
|
||||
params_layout.addWidget(iso_label, row, 0)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
if self.cal_type == 'dark':
|
||||
exposure_label = QLabel("Выдержка (сек):")
|
||||
exposure_label.setFont(QFont("", 10, QFont.Bold))
|
||||
params_layout.addWidget(exposure_label, row, 0)
|
||||
|
||||
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)
|
||||
if self.cal_type == 'flat':
|
||||
optics_label = QLabel("Оптика:")
|
||||
optics_label.setFont(QFont("", 10, QFont.Bold))
|
||||
params_layout.addWidget(optics_label, row, 0)
|
||||
|
||||
self.optics_combo = QComboBox()
|
||||
self.optics_combo.setEditable(True)
|
||||
params_layout.addWidget(self.optics_combo, row, 1, 1, 2)
|
||||
|
||||
row += 1
|
||||
|
||||
aperture_label = QLabel("Диафрагма:")
|
||||
aperture_label.setFont(QFont("", 10, QFont.Bold))
|
||||
params_layout.addWidget(aperture_label, row, 0)
|
||||
|
||||
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)
|
||||
|
||||
row += 1
|
||||
|
||||
telescope_hint = QLabel("💡 Для телескопов диафрагма фиксированная и выбирается автоматически")
|
||||
telescope_hint.setStyleSheet("color: #888888; font-size: 10px;")
|
||||
params_layout.addWidget(telescope_hint, row, 0, 1, 3)
|
||||
|
||||
row += 1
|
||||
|
||||
# Количество кадров
|
||||
count_label = QLabel("Количество кадров:")
|
||||
count_label.setFont(QFont("", 10, QFont.Bold))
|
||||
params_layout.addWidget(count_label, row, 0)
|
||||
|
||||
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.recommended_label = QLabel(f"(рекомендуется {self.settings['recommended_count']})")
|
||||
self.recommended_label.setStyleSheet("color: #888888;")
|
||||
params_layout.addWidget(self.recommended_label, row, 2)
|
||||
|
||||
layout.addWidget(params_group)
|
||||
|
||||
# Группа рекомендаций
|
||||
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)
|
||||
|
||||
self.tips_text = QLabel()
|
||||
self.tips_text.setWordWrap(True)
|
||||
self.tips_text.setStyleSheet("color: #FFD700; padding: 5px;")
|
||||
tips_layout.addWidget(self.tips_text)
|
||||
|
||||
layout.addWidget(tips_group)
|
||||
|
||||
# Группа прогресса
|
||||
self.progress_group = QGroupBox("📊 Прогресс съёмки")
|
||||
self.progress_group.setVisible(False)
|
||||
progress_layout = QVBoxLayout(self.progress_group)
|
||||
|
||||
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._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}")
|
||||
|
||||
self.optics_combo.addItems(all_optics)
|
||||
|
||||
def on_optics_changed():
|
||||
current = self.optics_combo.currentText()
|
||||
if current.startswith("🪐"):
|
||||
self.aperture_combo.setEnabled(False)
|
||||
match = re.search(r'f/(\d+\.?\d*)', current)
|
||||
if match:
|
||||
self.aperture_combo.setCurrentText(f"f/{match.group(1)}")
|
||||
else:
|
||||
self.aperture_combo.setEnabled(True)
|
||||
|
||||
if all_optics:
|
||||
self.optics_combo.currentTextChanged.connect(on_optics_changed)
|
||||
|
||||
def _update_save_path(self):
|
||||
"""Обновляет отображение пути сохранения"""
|
||||
iso = int(self.iso_combo.currentText())
|
||||
|
||||
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()
|
||||
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()
|
||||
invalid_chars = '<>:"/\\|?*'
|
||||
for char in invalid_chars:
|
||||
optics_name = optics_name.replace(char, '_')
|
||||
date_str = datetime.now().strftime("%Y-%m-%d")
|
||||
path = self.base_folder / "Calibration" / self.camera_name / "Flat" / optics_name / date_str
|
||||
else:
|
||||
path = self.base_folder
|
||||
|
||||
self.save_path_label.setText(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._update_save_path()
|
||||
|
||||
def _start_capture(self):
|
||||
"""Начинает съёмку калибровочных кадров"""
|
||||
self.target_count = self.count_spin.value()
|
||||
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}")
|
||||
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} кадров")
|
||||
|
||||
# Меняем кнопки
|
||||
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)
|
||||
|
||||
self.is_capturing = True
|
||||
|
||||
# Получаем папку наблюдения
|
||||
watch_folder = self._get_watch_folder()
|
||||
print(f"Получена папка наблюдения: {watch_folder}")
|
||||
|
||||
if not watch_folder:
|
||||
QMessageBox.critical(self, "Ошибка",
|
||||
"Не удалось определить папку наблюдения!\nУбедитесь, что вы выбрали папку в главном окне.")
|
||||
self._stop_capture()
|
||||
return
|
||||
|
||||
if not watch_folder.exists():
|
||||
QMessageBox.critical(self, "Ошибка", f"Папка наблюдения не существует:\n{watch_folder}")
|
||||
self._stop_capture()
|
||||
return
|
||||
|
||||
# Очищаем папку наблюдения от старых файлов
|
||||
FileService.clear_watch_folder(watch_folder)
|
||||
|
||||
# Создаём НОВЫЙ WatchService для калибровки
|
||||
self._calibration_watch_service = WatchService()
|
||||
|
||||
# Функция обратного вызова при получении файла (выполняется в потоке WatchService)
|
||||
def on_file_received(file_path: 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)
|
||||
|
||||
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 not success:
|
||||
QMessageBox.critical(self, "Ошибка", "Не удалось запустить отслеживание папки!")
|
||||
self._stop_capture()
|
||||
return
|
||||
|
||||
self.progress_status.setText(f"Отслеживается папка: {watch_folder}\nОжидание новых файлов...")
|
||||
|
||||
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 _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:
|
||||
"""Обрабатывает файл из папки наблюдения"""
|
||||
if not FileService.is_photo(file_path):
|
||||
print(f"Файл {file_path.name} не является фото, пропускаем")
|
||||
return False
|
||||
|
||||
try:
|
||||
target_folder.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
timestamp = datetime.now()
|
||||
date_str = timestamp.strftime("%Y-%m-%d")
|
||||
time_str = timestamp.strftime("%H-%M-%S")
|
||||
suffix = file_path.suffix
|
||||
|
||||
if self.cal_type == 'bias':
|
||||
iso = self.iso_combo.currentText()
|
||||
prefix = f"Bias_{self.camera_name}_ISO{iso}"
|
||||
elif self.cal_type == 'dark':
|
||||
iso = self.iso_combo.currentText()
|
||||
exposure = self.exposure_combo.currentText()
|
||||
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()
|
||||
invalid_chars = '<>:"/\\|?*'
|
||||
for char in invalid_chars:
|
||||
optics_name = optics_name.replace(char, '_')
|
||||
aperture = self.aperture_combo.currentText()
|
||||
prefix = f"Flat_{optics_name}_{aperture}"
|
||||
else:
|
||||
prefix = "Calibration"
|
||||
|
||||
for char in '<>:"/\\|?*':
|
||||
prefix = prefix.replace(char, '_')
|
||||
|
||||
new_filename = f"{prefix}_{date_str}_{time_str}{suffix}"
|
||||
target_path = target_folder / new_filename
|
||||
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}")
|
||||
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
|
||||
|
||||
self.start_btn.setVisible(True)
|
||||
self.stop_btn.setVisible(False)
|
||||
self.back_btn.setEnabled(True)
|
||||
|
||||
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_status.setText("Съёмка остановлена")
|
||||
|
||||
# Скрываем группу прогресса через 2 секунды
|
||||
QTimer.singleShot(2000, lambda: self.progress_group.setVisible(False))
|
||||
|
||||
def _on_back_clicked(self):
|
||||
"""Обработчик кнопки 'Назад'"""
|
||||
if self.is_capturing:
|
||||
QMessageBox.warning(self, "Внимание", "Сначала остановите съёмку!")
|
||||
return
|
||||
self.reject()
|
||||
|
||||
def _on_stop_clicked(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:
|
||||
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:
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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"
|
||||
"• Температура не важна"
|
||||
)
|
||||
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 делает кадры бесполезными!"
|
||||
)
|
||||
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"
|
||||
"• Снимайте в конце сессии"
|
||||
)
|
||||
|
||||
QMessageBox.information(self, "Справка", help_text)
|
||||
|
||||
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()
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
"""
|
||||
EquipmentDialog - диалог управления оборудованием (камеры и объективы)
|
||||
Аналог EquipmentDialogController из JavaFX версии
|
||||
EquipmentDialog - диалог управления оборудованием
|
||||
Камеры, объективы и телескопы
|
||||
"""
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QListWidget,
|
||||
QPushButton, QInputDialog, QMessageBox, QListWidgetItem
|
||||
QPushButton, QInputDialog, QMessageBox, QWidget, QTabWidget,
|
||||
QFormLayout, QDoubleSpinBox, QSpinBox, QLineEdit
|
||||
)
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QFont
|
||||
|
|
@ -13,26 +14,27 @@ from services.config_service import ConfigService
|
|||
|
||||
|
||||
class EquipmentDialog(QDialog):
|
||||
"""Диалог для управления списками камер и объективов"""
|
||||
"""Диалог для управления оборудованием"""
|
||||
|
||||
def __init__(self, parent, config_service: ConfigService):
|
||||
super().__init__(parent)
|
||||
|
||||
self.config_service = config_service
|
||||
self.setWindowTitle("Управление оборудованием")
|
||||
self.setMinimumSize(600, 400)
|
||||
self.resize(650, 450)
|
||||
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._create_ui()
|
||||
self._update_cameras_list()
|
||||
self._update_lenses_list()
|
||||
self._update_telescopes_list()
|
||||
|
||||
def _create_ui(self):
|
||||
"""Создаёт интерфейс диалога"""
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(15)
|
||||
layout.setContentsMargins(20, 20, 20, 20)
|
||||
|
|
@ -46,64 +48,22 @@ class EquipmentDialog(QDialog):
|
|||
title_label.setAlignment(Qt.AlignCenter)
|
||||
layout.addWidget(title_label)
|
||||
|
||||
# Контейнер для двух колонок
|
||||
columns_layout = QHBoxLayout()
|
||||
columns_layout.setSpacing(20)
|
||||
# Используем QTabWidget для трёх вкладок
|
||||
tab_widget = QTabWidget()
|
||||
|
||||
# Левая колонка - Камеры
|
||||
left_layout = QVBoxLayout()
|
||||
# Вкладка: Камеры
|
||||
cameras_tab = self._create_cameras_tab()
|
||||
tab_widget.addTab(cameras_tab, "📷 Камеры")
|
||||
|
||||
cameras_label = QLabel("Камеры")
|
||||
cameras_font = QFont()
|
||||
cameras_font.setPointSize(12)
|
||||
cameras_font.setBold(True)
|
||||
cameras_label.setFont(cameras_font)
|
||||
left_layout.addWidget(cameras_label)
|
||||
# Вкладка: Объективы
|
||||
lenses_tab = self._create_lenses_tab()
|
||||
tab_widget.addTab(lenses_tab, "🔭 Объективы")
|
||||
|
||||
self.cameras_list = QListWidget()
|
||||
self.cameras_list.itemClicked.connect(lambda item: self._select_camera(item.text()))
|
||||
left_layout.addWidget(self.cameras_list)
|
||||
# Вкладка: Телескопы
|
||||
telescopes_tab = self._create_telescopes_tab()
|
||||
tab_widget.addTab(telescopes_tab, "🪐 Телескопы")
|
||||
|
||||
cameras_buttons_layout = QHBoxLayout()
|
||||
|
||||
add_camera_btn = QPushButton("➕ Добавить")
|
||||
add_camera_btn.clicked.connect(self._add_camera)
|
||||
cameras_buttons_layout.addWidget(add_camera_btn)
|
||||
|
||||
self.remove_camera_btn = QPushButton("❌ Удалить")
|
||||
self.remove_camera_btn.setEnabled(False)
|
||||
self.remove_camera_btn.clicked.connect(self._remove_camera)
|
||||
cameras_buttons_layout.addWidget(self.remove_camera_btn)
|
||||
|
||||
left_layout.addLayout(cameras_buttons_layout)
|
||||
|
||||
# Правая колонка - Объективы
|
||||
right_layout = QVBoxLayout()
|
||||
|
||||
lenses_label = QLabel("Объективы")
|
||||
lenses_label.setFont(cameras_font)
|
||||
right_layout.addWidget(lenses_label)
|
||||
|
||||
self.lenses_list = QListWidget()
|
||||
self.lenses_list.itemClicked.connect(lambda item: self._select_lens(item.text()))
|
||||
right_layout.addWidget(self.lenses_list)
|
||||
|
||||
lenses_buttons_layout = QHBoxLayout()
|
||||
|
||||
add_lens_btn = QPushButton("➕ Добавить")
|
||||
add_lens_btn.clicked.connect(self._add_lens)
|
||||
lenses_buttons_layout.addWidget(add_lens_btn)
|
||||
|
||||
self.remove_lens_btn = QPushButton("❌ Удалить")
|
||||
self.remove_lens_btn.setEnabled(False)
|
||||
self.remove_lens_btn.clicked.connect(self._remove_lens)
|
||||
lenses_buttons_layout.addWidget(self.remove_lens_btn)
|
||||
|
||||
right_layout.addLayout(lenses_buttons_layout)
|
||||
|
||||
columns_layout.addLayout(left_layout)
|
||||
columns_layout.addLayout(right_layout)
|
||||
layout.addLayout(columns_layout)
|
||||
layout.addWidget(tab_widget)
|
||||
|
||||
# Кнопка закрытия
|
||||
close_btn = QPushButton("Закрыть")
|
||||
|
|
@ -113,90 +73,289 @@ class EquipmentDialog(QDialog):
|
|||
close_layout.addWidget(close_btn)
|
||||
layout.addLayout(close_layout)
|
||||
|
||||
def _create_cameras_tab(self) -> QWidget:
|
||||
"""Создаёт вкладку с камерами"""
|
||||
tab = QWidget()
|
||||
layout = QVBoxLayout(tab)
|
||||
|
||||
# Список камер
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
def _select_lens(self, lens: str):
|
||||
"""Выделяет объектив в списке"""
|
||||
self._selected_lens = lens
|
||||
self.remove_lens_btn.setEnabled(True)
|
||||
|
||||
# Снимаем выделение с камер
|
||||
self.cameras_list.clearSelection()
|
||||
self._selected_camera = None
|
||||
self.remove_camera_btn.setEnabled(False)
|
||||
self.remove_telescope_btn.setEnabled(False)
|
||||
|
||||
def _add_camera(self):
|
||||
"""Добавляет новую камеру"""
|
||||
new_camera, ok = QInputDialog.getText(self, "Добавить камеру", "Введите название камеры:")
|
||||
if ok and new_camera and new_camera.strip():
|
||||
new_name = new_camera.strip()
|
||||
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}' добавлена")
|
||||
|
||||
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)
|
||||
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()
|
||||
QMessageBox.information(self, "Успех", f"Камера '{self._selected_camera}' удалена")
|
||||
|
||||
# ===== Методы для объективов =====
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
def _add_lens(self):
|
||||
"""Добавляет новый объектив"""
|
||||
new_lens, ok = QInputDialog.getText(self, "Добавить объектив", "Введите название объектива:")
|
||||
if ok and new_lens and new_lens.strip():
|
||||
new_name = new_lens.strip()
|
||||
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}' добавлен")
|
||||
|
||||
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)
|
||||
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()
|
||||
QMessageBox.information(self, "Успех", f"Объектив '{self._selected_lens}' удалён")
|
||||
|
||||
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 new_name in self.lenses:
|
||||
QMessageBox.warning(self, "Ошибка", "Такой объектив уже существует!")
|
||||
return
|
||||
idx = self.lenses.index(self._selected_lens)
|
||||
self.lenses[idx] = new_name
|
||||
# Обновляем в конфиге (пока просто удаляем старый и добавляем новый)
|
||||
self.config_service.remove_lens(self._selected_lens)
|
||||
self.config_service.add_lens(new_name)
|
||||
self._update_lenses_list()
|
||||
|
||||
# ===== Методы для телескопов =====
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
def _add_telescope(self):
|
||||
"""Добавляет телескоп с указанием диафрагмы (фиксированной)"""
|
||||
dialog = QDialog(self)
|
||||
dialog.setWindowTitle("Добавить телескоп")
|
||||
dialog.setMinimumWidth(400)
|
||||
|
||||
layout = QVBoxLayout(dialog)
|
||||
|
||||
form_layout = QFormLayout()
|
||||
|
||||
name_edit = QLineEdit()
|
||||
name_edit.setPlaceholderText("例如: Celestron 8\"")
|
||||
form_layout.addRow("Название:", name_edit)
|
||||
|
||||
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)
|
||||
|
||||
focal_spin = QSpinBox()
|
||||
focal_spin.setRange(100, 5000)
|
||||
focal_spin.setSingleStep(50)
|
||||
focal_spin.setSuffix(" мм")
|
||||
form_layout.addRow("Фокусное расстояние:", focal_spin)
|
||||
|
||||
diameter_spin = QSpinBox()
|
||||
diameter_spin.setRange(50, 500)
|
||||
diameter_spin.setSingleStep(10)
|
||||
diameter_spin.setSuffix(" мм")
|
||||
form_layout.addRow("Диаметр объектива:", diameter_spin)
|
||||
|
||||
layout.addLayout(form_layout)
|
||||
|
||||
buttons_layout = QHBoxLayout()
|
||||
ok_btn = QPushButton("OK")
|
||||
cancel_btn = QPushButton("Отмена")
|
||||
buttons_layout.addWidget(ok_btn)
|
||||
buttons_layout.addWidget(cancel_btn)
|
||||
layout.addLayout(buttons_layout)
|
||||
|
||||
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}' добавлен")
|
||||
|
||||
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()
|
||||
|
||||
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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue