reworked with tkinter, UI improements needed

This commit is contained in:
Vic Sergeev 2026-05-09 19:44:16 +03:00
parent d3878857af
commit 3ae07ca289
26 changed files with 1488 additions and 1751 deletions

View file

@ -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()
self.wait_window(dialog)

File diff suppressed because it is too large Load diff

View file

@ -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('<<ListboxSelect>>', 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}'")
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('<Return>', lambda e: save())

View file

@ -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('<<ListboxSelect>>', 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('<Return>', 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('<Return>', 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('<<ListboxSelect>>', 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('<Return>', 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('<Return>', 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('<<ListboxSelect>>', 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()
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)

View file

@ -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)
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}')