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

23
.idea/workspace.xml generated
View file

@ -1,7 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="40c00e43-cac1-46a2-8996-291e69775bbb" name="Changes" comment="" /> <list default="true" id="40c00e43-cac1-46a2-8996-291e69775bbb" name="Changes" comment="">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/astro_settings.json" beforeDir="false" afterPath="$PROJECT_DIR$/astro_settings.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/celestial_bodies.json" beforeDir="false" afterPath="$PROJECT_DIR$/celestial_bodies.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/main.py" beforeDir="false" afterPath="$PROJECT_DIR$/main.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/ui/dialogs/calibration_dialog.py" beforeDir="false" afterPath="$PROJECT_DIR$/ui/dialogs/calibration_dialog.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/ui/dialogs/calibration_type_dialog.py" beforeDir="false" afterPath="$PROJECT_DIR$/ui/dialogs/calibration_type_dialog.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/ui/dialogs/celestial_dialog.py" beforeDir="false" afterPath="$PROJECT_DIR$/ui/dialogs/celestial_dialog.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/ui/dialogs/equipment_dialog.py" beforeDir="false" afterPath="$PROJECT_DIR$/ui/dialogs/equipment_dialog.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/ui/dialogs/instructions_dialog.py" beforeDir="false" afterPath="$PROJECT_DIR$/ui/dialogs/instructions_dialog.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/ui/main_window.py" beforeDir="false" afterPath="$PROJECT_DIR$/ui/main_window.py" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" /> <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
@ -17,9 +28,9 @@
<component name="Git.Settings"> <component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" /> <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component> </component>
<component name="ProjectColorInfo"><![CDATA[{ <component name="ProjectColorInfo">{
"associatedIndex": 8 &quot;associatedIndex&quot;: 8
}]]></component> }</component>
<component name="ProjectId" id="3DOCpPEAGgO0as5zvcSEwPDpHTU" /> <component name="ProjectId" id="3DOCpPEAGgO0as5zvcSEwPDpHTU" />
<component name="ProjectLevelVcsManager"> <component name="ProjectLevelVcsManager">
<ConfirmationsSetting value="2" id="Add" /> <ConfirmationsSetting value="2" id="Add" />
@ -39,7 +50,7 @@
"RunOnceActivity.typescript.service.memoryLimit.init": "true", "RunOnceActivity.typescript.service.memoryLimit.init": "true",
"SHARE_PROJECT_CONFIGURATION_FILES": "true", "SHARE_PROJECT_CONFIGURATION_FILES": "true",
"ai.playground.ignore.import.keys.banner.in.settings": "true", "ai.playground.ignore.import.keys.banner.in.settings": "true",
"git-widget-placeholder": "dev-pyside", "git-widget-placeholder": "dev-tkinter-v2",
"ignore.virus.scanning.warn.message": "true", "ignore.virus.scanning.warn.message": "true",
"settings.editor.selected.configurable": "preferences.lookFeel", "settings.editor.selected.configurable": "preferences.lookFeel",
"vue.rearranger.settings.migration": "true" "vue.rearranger.settings.migration": "true"
@ -68,6 +79,6 @@
<option name="version" value="3" /> <option name="version" value="3" />
</component> </component>
<component name="com.intellij.coverage.CoverageDataManagerImpl"> <component name="com.intellij.coverage.CoverageDataManagerImpl">
<SUITE FILE_PATH="coverage/AstroSessionWatcher$main.coverage" NAME="main Coverage Results" MODIFIED="1778176299697" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" /> <SUITE FILE_PATH="coverage/AstroSessionWatcher$main.coverage" NAME="main Coverage Results" MODIFIED="1778343435220" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
</component> </component>
</project> </project>

View file

@ -1,13 +1,12 @@
{ {
"cameras": [ "cameras": [
"Canon 40D", "Canon 40D",
"Canon 400D", "Canon 400D"
"Canon 500D"
], ],
"lenses": [ "lenses": [
"MTO-500A", "MTO-500A",
"Юпитер-21м", "Tamron 18-200mm",
"Tamron 18-200mm" "Юпитер-21м 200мм"
], ],
"telescopes": [ "telescopes": [
"Celestron Astromaster 130 (f/5.0, F=650mm, D=130mm)" "Celestron Astromaster 130 (f/5.0, F=650mm, D=130mm)"

View file

@ -10,5 +10,6 @@
"M89", "M89",
"Венера", "Венера",
"Меркурий", "Меркурий",
"Нептун" "Нептун",
"Saturn"
] ]

24
main.py
View file

@ -1,32 +1,22 @@
""" """
Astro Session Watcher - Главный входной файл Astro Session Watcher v0.4.0 - tkinter версия
Приложение для астрофотографов с отслеживанием файлов и сортировкой по объектам
""" """
import tkinter as tk
from tkinter import ttk
import sys import sys
import os import os
from pathlib import Path
# Добавляем корневую директорию в путь # Добавляем корневую директорию в путь
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from PySide6.QtWidgets import QApplication
from ui.main_window import MainWindow from ui.main_window import MainWindow
def main(): def main():
"""Точка входа в приложение""" root = tk.Tk()
app = QApplication(sys.argv) app = MainWindow(root)
root.mainloop()
# Устанавливаем стиль Fusion для современного вида
app.setStyle("Fusion")
# Тёмная палитра
app.setPalette(app.style().standardPalette())
window = MainWindow()
window.show()
sys.exit(app.exec())
if __name__ == "__main__": if __name__ == "__main__":

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) super().__init__(parent)
self.parent = parent
self.config_service = config_service self.config_service = config_service
self._blink_active = False
self._blink_after_id = None
self.setWindowTitle("🌑 Калибровочные кадры") self.title("Calibration Frames")
self.setMinimumSize(600, 450) self.geometry("650x500")
self.resize(650, 500) self.minsize(600, 450)
self.transient(parent)
self.grab_set()
self._create_ui() self._create_ui()
self._load_saved_settings() self._load_saved_settings()
# Таймер для мигания кнопки "Обзор"
self._browse_blink_timer = None
self._check_folder_path() self._check_folder_path()
self._center_window()
def _create_ui(self): def _create_ui(self):
layout = QVBoxLayout(self) # Main frame
layout.setSpacing(20) main_frame = ttk.Frame(self, padding="25")
layout.setContentsMargins(25, 25, 25, 25) main_frame.pack(fill='both', expand=True)
# Заголовок # Title
title_label = QLabel("🌑 Калибровочные кадры") title_label = ttk.Label(main_frame, text="Calibration Frames", font=('Segoe UI', 18, 'bold'))
title_font = QFont() title_label.pack(pady=(0, 15))
title_font.setPointSize(18)
title_font.setBold(True)
title_label.setFont(title_font)
layout.addWidget(title_label)
# Основная сетка # Separator
grid = QGridLayout() ttk.Separator(main_frame, orient='horizontal').pack(fill='x', pady=(0, 15))
grid.setVerticalSpacing(15)
grid.setHorizontalSpacing(15)
# Строка 0: Камера # Camera selection
camera_label = QLabel("📷 Камера:") camera_frame = ttk.Frame(main_frame)
camera_label.setFont(QFont("", 10, QFont.Bold)) camera_frame.pack(fill='x', pady=5)
grid.addWidget(camera_label, 0, 0)
self.camera_combo = QComboBox() ttk.Label(camera_frame, text="Camera:", font=('Segoe UI', 10, 'bold')).pack(side='left', padx=(0, 10))
self.camera_combo.setEditable(True)
self.camera_combo.setMinimumWidth(250)
grid.addWidget(self.camera_combo, 0, 1)
# Строка 1: Папка self.camera_combo = ttk.Combobox(camera_frame, width=30)
folder_label = QLabel("📁 Папка:") self.camera_combo.pack(side='left', fill='x', expand=True)
folder_label.setFont(QFont("", 10, QFont.Bold))
grid.addWidget(folder_label, 1, 0)
folder_widget = QWidget() # Folder selection
folder_layout = QHBoxLayout(folder_widget) folder_frame = ttk.Frame(main_frame)
folder_layout.setContentsMargins(0, 0, 0, 0) folder_frame.pack(fill='x', pady=5)
folder_layout.setSpacing(10)
self.folder_entry = QLineEdit() ttk.Label(folder_frame, text="Folder:", font=('Segoe UI', 10, 'bold')).pack(side='left', padx=(0, 10))
self.folder_entry.setPlaceholderText("Выберите папку для сохранения калибровочных кадров")
folder_layout.addWidget(self.folder_entry)
self.browse_button = QPushButton("✨ Обзор") folder_input_frame = ttk.Frame(folder_frame)
self.browse_button.setFixedWidth(100) folder_input_frame.pack(side='left', fill='x', expand=True)
self.browse_button.clicked.connect(self._browse_folder)
folder_layout.addWidget(self.browse_button)
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
separator = QFrame() ttk.Separator(main_frame, orient='horizontal').pack(fill='x', pady=15)
separator.setFrameShape(QFrame.HLine)
separator.setStyleSheet("background-color: #333333; max-height: 1px;")
layout.addWidget(separator)
# Кнопки типов кадров # Type buttons
types_layout = QHBoxLayout() types_frame = ttk.Frame(main_frame)
types_layout.setSpacing(20) types_frame.pack(pady=10)
types_layout.setAlignment(Qt.AlignCenter)
self.bias_btn = QPushButton("⚪ BIAS") self.bias_btn = tk.Button(types_frame, text="BIAS", font=('Segoe UI', 12, 'bold'),
self.bias_btn.setFixedSize(120, 50) bg='#2196F3', fg='white', activebackground='#1976D2',
self.bias_btn.setStyleSheet(""" width=12, height=2,
QPushButton { command=lambda: self._open_calibration_type('bias'))
background-color: #2196F3; self.bias_btn.pack(side='left', padx=10)
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 = tk.Button(types_frame, text="DARK", font=('Segoe UI', 12, 'bold'),
self.dark_btn.setFixedSize(120, 50) bg='#9C27B0', fg='white', activebackground='#7B1FA2',
self.dark_btn.setStyleSheet(""" width=12, height=2,
QPushButton { command=lambda: self._open_calibration_type('dark'))
background-color: #9C27B0; self.dark_btn.pack(side='left', padx=10)
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 = tk.Button(types_frame, text="FLAT", font=('Segoe UI', 12, 'bold'),
self.flat_btn.setFixedSize(120, 50) bg='#4CAF50', fg='white', activebackground='#388E3C',
self.flat_btn.setStyleSheet(""" width=12, height=2,
QPushButton { command=lambda: self._open_calibration_type('flat'))
background-color: #4CAF50; self.flat_btn.pack(side='left', padx=10)
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) # Tips frame
types_layout.addWidget(self.dark_btn) tips_frame = tk.Frame(main_frame, bg='#2d2d2d', relief='groove', bd=1)
types_layout.addWidget(self.flat_btn) 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))
# Совет self.tips_label = tk.Label(tips_frame,
tips_frame = QFrame() 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",
tips_frame.setStyleSheet(""" bg='#2d2d2d', fg='#e0e0e0', justify='left', font=('Segoe UI', 9))
QFrame { self.tips_label.pack(anchor='w', padx=10, pady=(0, 10))
background-color: #2d2d2d;
border-radius: 8px;
padding: 10px;
}
""")
tips_layout = QVBoxLayout(tips_frame)
tips_title = QLabel("💡 Совет") # Cancel button
tips_title.setFont(QFont("", 11, QFont.Bold)) btn_frame = ttk.Frame(main_frame)
tips_layout.addWidget(tips_title) btn_frame.pack(pady=10)
ttk.Button(btn_frame, text="Cancel", command=self.destroy).pack()
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): def _load_saved_settings(self):
"""Загружает сохранённые камеры"""
cameras = self.config_service.get_cameras() cameras = self.config_service.get_cameras()
if cameras: if cameras:
self.camera_combo.addItems(cameras) self.camera_combo['values'] = cameras
last_camera = self.config_service.get_last_camera() last_camera = self.config_service.get_last_camera()
if last_camera and last_camera in cameras: if last_camera and last_camera in cameras:
self.camera_combo.setCurrentText(last_camera) self.camera_combo.set(last_camera)
def _browse_folder(self): def _browse_folder(self):
"""Выбор папки для калибровочных кадров""" folder = filedialog.askdirectory(title="Select folder for calibration frames")
folder = QFileDialog.getExistingDirectory(self, "Выберите папку для калибровочных кадров")
if folder: if folder:
self.folder_entry.setText(folder) self.folder_entry.delete(0, tk.END)
self._stop_browse_blinking() self.folder_entry.insert(0, folder)
self._stop_blinking()
self.browse_btn.config(bg='#3c3c3c', fg='#e0e0e0')
def _check_folder_path(self): def _check_folder_path(self):
"""Проверяет, заполнено ли поле пути и запускает мигание если нет""" if not self.folder_entry.get():
if not self.folder_entry.text(): self._start_blinking()
self._start_browse_blinking()
else: else:
self._stop_browse_blinking() self._stop_blinking()
def _start_browse_blinking(self): def _start_blinking(self):
"""Запускает мигание кнопки 'Обзор' зелёным цветом""" self._blink_active = True
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): def blink():
"""Мигание кнопки""" if not self._blink_active:
current_style = self.browse_button.styleSheet() return
if "background-color: #4CAF50" in current_style: if self.browse_btn.cget('bg') == '#3c3c3c':
self.browse_button.setStyleSheet(""" self.browse_btn.config(bg='#f44336', fg='white')
QPushButton {
background-color: #2196F3;
color: white;
font-weight: bold;
border-radius: 4px;
}
""")
else: else:
self.browse_button.setStyleSheet(""" self.browse_btn.config(bg='#3c3c3c', fg='#e0e0e0')
QPushButton { self._blink_after_id = self.after(1500, blink)
background-color: #4CAF50;
color: white;
font-weight: bold;
border-radius: 4px;
}
""")
def _stop_browse_blinking(self): blink()
"""Останавливает мигание кнопки"""
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): def _stop_blinking(self):
"""Открывает дочернее окно для выбранного типа калибровки""" self._blink_active = False
if not self.folder_entry.text(): if self._blink_after_id:
QMessageBox.warning(self, "Внимание", "Сначала выберите папку для сохранения!") self.after_cancel(self._blink_after_id)
self._start_browse_blinking() 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 return
camera_name = self.camera_combo.currentText() camera_name = self.camera_combo.get()
if not camera_name: if not camera_name:
QMessageBox.warning(self, "Внимание", "Введите или выберите название камеры!") messagebox.showwarning("Warning", "Please enter or select a camera name!", parent=self)
return return
from ui.dialogs.calibration_type_dialog import CalibrationTypeDialog from ui.dialogs.calibration_type_dialog import CalibrationTypeDialog
dialog = CalibrationTypeDialog( dialog = CalibrationTypeDialog(
self, self,
cal_type, cal_type,
self.folder_entry.text(), self.folder_entry.get(),
camera_name, camera_name,
self.config_service self.config_service
) )
self.wait_window(dialog)
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()

File diff suppressed because it is too large Load diff

View file

@ -1,160 +1,153 @@
""" """
CelestialDialog - диалог управления небесными телами CelestialDialog - диалог управления небесными телами (tkinter)
Аналог CelestialBodiesDialogController из JavaFX версии
""" """
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) super().__init__(parent)
self.parent = parent
self.config_service = config_service 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.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._create_ui()
self._update_list() self._center_window()
def _create_ui(self): def _create_ui(self):
"""Создаёт интерфейс диалога""" # Main frame
layout = QVBoxLayout(self) main_frame = ttk.Frame(self, padding="20")
layout.setSpacing(15) main_frame.pack(fill='both', expand=True)
layout.setContentsMargins(20, 20, 20, 20)
# Заголовок # Title
title_label = QLabel("Управление небесными телами") ttk.Label(main_frame, text="Celestial Bodies", font=('Segoe UI', 14, 'bold')).pack(pady=(0, 10))
title_font = QFont() ttk.Label(main_frame, text="List of observation targets", font=('Segoe UI', 10)).pack(pady=(0, 15))
title_font.setPointSize(16)
title_font.setBold(True)
title_label.setFont(title_font)
title_label.setAlignment(Qt.AlignCenter)
layout.addWidget(title_label)
# Подпись # Listbox with scrollbar
subtitle_label = QLabel("Список объектов для наблюдения") list_frame = ttk.Frame(main_frame)
subtitle_font = QFont() list_frame.pack(fill='both', expand=True, pady=(0, 10))
subtitle_font.setPointSize(11)
subtitle_font.setBold(True)
subtitle_label.setFont(subtitle_font)
layout.addWidget(subtitle_label)
# Список небесных тел scrollbar = ttk.Scrollbar(list_frame)
self.bodies_list = QListWidget() scrollbar.pack(side='right', fill='y')
self.bodies_list.itemClicked.connect(lambda item: self._select_body(item.text()))
layout.addWidget(self.bodies_list)
# Поле для добавления нового self.bodies_listbox = tk.Listbox(list_frame, yscrollcommand=scrollbar.set, height=15,
add_layout = QHBoxLayout() 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: for body in self.celestial_bodies:
self.bodies_list.addItem(body) self.bodies_listbox.insert('end', body)
self._selected_body = None
self.remove_btn.setEnabled(False)
self.edit_btn.setEnabled(False)
def _select_body(self, body: str): self.bodies_listbox.bind('<<ListboxSelect>>', self._on_body_select)
"""Выделяет объект в списке"""
self._selected_body = body # Add new body
self.remove_btn.setEnabled(True) add_frame = ttk.Frame(main_frame)
self.edit_btn.setEnabled(True) 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): def _add_celestial_body(self):
"""Добавляет новое небесное тело""" new_body = self.new_body_entry.get().strip()
new_body = self.new_body_entry.text() if not new_body:
if not new_body or not new_body.strip(): messagebox.showwarning("Warning", "Please enter object name!", parent=self)
QMessageBox.warning(self, "Ошибка", "Введите название объекта")
return return
new_name = new_body.strip() if new_body in self.celestial_bodies:
if new_name in self.celestial_bodies: messagebox.showwarning("Warning", f"Object '{new_body}' already exists!", parent=self)
QMessageBox.warning(self, "Ошибка", f"Объект '{new_name}' уже существует!")
return return
self.celestial_bodies.append(new_name) self.celestial_bodies.append(new_body)
self.config_service.add_celestial_body(new_name) self.config_service.add_celestial_body(new_body)
self._update_list() self.bodies_listbox.insert('end', new_body)
self.new_body_entry.clear() self.new_body_entry.delete(0, 'end')
QMessageBox.information(self, "Успех", f"Объект '{new_name}' добавлен") messagebox.showinfo("Success", f"Object '{new_body}' added", parent=self)
def _remove_celestial_body(self): def _remove_celestial_body(self):
"""Удаляет выбранное небесное тело""" if self.selected_body:
if hasattr(self, '_selected_body') and self._selected_body: reply = messagebox.askyesno("Remove Object", f"Remove '{self.selected_body}'?", parent=self)
reply = QMessageBox.question(self, "Подтверждение", if reply:
f"Удалить объект '{self._selected_body}'?", self.celestial_bodies.remove(self.selected_body)
QMessageBox.Yes | QMessageBox.No) self.config_service.remove_celestial_body(self.selected_body)
if reply == QMessageBox.Yes: self.bodies_listbox.delete(0, 'end')
self.celestial_bodies.remove(self._selected_body) for body in self.celestial_bodies:
self.config_service.remove_celestial_body(self._selected_body) self.bodies_listbox.insert('end', body)
self._update_list() self.selected_body = None
QMessageBox.information(self, "Успех", f"Объект '{self._selected_body}' удалён") self.remove_btn.config(state='disabled')
self.edit_btn.config(state='disabled')
def _edit_celestial_body(self): def _edit_celestial_body(self):
"""Редактирует выбранное небесное тело""" if self.selected_body:
if hasattr(self, '_selected_body') and self._selected_body: dialog = tk.Toplevel(self)
new_name, ok = QInputDialog.getText(self, "Редактировать", dialog.title("Edit Celestial Body")
f"Изменить '{self._selected_body}' на:", dialog.geometry("350x120")
text=self._selected_body) dialog.transient(self)
if ok and new_name and new_name.strip(): dialog.grab_set()
new_name = new_name.strip()
if new_name != self._selected_body: 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: 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 return
idx = self.celestial_bodies.index(self._selected_body) idx = self.celestial_bodies.index(self.selected_body)
old_name = self.celestial_bodies[idx]
self.celestial_bodies[idx] = new_name self.celestial_bodies[idx] = new_name
self.config_service.update_celestial_body(old_name, new_name) self.config_service.update_celestial_body(self.selected_body, new_name)
self._update_list() self.bodies_listbox.delete(0, 'end')
QMessageBox.information(self, "Успех", f"Объект переименован в '{new_name}'") 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) super().__init__(parent)
self.parent = parent
self.config_service = config_service self.config_service = config_service
self.setWindowTitle("Управление оборудованием")
self.setMinimumSize(700, 500)
self.resize(800, 550)
# Загружаем данные
self.cameras = self.config_service.get_cameras() self.cameras = self.config_service.get_cameras()
self.lenses = self.config_service.get_lenses() self.lenses = self.config_service.get_lenses()
self.telescopes = self.config_service.get_telescopes() 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._create_ui()
self._update_cameras_list() self._center_window()
self._update_lenses_list()
self._update_telescopes_list()
def _create_ui(self): def _create_ui(self):
layout = QVBoxLayout(self) # Notebook for tabs
layout.setSpacing(15) notebook = ttk.Notebook(self)
layout.setContentsMargins(20, 20, 20, 20) notebook.pack(fill='both', expand=True, padx=10, pady=10)
# Заголовок # Tab 1: Cameras
title_label = QLabel("Управление оборудованием") cameras_frame = ttk.Frame(notebook)
title_font = QFont() notebook.add(cameras_frame, text="Cameras")
title_font.setPointSize(16) self._create_cameras_tab(cameras_frame)
title_font.setBold(True)
title_label.setFont(title_font)
title_label.setAlignment(Qt.AlignCenter)
layout.addWidget(title_label)
# Используем QTabWidget для трёх вкладок # Tab 2: Lenses
tab_widget = QTabWidget() lenses_frame = ttk.Frame(notebook)
notebook.add(lenses_frame, text="Lenses")
self._create_lenses_tab(lenses_frame)
# Вкладка: Камеры # Tab 3: Telescopes
cameras_tab = self._create_cameras_tab() telescopes_frame = ttk.Frame(notebook)
tab_widget.addTab(cameras_tab, "📷 Камеры") notebook.add(telescopes_frame, text="Telescopes")
self._create_telescopes_tab(telescopes_frame)
# Вкладка: Объективы # Close button
lenses_tab = self._create_lenses_tab() btn_frame = ttk.Frame(self)
tab_widget.addTab(lenses_tab, "🔭 Объективы") btn_frame.pack(pady=10)
ttk.Button(btn_frame, text="Close", command=self.destroy).pack()
# Вкладка: Телескопы def _center_window(self):
telescopes_tab = self._create_telescopes_tab() self.update_idletasks()
tab_widget.addTab(telescopes_tab, "🪐 Телескопы") 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)
# Кнопка закрытия scrollbar = ttk.Scrollbar(list_frame)
close_btn = QPushButton("Закрыть") scrollbar.pack(side='right', fill='y')
close_btn.clicked.connect(self.accept)
close_layout = QHBoxLayout()
close_layout.addStretch()
close_layout.addWidget(close_btn)
layout.addLayout(close_layout)
def _create_cameras_tab(self) -> QWidget: self.cameras_listbox = tk.Listbox(list_frame, yscrollcommand=scrollbar.set,
"""Создаёт вкладку с камерами""" bg='#2d2d2d', fg='#e0e0e0',
tab = QWidget() selectbackground='#4CAF50', selectforeground='white',
layout = QVBoxLayout(tab) 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: for camera in self.cameras:
self.cameras_list.addItem(camera) self.cameras_listbox.insert('end', camera)
self._selected_camera = None
self.remove_camera_btn.setEnabled(False)
def _select_camera(self, camera: str): self.cameras_listbox.bind('<<ListboxSelect>>', self._on_camera_select)
self._selected_camera = camera
self.remove_camera_btn.setEnabled(True) # Buttons
self.lenses_list.clearSelection() btn_frame = ttk.Frame(parent)
self.telescopes_list.clearSelection() btn_frame.pack(pady=10)
self._selected_lens = None
self._selected_telescope = None ttk.Button(btn_frame, text="Add Camera", command=self._add_camera).pack(side='left', padx=5)
self.remove_lens_btn.setEnabled(False) self.remove_camera_btn = ttk.Button(btn_frame, text="Remove", command=self._remove_camera, state='disabled')
self.remove_telescope_btn.setEnabled(False) 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): def _add_camera(self):
name, ok = QInputDialog.getText(self, "Добавить камеру", "Введите название камеры:") dialog = tk.Toplevel(self)
if ok and name and name.strip(): dialog.title("Add Camera")
new_name = name.strip() dialog.geometry("350x120")
if new_name in self.cameras: dialog.transient(self)
QMessageBox.warning(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 return
self.cameras.append(new_name) self.cameras.append(new_camera)
self.config_service.add_camera(new_name) self.config_service.add_camera(new_camera)
self._update_cameras_list() self.cameras_listbox.insert('end', new_camera)
QMessageBox.information(self, "Успех", f"Камера '{new_name}' добавлена") 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): def _remove_camera(self):
if hasattr(self, '_selected_camera') and self._selected_camera: if self.selected_camera:
reply = QMessageBox.question(self, "Подтверждение", reply = messagebox.askyesno("Remove Camera", f"Remove '{self.selected_camera}'?", parent=self)
f"Удалить камеру '{self._selected_camera}'?", if reply:
QMessageBox.Yes | QMessageBox.No) self.cameras.remove(self.selected_camera)
if reply == QMessageBox.Yes: self.config_service.remove_camera(self.selected_camera)
self.cameras.remove(self._selected_camera) self.cameras_listbox.delete(0, 'end')
self.config_service.remove_camera(self._selected_camera) for camera in self.cameras:
self._update_cameras_list() 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: for lens in self.lenses:
self.lenses_list.addItem(lens) self.lenses_listbox.insert('end', lens)
self._selected_lens = None
self.remove_lens_btn.setEnabled(False)
def _select_lens(self, lens: str): self.lenses_listbox.bind('<<ListboxSelect>>', self._on_lens_select)
self._selected_lens = lens
self.remove_lens_btn.setEnabled(True) # Buttons
self.cameras_list.clearSelection() btn_frame = ttk.Frame(parent)
self.telescopes_list.clearSelection() btn_frame.pack(pady=10)
self._selected_camera = None
self._selected_telescope = None ttk.Button(btn_frame, text="Add Lens", command=self._add_lens).pack(side='left', padx=5)
self.remove_camera_btn.setEnabled(False) self.remove_lens_btn = ttk.Button(btn_frame, text="Remove", command=self._remove_lens, state='disabled')
self.remove_telescope_btn.setEnabled(False) 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): def _add_lens(self):
name, ok = QInputDialog.getText(self, "Добавить объектив", "Введите название объектива:") dialog = tk.Toplevel(self)
if ok and name and name.strip(): dialog.title("Add Lens")
new_name = name.strip() dialog.geometry("350x120")
if new_name in self.lenses: dialog.transient(self)
QMessageBox.warning(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 return
self.lenses.append(new_name) self.lenses.append(new_lens)
self.config_service.add_lens(new_name) self.config_service.add_lens(new_lens)
self._update_lenses_list() self.lenses_listbox.insert('end', new_lens)
QMessageBox.information(self, "Успех", f"Объектив '{new_name}' добавлен") 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): def _remove_lens(self):
if hasattr(self, '_selected_lens') and self._selected_lens: if self.selected_lens:
reply = QMessageBox.question(self, "Подтверждение", reply = messagebox.askyesno("Remove Lens", f"Remove '{self.selected_lens}'?", parent=self)
f"Удалить объектив '{self._selected_lens}'?", if reply:
QMessageBox.Yes | QMessageBox.No) self.lenses.remove(self.selected_lens)
if reply == QMessageBox.Yes: self.config_service.remove_lens(self.selected_lens)
self.lenses.remove(self._selected_lens) self.lenses_listbox.delete(0, 'end')
self.config_service.remove_lens(self._selected_lens) for lens in self.lenses:
self._update_lenses_list() 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): def _edit_lens(self):
if hasattr(self, '_selected_lens') and self._selected_lens: if self.selected_lens:
new_name, ok = QInputDialog.getText(self, "Редактировать объектив", dialog = tk.Toplevel(self)
f"Изменить '{self._selected_lens}' на:", dialog.title("Edit Lens")
text=self._selected_lens) dialog.geometry("350x120")
if ok and new_name and new_name.strip(): dialog.transient(self)
new_name = new_name.strip() dialog.grab_set()
if new_name != self._selected_lens:
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: if new_name in self.lenses:
QMessageBox.warning(self, "Ошибка", "Такой объектив уже существует!") messagebox.showerror("Error", "Lens already exists!", parent=dialog)
return return
idx = self.lenses.index(self._selected_lens) idx = self.lenses.index(self.selected_lens)
self.lenses[idx] = new_name 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.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: for telescope in self.telescopes:
self.telescopes_list.addItem(telescope) self.telescopes_listbox.insert('end', telescope)
self._selected_telescope = None
self.remove_telescope_btn.setEnabled(False)
def _select_telescope(self, telescope: str): self.telescopes_listbox.bind('<<ListboxSelect>>', self._on_telescope_select)
self._selected_telescope = telescope
self.remove_telescope_btn.setEnabled(True) # Buttons
self.cameras_list.clearSelection() btn_frame = ttk.Frame(parent)
self.lenses_list.clearSelection() btn_frame.pack(pady=10)
self._selected_camera = None
self._selected_lens = None ttk.Button(btn_frame, text="Add Telescope", command=self._add_telescope).pack(side='left', padx=5)
self.remove_camera_btn.setEnabled(False) self.remove_telescope_btn = ttk.Button(btn_frame, text="Remove", command=self._remove_telescope, state='disabled')
self.remove_lens_btn.setEnabled(False) 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): def _add_telescope(self):
"""Добавляет телескоп с указанием диафрагмы (фиксированной)""" dialog = tk.Toplevel(self)
dialog = QDialog(self) dialog.title("Add Telescope")
dialog.setWindowTitle("Добавить телескоп") dialog.geometry("400x320")
dialog.setMinimumWidth(400) 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() ttk.Label(dialog, text="Focal length (mm):").pack(pady=(10, 5))
name_edit.setPlaceholderText("例如: Celestron 8\"") focal_entry = ttk.Entry(dialog, width=20)
form_layout.addRow("Название:", name_edit) focal_entry.insert(0, "1000")
focal_entry.pack()
aperture_spin = QDoubleSpinBox() ttk.Label(dialog, text="Diameter (mm):").pack(pady=(10, 5))
aperture_spin.setRange(0.5, 20.0) diameter_entry = ttk.Entry(dialog, width=20)
aperture_spin.setSingleStep(0.1) diameter_entry.insert(0, "200")
aperture_spin.setValue(5.0) diameter_entry.pack()
aperture_spin.setSuffix(" (f/)")
form_layout.addRow("Диафрагма (f/):", aperture_spin)
focal_spin = QSpinBox() def save():
focal_spin.setRange(100, 5000) name = name_entry.get().strip()
focal_spin.setSingleStep(50) if not name:
focal_spin.setSuffix(" мм") messagebox.showerror("Error", "Please enter telescope name!", parent=dialog)
form_layout.addRow("Фокусное расстояние:", focal_spin) return
diameter_spin = QSpinBox() try:
diameter_spin.setRange(50, 500) aperture = float(aperture_entry.get())
diameter_spin.setSingleStep(10) focal = int(focal_entry.get())
diameter_spin.setSuffix(" мм") diameter = int(diameter_entry.get())
form_layout.addRow("Диаметр объектива:", diameter_spin) 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)
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.telescopes.append(telescope_info)
self.config_service.add_telescope(telescope_info) self.config_service.add_telescope(telescope_info)
self._update_telescopes_list() self.telescopes_listbox.insert('end', telescope_info)
QMessageBox.information(self, "Успех", f"Телескоп '{name}' добавлен") dialog.destroy()
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): def _remove_telescope(self):
if hasattr(self, '_selected_telescope') and self._selected_telescope: if self.selected_telescope:
reply = QMessageBox.question(self, "Подтверждение", reply = messagebox.askyesno("Remove Telescope", f"Remove '{self.selected_telescope}'?", parent=self)
f"Удалить телескоп '{self._selected_telescope}'?", if reply:
QMessageBox.Yes | QMessageBox.No) self.telescopes.remove(self.selected_telescope)
if reply == QMessageBox.Yes: self.config_service.remove_telescope(self.selected_telescope)
self.telescopes.remove(self._selected_telescope) self.telescopes_listbox.delete(0, 'end')
self.config_service.remove_telescope(self._selected_telescope) for telescope in self.telescopes:
self._update_telescopes_list() 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): def _edit_telescope(self):
if hasattr(self, '_selected_telescope') and self._selected_telescope: if self.selected_telescope:
new_name, ok = QInputDialog.getText(self, "Редактировать телескоп", import re
f"Изменить '{self._selected_telescope}' на:", match = re.search(r'(.+?) \(f/([\d\.]+), F=(\d+)mm, D=(\d+)mm\)', self.selected_telescope)
text=self._selected_telescope) if match:
if ok and new_name and new_name.strip(): old_name = match.group(1)
new_name = new_name.strip() old_aperture = match.group(2)
if new_name != self._selected_telescope: old_focal = match.group(3)
if new_name in self.telescopes: old_diameter = match.group(4)
QMessageBox.warning(self, "Ошибка", "Такой телескоп уже существует!") 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 return
idx = self.telescopes.index(self._selected_telescope)
self.telescopes[idx] = new_name new_info = f"{new_name} (f/{aperture}, F={focal}mm, D={diameter}mm)"
self.config_service.remove_telescope(self._selected_telescope) if new_info != self.selected_telescope and new_info in self.telescopes:
self.config_service.add_telescope(new_name) messagebox.showerror("Error", "Telescope already exists!", parent=dialog)
self._update_telescopes_list() 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, import tkinter as tk
QPushButton, QScrollArea from tkinter import ttk
)
from PySide6.QtCore import Qt
from PySide6.QtGui import QFont
class InstructionsDialog(QDialog): class InstructionsDialog(tk.Toplevel):
"""Диалог с подробной инструкцией пользователя""" """Диалог с подробной инструкцией пользователя на английском"""
def __init__(self, parent): def __init__(self, parent):
super().__init__(parent) super().__init__(parent)
self.parent = parent
self.setWindowTitle("Инструкция по использованию") self.title("Instructions")
self.setMinimumSize(700, 500) self.geometry("750x600")
self.resize(750, 550) self.minsize(700, 500)
self.transient(parent)
self.grab_set()
self._create_ui() self._create_ui()
self._center_window()
def _create_ui(self): def _create_ui(self):
"""Создаёт интерфейс диалога""" # Main frame
layout = QVBoxLayout(self) main_frame = ttk.Frame(self, padding="15")
layout.setSpacing(10) main_frame.pack(fill='both', expand=True)
layout.setContentsMargins(15, 15, 15, 15)
# Заголовок # Title
title_label = QLabel("Astro Session Watcher - Руководство пользователя") ttk.Label(main_frame, text="Astro Session Watcher - User Guide", font=('Segoe UI', 16, 'bold')).pack(pady=(0, 10))
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)
# Текст инструкции # Text with scrollbar
text_edit = QTextEdit() text_frame = ttk.Frame(main_frame)
text_edit.setReadOnly(True) text_frame.pack(fill='both', expand=True)
text_edit.setFont(QFont("Consolas", 10))
instructions = """ scrollbar = ttk.Scrollbar(text_frame)
======================= ASTRO SESSION WATCHER ======================= 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 или аналогичное ПО, The application automatically tracks new photos in the selected folder,
все фотографии сохраняются в одну папку. Astro Session Watcher помогает: 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. Вы выбираете папку, куда камера сохраняет снимки 🚀 HOW IT WORKS?
2. Приложение создает папку сессии: "AstroSession_ГГГГ-ММ-ДД"
3. Каждый объект съемки получает свою подпапку внутри папки сессии
4. Когда вы меняете объект (нажимаете "Новая цель"), все накопленные
в папке наблюдения файлы автоматически ПЕРЕМЕЩАЮТСЯ в папку предыдущего объекта
5. При завершении сессии оставшиеся файлы также перемещаются в папку последнего объекта
📝 ПОШАГОВАЯ ИНСТРУКЦИЯ 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. STARTING A SESSION
2. Выберите камеру и объектив из выпадающих списков
3. Введите название цели (или выберите из списка небесных тел)
4. Нажмите "Начать отслеживание"
После запуска: 1. Click "Browse" and select the folder where your camera saves photos
Статус изменится на "● ON AIR" с мигающим красным текстом 2. Select camera and lens/telescope from dropdowns
Кнопка "Новая цель" начнет мигать красным контуром 3. Enter target name (or select from celestial bodies list)
В папке наблюдения создастся папка "AstroSession_дата" 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) 1. Click "New Target" button (or Ctrl+Shift+N)
2. Введите название нового объекта 2. Enter new target name
3. Приложение автоматически: 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) 1. Click "■ Stop" (or Ctrl+X)
2. Приложение: 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 + O Select watch folder
Ctrl + E Управление оборудованием Ctrl + E Equipment management
Ctrl + B Управление небесными телами Ctrl + B Celestial bodies management
Ctrl + S Начать сессию Ctrl + S Start session
Ctrl + X Остановить сессию Ctrl + X Stop session
Ctrl + F Открыть папку текущей сессии Ctrl + F Open current session folder
Ctrl + Shift+N Создать новый объект Ctrl + Shift+N Create new target
F1 О программе F1 About
F2 Эта инструкция F2 This instruction
🔧 ФАЙЛЫ НАСТРОЕК 🔧 CONFIG FILES
📄 astro_settings.json камеры, объективы, последняя папка 📄 astro_settings.json cameras, lenses, last folder
📄 celestial_bodies.json список небесных тел 📄 celestial_bodies.json list of celestial bodies
Все файлы хранятся в папке с программой. Вы можете редактировать их вручную. All files are stored in the program folder. You can edit them manually.
📧 ТЕХНИЧЕСКАЯ ПОДДЕРЖКА 📧 TECHNICAL SUPPORT
Text me: norvicdev@gmail.com
Разработчик: Vic Sergeev Developer: Vic Sergeev
Версия: 0.3.0-alpha Version: 0.4.0-alpha
При обнаружении ошибок или для предложений по улучшению:
Сообщите разработчику
Приложите файлы логов (SessionLog.txt, ObjectLog.txt)
""" """
text_edit.setText(instructions) self.text_widget.insert('1.0', instructions)
layout.addWidget(text_edit) self.text_widget.config(state='disabled')
# Кнопка закрытия # Close button
close_layout = QHBoxLayout() ttk.Button(main_frame, text="Close", command=self.destroy).pack(pady=10)
close_layout.addStretch()
close_btn = QPushButton("Закрыть") def _center_window(self):
close_btn.clicked.connect(self.accept) self.update_idletasks()
close_layout.addWidget(close_btn) 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)
layout.addLayout(close_layout) self.geometry(f'+{x}+{y}')

File diff suppressed because it is too large Load diff