From 3ae07ca2894022ea4d53632f3f2fe3e34076fd9b Mon Sep 17 00:00:00 2001 From: Vic Sergeev Date: Sat, 9 May 2026 19:44:16 +0300 Subject: [PATCH] reworked with tkinter, UI improements needed --- .idea/workspace.xml | 23 +- astro_settings.json | 7 +- celestial_bodies.json | 3 +- main.py | 24 +- models/__pycache__/__init__.cpython-313.pyc | Bin 311 -> 311 bytes .../__pycache__/astro_object.cpython-313.pyc | Bin 1406 -> 1406 bytes models/__pycache__/session.cpython-313.pyc | Bin 2200 -> 2200 bytes services/__pycache__/__init__.cpython-313.pyc | Bin 461 -> 461 bytes .../config_service.cpython-313.pyc | Bin 12601 -> 12601 bytes .../__pycache__/file_service.cpython-313.pyc | Bin 7032 -> 7032 bytes .../session_service.cpython-313.pyc | Bin 9011 -> 9011 bytes .../__pycache__/watch_service.cpython-313.pyc | Bin 7653 -> 7653 bytes ui/__pycache__/__init__.cpython-313.pyc | Bin 244 -> 244 bytes ui/__pycache__/main_window.cpython-313.pyc | Bin 39354 -> 36714 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 589 -> 589 bytes .../calibration_dialog.cpython-313.pyc | Bin 14819 -> 10815 bytes .../calibration_type_dialog.cpython-313.pyc | Bin 40397 -> 32008 bytes .../celestial_dialog.cpython-313.pyc | Bin 10441 -> 10625 bytes .../equipment_dialog.cpython-313.pyc | Bin 22678 -> 30682 bytes .../instructions_dialog.cpython-313.pyc | Bin 11153 -> 8565 bytes ui/dialogs/calibration_dialog.py | 335 +++---- ui/dialogs/calibration_type_dialog.py | 830 +++++++---------- ui/dialogs/celestial_dialog.py | 243 +++-- ui/dialogs/equipment_dialog.py | 700 +++++++++------ ui/dialogs/instructions_dialog.py | 230 +++-- ui/main_window.py | 844 ++++++++---------- 26 files changed, 1488 insertions(+), 1751 deletions(-) diff --git a/.idea/workspace.xml b/.idea/workspace.xml index fc59fa5..5284823 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -1,7 +1,18 @@ - + + + + + + + + + + + + \ No newline at end of file diff --git a/astro_settings.json b/astro_settings.json index 6562295..756754f 100644 --- a/astro_settings.json +++ b/astro_settings.json @@ -1,13 +1,12 @@ { "cameras": [ "Canon 40D", - "Canon 400D", - "Canon 500D" + "Canon 400D" ], "lenses": [ "MTO-500A", - "Юпитер-21м", - "Tamron 18-200mm" + "Tamron 18-200mm", + "Юпитер-21м 200мм" ], "telescopes": [ "Celestron Astromaster 130 (f/5.0, F=650mm, D=130mm)" diff --git a/celestial_bodies.json b/celestial_bodies.json index c510679..bd6d355 100644 --- a/celestial_bodies.json +++ b/celestial_bodies.json @@ -10,5 +10,6 @@ "M89", "Венера", "Меркурий", - "Нептун" + "Нептун", + "Saturn" ] \ No newline at end of file diff --git a/main.py b/main.py index b079cf5..30672c9 100644 --- a/main.py +++ b/main.py @@ -1,32 +1,22 @@ """ -Astro Session Watcher - Главный входной файл -Приложение для астрофотографов с отслеживанием файлов и сортировкой по объектам +Astro Session Watcher v0.4.0 - tkinter версия """ + +import tkinter as tk +from tkinter import ttk import sys import os -from pathlib import Path # Добавляем корневую директорию в путь sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from PySide6.QtWidgets import QApplication from ui.main_window import MainWindow def main(): - """Точка входа в приложение""" - app = QApplication(sys.argv) - - # Устанавливаем стиль Fusion для современного вида - app.setStyle("Fusion") - - # Тёмная палитра - app.setPalette(app.style().standardPalette()) - - window = MainWindow() - window.show() - - sys.exit(app.exec()) + root = tk.Tk() + app = MainWindow(root) + root.mainloop() if __name__ == "__main__": diff --git a/models/__pycache__/__init__.cpython-313.pyc b/models/__pycache__/__init__.cpython-313.pyc index b164826f061a9d815498f7ce56b96b9c5f24093c..a502d50c007fc0f61f132d3c977f3821830b05dd 100644 GIT binary patch delta 20 acmdnaw4I6jGcPX}0}$L)`M;4{hYH diff --git a/services/__pycache__/session_service.cpython-313.pyc b/services/__pycache__/session_service.cpython-313.pyc index 5f88702dc216b97aedabd3d1b6d687fd5ada5081..fb65467d2d3adcf6107bed48cc746e3c792b0be7 100644 GIT binary patch delta 20 acmdn&w%LvQGcPX}0}$L)`M;4{Lm2=>Tm~2b delta 20 acmdn&w%LvQGcPX}0}$Mt_h%!whB5$0DF%-K diff --git a/services/__pycache__/watch_service.cpython-313.pyc b/services/__pycache__/watch_service.cpython-313.pyc index 7f20f07d09e6b0b407ddd461088ac44b2cd92a36..49d4f78f664fa4fb1f33cb052bb37c173f9b7e7b 100644 GIT binary patch delta 20 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 diff --git a/ui/dialogs/__pycache__/__init__.cpython-313.pyc b/ui/dialogs/__pycache__/__init__.cpython-313.pyc index 9c7cd839ab31e78ceb5a60e7c0b2b8b17345c4d0..da86210e1282f626e15187f5de0d84249c670f83 100644 GIT binary patch delta 20 acmX@ha+ZbrGcPX}0}yb>{oly#!~_63y#*}* delta 20 acmX@ha+ZbrGcPX}0}wph^k*Zt6B7VJqXt<3 diff --git a/ui/dialogs/__pycache__/calibration_dialog.cpython-313.pyc b/ui/dialogs/__pycache__/calibration_dialog.cpython-313.pyc index 5316e2341588fd61d5bad2347460876ed8a286bc..1a0b6852c67ede5ab1074eb12b2340f3b6fd5aa1 100644 GIT binary patch literal 10815 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 Q&UrWZm%-mKSf+^mF9lVR2><{9 literal 14819 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 Le#POFPR4%&FVD;# diff --git a/ui/dialogs/__pycache__/calibration_type_dialog.cpython-313.pyc b/ui/dialogs/__pycache__/calibration_type_dialog.cpython-313.pyc index 764ab6402f4ea44749e845af5c6ed8230357f696..ecc1545f17b782e9af31de11361a16fe45af5832 100644 GIT binary patch literal 32008 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 literal 10441 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 OFPJV(-{n|>QT<=_f4XJ> diff --git a/ui/dialogs/__pycache__/equipment_dialog.cpython-313.pyc b/ui/dialogs/__pycache__/equipment_dialog.cpython-313.pyc index 4f0a79b2e44d2e53e8d08d591fdd181397c5b20b..863cbb36b299b6b6245d461c7befb467ce5425c5 100644 GIT binary patch literal 30682 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 literal 22678 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 Cw}4&% diff --git a/ui/dialogs/__pycache__/instructions_dialog.cpython-313.pyc b/ui/dialogs/__pycache__/instructions_dialog.cpython-313.pyc index cc50530b17118ccf445d102dd58d4f0552b58038..5837dfef2c24c55bc6ba6393b1f85670c12befc1 100644 GIT binary patch literal 8565 zcmd^FU2xpib;kaYAhe`pS(0TbiY`}HC1GI~ zn+q(d)%1aOTaT3Sj5Lmam`pNe@?!T*Y0|`w<0OxMTvW~HW*R(6n zh^brl9P49^Uo<{yTy1>P_-*6o%zKUD?NjeH?{#m<`^sBb7sXgcv`tdQ%w7 z5|61-9?wfpVsU$i= z(Dw^(3HP6Y?%&cX?y)$6{KR{+@hd$4p>frF9c%u?yL|6e7n&=gg~jnl+kIzGM3SD3 zyd%9tU&WP+=B2*^OY@N<=%b5CXSP{uqw`X>Gx*+mS6iR~K`Y#FsnNUm0I_95H(u44 z8b1=w!z93TZ-!~53J_a2LmUY|2@DT>MgN0;u76HSCno)5D+B$`qel*(f9mjI^EV*W z{1Q8qR{Vrhs~X&wHO;J;uBQ1NRh=6Z*H5^nYZ<;gXV_)K<#oTaY*rleg5h_$Tt|z5 zgTx4v? zpB^5Z$vXyjX1-msOv81JnbY;cyw1y~x&5k9aGjYU=#s4(jstC)*4@Iq!DnjbOi}2S zGXu)jbWICB_E+ni80GJ6-Go39`B+m7cU_m+ja^@RbIZTU z$;-+m<;t_y<n_Fq<)=OKj%MYV_ zRo?5#d;cKsyMvjeLMWQlmPpuyFcVK&MLD}Bv;t1tQV=Y%PRF8^NIsHn+OE3*LJZZ! zu_$X%ifl& zywr#9At!qRIf1#^JLyLUj=&vtR`p`htjziGS=*h@rK5hlWLlQrVVtk(mEwhz&}9}X zGYhpDzL87-8?Tu~BOSXStHzvduzcngEs}6qMMC8o=UpJQD{eZfr-hH;pF03;8?J=-uy`~cxe_&s%}z-`Ma*yS?F z_2Y|Nulmv1IX_yO&ak)k)9sRUy#2+7)LeFosiUfznVMwNL%HFJ(JX5Zd|Xj- z^9IwaRm&{s6f-cr=GtX63%X_18EkIhoWm-{BCF2ZuI(_h!dwi2Wrc+_imZekGWa3I zvAOF|SDDS$nLUeLT?jC`I-fJphh?#{jxYj$4l5dXT0npZ#pzd++wXt>BQ`xTlw%o{ zWpZq6D*G+v=G(9RU;kB(8^mx3z=Fw=42)Yz`C=L;mJThECETd5avLJ zY16PZb-Tt{0h`FNr}_eRBanRMiGCI;3k!k{vFialLaq(6z|4eJMmvkE@6@Tjk&%@0 z&3-mY3=G*URKf)B?ZpanYO^5?6zmj@P{jnNL)6NX{cM`J3i<-h0!nBWd$h_y1xrmr zOLO8=Dt(ASf0?O@q8IyNW|q#SwOl zj#*9XQz3uzsLh*`3-C=I#ezpklMM^0zWMGinF{;ivK)mNurw)f?ni&ehDwCn(rcB% z{Gh@ZBlihNx8{Uq4^ak$^yYh)8RDCvOg6<9O?RHnT4sgtG{>}>Lk3^#BDk3~5J8a@ zM1Ew!XhJq)!q5b-H{lRHY%XLku=&O2ARO<=Q1^xAIiNxKj*`h+Y$Hh{O#kr2(BwEt zBcyVCG{;8rS$Ye@wEI&sTl`Uz0VF5GF1H?6=gpG)_+*;A>ZlMWiZMfnMj}OW)^xY) zV^VToU}0PL7J4T*#0Ara#DpG-1VObP2?eFRLK9Z$iQ=W({N(9O3w2S!zU_ z@)Qst_%|yxOf7<9k~D-zZ~cU2PMx00=7uJ7gDg`bgmAIsW&_U|#>Nq~5^lUVuynIKdugQ=$A) z@DMtdR>@aFu+bK zVgFc5{}eH}P~()AY{E5T@en*TVj@%u?2BM@XmrK~LTtfN)AIHQY?|F$ z^a?YIrZ^^+NkD7xJM|!4@d3+?4o^&GhKHVKYW_4if%`(2bL0-o#vrTMe8DUfjfLmt za2mAw5j!a(#)4tN_VGdXf?0r$Be!ELC@&z5j_lDOJKXI7Ga*bT~8-C zB}aXQPPv*$nfQvp6STX6WE{VL*7o=`sEis9J#pFlLx-+kh+qWyS~?T<*~+r z@y2Ayljl}t%ag4JuQ#Z10U)~`7Dz5!+xggL6rb;S=)F^KomzS9+K#@9T{q+hs7A0n z{^5a(J)YeE`QCl+AN}FcmBHVYKQ1@2XKsAs@YS=8(fk*&$k(PL7vtCE-KZd}%DYjI zSe5sA^1hX)u0GPB)=}UJI#zb9_6~Tx1FOA9z22h@0)F~-$IFr3@r%jZ&&8y!?k0l$ zp1gl0vGUi~)6(JsO;5R+dv0h>xx=!stE1!?9DO2*lt9dB3^TZ8tKD`KFo`; zUk!)kF={F8XK7OiY|e6z5p4>C%@OP9`f50$3&qTKWkfZakMu~mM|-4i#_tv;k%Ny- z-lFaDBafqcyRBBGBa>zpEo69|b52ly>qJqmWNYXBEkOhN>dyP&*Sen!zRnwzN9R;T zNyh>ejhyFyf!DBV#{#{hYJ=a~R`1}g*GhnI)4&znmLws7B+=L9{Wp@^E}y(~ay7Z% zOYXmx>|M!y@X}9ST0QuTckr2O2cP|YuXk|ROOAXNU!6GPO`N$l@nYj;&707?&jlMz_Kwqy zq|%|H#-HAh{yDzz$M}}S*Z*@*J(BoNv z?{dzrTI5Cq;NMHZAhM^v{7kUWXZabt5*^MXxZRFLB9Skm(MbG`EJb$ytMt&99aGWB Sf#vP*@BP8vKT6aVSpElTx;pp( literal 11153 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 diff --git a/ui/dialogs/calibration_dialog.py b/ui/dialogs/calibration_dialog.py index adcf9fc..f7bcd2a 100644 --- a/ui/dialogs/calibration_dialog.py +++ b/ui/dialogs/calibration_dialog.py @@ -1,281 +1,180 @@ """ -CalibrationDialog - главный диалог калибровки -С выбором камеры, папки и типа кадров +CalibrationDialog - главный диалог калибровки (tkinter) """ -from PySide6.QtWidgets import ( - QDialog, QVBoxLayout, QHBoxLayout, QGridLayout, - QLabel, QComboBox, QLineEdit, QPushButton, QFrame, - QMessageBox, QFileDialog, QWidget -) -from PySide6.QtCore import Qt, QTimer -from PySide6.QtGui import QFont -from services.config_service import ConfigService +import tkinter as tk +from tkinter import ttk, messagebox, filedialog +from pathlib import Path -class CalibrationDialog(QDialog): +class CalibrationDialog(tk.Toplevel): """Главное окно калибровки""" - def __init__(self, parent, config_service: ConfigService): + def __init__(self, parent, config_service): super().__init__(parent) - + self.parent = parent self.config_service = config_service + self._blink_active = False + self._blink_after_id = None - self.setWindowTitle("🌑 Калибровочные кадры") - self.setMinimumSize(600, 450) - self.resize(650, 500) + self.title("Calibration Frames") + self.geometry("650x500") + self.minsize(600, 450) + self.transient(parent) + self.grab_set() self._create_ui() self._load_saved_settings() - - # Таймер для мигания кнопки "Обзор" - self._browse_blink_timer = None self._check_folder_path() + self._center_window() def _create_ui(self): - layout = QVBoxLayout(self) - layout.setSpacing(20) - layout.setContentsMargins(25, 25, 25, 25) + # Main frame + main_frame = ttk.Frame(self, padding="25") + main_frame.pack(fill='both', expand=True) - # Заголовок - title_label = QLabel("🌑 Калибровочные кадры") - title_font = QFont() - title_font.setPointSize(18) - title_font.setBold(True) - title_label.setFont(title_font) - layout.addWidget(title_label) + # Title + title_label = ttk.Label(main_frame, text="Calibration Frames", font=('Segoe UI', 18, 'bold')) + title_label.pack(pady=(0, 15)) - # Основная сетка - grid = QGridLayout() - grid.setVerticalSpacing(15) - grid.setHorizontalSpacing(15) + # Separator + ttk.Separator(main_frame, orient='horizontal').pack(fill='x', pady=(0, 15)) - # Строка 0: Камера - camera_label = QLabel("📷 Камера:") - camera_label.setFont(QFont("", 10, QFont.Bold)) - grid.addWidget(camera_label, 0, 0) + # Camera selection + camera_frame = ttk.Frame(main_frame) + camera_frame.pack(fill='x', pady=5) - self.camera_combo = QComboBox() - self.camera_combo.setEditable(True) - self.camera_combo.setMinimumWidth(250) - grid.addWidget(self.camera_combo, 0, 1) + ttk.Label(camera_frame, text="Camera:", font=('Segoe UI', 10, 'bold')).pack(side='left', padx=(0, 10)) - # Строка 1: Папка - folder_label = QLabel("📁 Папка:") - folder_label.setFont(QFont("", 10, QFont.Bold)) - grid.addWidget(folder_label, 1, 0) + self.camera_combo = ttk.Combobox(camera_frame, width=30) + self.camera_combo.pack(side='left', fill='x', expand=True) - folder_widget = QWidget() - folder_layout = QHBoxLayout(folder_widget) - folder_layout.setContentsMargins(0, 0, 0, 0) - folder_layout.setSpacing(10) + # Folder selection + folder_frame = ttk.Frame(main_frame) + folder_frame.pack(fill='x', pady=5) - self.folder_entry = QLineEdit() - self.folder_entry.setPlaceholderText("Выберите папку для сохранения калибровочных кадров") - folder_layout.addWidget(self.folder_entry) + ttk.Label(folder_frame, text="Folder:", font=('Segoe UI', 10, 'bold')).pack(side='left', padx=(0, 10)) - self.browse_button = QPushButton("✨ Обзор") - self.browse_button.setFixedWidth(100) - self.browse_button.clicked.connect(self._browse_folder) - folder_layout.addWidget(self.browse_button) + folder_input_frame = ttk.Frame(folder_frame) + folder_input_frame.pack(side='left', fill='x', expand=True) - grid.addWidget(folder_widget, 1, 1) + self.folder_entry = ttk.Entry(folder_input_frame) + self.folder_entry.pack(side='left', fill='x', expand=True, padx=(0, 10)) - layout.addLayout(grid) + self.browse_btn = tk.Button(folder_input_frame, text="Browse...", width=10, + bg='#3c3c3c', fg='#e0e0e0', activebackground='#4c4c4c', + relief='raised', borderwidth=1) + self.browse_btn.config(command=self._browse_folder) + self.browse_btn.pack(side='right') - # Разделитель - separator = QFrame() - separator.setFrameShape(QFrame.HLine) - separator.setStyleSheet("background-color: #333333; max-height: 1px;") - layout.addWidget(separator) + # Separator + ttk.Separator(main_frame, orient='horizontal').pack(fill='x', pady=15) - # Кнопки типов кадров - types_layout = QHBoxLayout() - types_layout.setSpacing(20) - types_layout.setAlignment(Qt.AlignCenter) + # Type buttons + types_frame = ttk.Frame(main_frame) + types_frame.pack(pady=10) - self.bias_btn = QPushButton("⚪ BIAS") - self.bias_btn.setFixedSize(120, 50) - self.bias_btn.setStyleSheet(""" - QPushButton { - background-color: #2196F3; - color: white; - font-weight: bold; - font-size: 14px; - border-radius: 6px; - } - QPushButton:hover { - background-color: #1976D2; - } - """) - self.bias_btn.clicked.connect(lambda: self._open_calibration_type('bias')) + self.bias_btn = tk.Button(types_frame, text="BIAS", font=('Segoe UI', 12, 'bold'), + bg='#2196F3', fg='white', activebackground='#1976D2', + width=12, height=2, + command=lambda: self._open_calibration_type('bias')) + self.bias_btn.pack(side='left', padx=10) - self.dark_btn = QPushButton("🌑 DARK") - self.dark_btn.setFixedSize(120, 50) - self.dark_btn.setStyleSheet(""" - QPushButton { - background-color: #9C27B0; - color: white; - font-weight: bold; - font-size: 14px; - border-radius: 6px; - } - QPushButton:hover { - background-color: #7B1FA2; - } - """) - self.dark_btn.clicked.connect(lambda: self._open_calibration_type('dark')) + self.dark_btn = tk.Button(types_frame, text="DARK", font=('Segoe UI', 12, 'bold'), + bg='#9C27B0', fg='white', activebackground='#7B1FA2', + width=12, height=2, + command=lambda: self._open_calibration_type('dark')) + self.dark_btn.pack(side='left', padx=10) - self.flat_btn = QPushButton("📖 FLAT") - self.flat_btn.setFixedSize(120, 50) - self.flat_btn.setStyleSheet(""" - QPushButton { - background-color: #4CAF50; - color: white; - font-weight: bold; - font-size: 14px; - border-radius: 6px; - } - QPushButton:hover { - background-color: #388E3C; - } - """) - self.flat_btn.clicked.connect(lambda: self._open_calibration_type('flat')) + self.flat_btn = tk.Button(types_frame, text="FLAT", font=('Segoe UI', 12, 'bold'), + bg='#4CAF50', fg='white', activebackground='#388E3C', + width=12, height=2, + command=lambda: self._open_calibration_type('flat')) + self.flat_btn.pack(side='left', padx=10) - types_layout.addWidget(self.bias_btn) - types_layout.addWidget(self.dark_btn) - types_layout.addWidget(self.flat_btn) + # Tips frame + tips_frame = tk.Frame(main_frame, bg='#2d2d2d', relief='groove', bd=1) + tips_frame.pack(fill='x', pady=15, padx=10) - layout.addLayout(types_layout) + tk.Label(tips_frame, text="Tips:", font=('Segoe UI', 10, 'bold'), + bg='#2d2d2d', fg='#FFD700').pack(anchor='w', padx=10, pady=(10, 5)) - # Совет - tips_frame = QFrame() - tips_frame.setStyleSheet(""" - QFrame { - background-color: #2d2d2d; - border-radius: 8px; - padding: 10px; - } - """) - tips_layout = QVBoxLayout(tips_frame) + self.tips_label = tk.Label(tips_frame, + text="• BIAS can be taken once a month (at home)\n• DARK must be taken on site at the same temperature\n• FLAT must be taken after session without changing focus", + bg='#2d2d2d', fg='#e0e0e0', justify='left', font=('Segoe UI', 9)) + self.tips_label.pack(anchor='w', padx=10, pady=(0, 10)) - tips_title = QLabel("💡 Совет") - tips_title.setFont(QFont("", 11, QFont.Bold)) - tips_layout.addWidget(tips_title) - - self.tips_label = QLabel( - "• BIAS снимаются один раз на месяц (можно дома)\n" - "• DARK снимаются на месте съёмки при той же температуре\n" - "• FLAT снимаются после сессии без изменения фокуса" - ) - self.tips_label.setWordWrap(True) - tips_layout.addWidget(self.tips_label) - - layout.addWidget(tips_frame) - - # Кнопки отмена/закрыть - buttons_layout = QHBoxLayout() - buttons_layout.addStretch() - - cancel_btn = QPushButton("❌ Отмена") - cancel_btn.clicked.connect(self.reject) - buttons_layout.addWidget(cancel_btn) - - layout.addLayout(buttons_layout) + # Cancel button + btn_frame = ttk.Frame(main_frame) + btn_frame.pack(pady=10) + ttk.Button(btn_frame, text="Cancel", command=self.destroy).pack() def _load_saved_settings(self): - """Загружает сохранённые камеры""" cameras = self.config_service.get_cameras() if cameras: - self.camera_combo.addItems(cameras) - + self.camera_combo['values'] = cameras last_camera = self.config_service.get_last_camera() if last_camera and last_camera in cameras: - self.camera_combo.setCurrentText(last_camera) + self.camera_combo.set(last_camera) def _browse_folder(self): - """Выбор папки для калибровочных кадров""" - folder = QFileDialog.getExistingDirectory(self, "Выберите папку для калибровочных кадров") + folder = filedialog.askdirectory(title="Select folder for calibration frames") if folder: - self.folder_entry.setText(folder) - self._stop_browse_blinking() + self.folder_entry.delete(0, tk.END) + self.folder_entry.insert(0, folder) + self._stop_blinking() + self.browse_btn.config(bg='#3c3c3c', fg='#e0e0e0') def _check_folder_path(self): - """Проверяет, заполнено ли поле пути и запускает мигание если нет""" - if not self.folder_entry.text(): - self._start_browse_blinking() + if not self.folder_entry.get(): + self._start_blinking() else: - self._stop_browse_blinking() + self._stop_blinking() - def _start_browse_blinking(self): - """Запускает мигание кнопки 'Обзор' зелёным цветом""" - self._browse_blink_timer = QTimer() - self._browse_blink_timer.timeout.connect(self._do_browse_blink) - self._browse_blink_timer.start(500) + def _start_blinking(self): + self._blink_active = True - def _do_browse_blink(self): - """Мигание кнопки""" - current_style = self.browse_button.styleSheet() - if "background-color: #4CAF50" in current_style: - self.browse_button.setStyleSheet(""" - QPushButton { - background-color: #2196F3; - color: white; - font-weight: bold; - border-radius: 4px; - } - """) - else: - self.browse_button.setStyleSheet(""" - QPushButton { - background-color: #4CAF50; - color: white; - font-weight: bold; - border-radius: 4px; - } - """) + def blink(): + if not self._blink_active: + return + if self.browse_btn.cget('bg') == '#3c3c3c': + self.browse_btn.config(bg='#f44336', fg='white') + else: + self.browse_btn.config(bg='#3c3c3c', fg='#e0e0e0') + self._blink_after_id = self.after(1500, blink) - def _stop_browse_blinking(self): - """Останавливает мигание кнопки""" - if self._browse_blink_timer: - self._browse_blink_timer.stop() - self._browse_blink_timer = None - self.browse_button.setStyleSheet(""" - QPushButton { - background-color: #2196F3; - color: white; - font-weight: bold; - border-radius: 4px; - } - """) + blink() - def _open_calibration_type(self, cal_type: str): - """Открывает дочернее окно для выбранного типа калибровки""" - if not self.folder_entry.text(): - QMessageBox.warning(self, "Внимание", "Сначала выберите папку для сохранения!") - self._start_browse_blinking() + def _stop_blinking(self): + self._blink_active = False + if self._blink_after_id: + self.after_cancel(self._blink_after_id) + self._blink_after_id = None + self.browse_btn.config(bg='#3c3c3c', fg='#e0e0e0') + + def _center_window(self): + self.update_idletasks() + x = self.parent.winfo_x() + (self.parent.winfo_width() // 2) - (self.winfo_width() // 2) + y = self.parent.winfo_y() + (self.parent.winfo_height() // 2) - (self.winfo_height() // 2) + self.geometry(f'+{x}+{y}') + + def _open_calibration_type(self, cal_type): + if not self.folder_entry.get(): + messagebox.showwarning("Warning", "Please select a folder to save calibration frames!", parent=self) + self._start_blinking() return - camera_name = self.camera_combo.currentText() + camera_name = self.camera_combo.get() if not camera_name: - QMessageBox.warning(self, "Внимание", "Введите или выберите название камеры!") + messagebox.showwarning("Warning", "Please enter or select a camera name!", parent=self) return from ui.dialogs.calibration_type_dialog import CalibrationTypeDialog dialog = CalibrationTypeDialog( self, cal_type, - self.folder_entry.text(), + self.folder_entry.get(), camera_name, self.config_service ) - - if dialog.exec(): - # После успешной съёмки - QMessageBox.information(self, "Успех", f"Съёмка {cal_type.upper()} завершена!") - - def reject(self): - """Закрытие диалога""" - if hasattr(self, '_browse_blink_timer') and self._browse_blink_timer: - self._browse_blink_timer.stop() - super().reject() \ No newline at end of file + self.wait_window(dialog) \ No newline at end of file diff --git a/ui/dialogs/calibration_type_dialog.py b/ui/dialogs/calibration_type_dialog.py index 1b3196a..d49be82 100644 --- a/ui/dialogs/calibration_type_dialog.py +++ b/ui/dialogs/calibration_type_dialog.py @@ -1,75 +1,56 @@ """ -CalibrationTypeDialog - диалог для конкретного типа калибровки -Dark / Bias / Flat с прогрессом, авто-остановкой и профилями +CalibrationTypeDialog - диалог для конкретного типа калибровки (tkinter) """ + +import tkinter as tk +from tkinter import ttk, messagebox import shutil -import os import re from pathlib import Path from datetime import datetime -from typing import Optional -from PySide6.QtWidgets import ( - QDialog, QVBoxLayout, QHBoxLayout, QGridLayout, - QLabel, QComboBox, QSpinBox, QPushButton, QFrame, - QProgressBar, QMessageBox, QGroupBox, - QInputDialog, QWidget, QFileDialog -) -from PySide6.QtCore import Qt, QTimer, QMetaObject, Q_ARG, Signal -from PySide6.QtGui import QFont - -from services.config_service import ConfigService from services.file_service import FileService from services.watch_service import WatchService -class CalibrationTypeDialog(QDialog): +class CalibrationTypeDialog(tk.Toplevel): """Диалог для съёмки калибровочных кадров определённого типа""" - # Сигнал для безопасного обновления UI из другого потока - progress_updated = Signal(int, int) # current, target - capture_completed = Signal(str) # target_folder - - def __init__(self, parent, cal_type: str, base_folder: str, - camera_name: str, config_service: ConfigService): + def __init__(self, parent, cal_type, base_folder, camera_name, config_service): super().__init__(parent) - - self.cal_type = cal_type # 'bias', 'dark', 'flat' + self.parent = parent + self.cal_type = cal_type self.base_folder = Path(base_folder) self.camera_name = camera_name self.config_service = config_service - # Состояние съёмки self.is_capturing = False self.current_count = 0 self.target_count = 0 - self._calibration_watch_service = None + self._watch_service = None - # Настройки для разных типов self.settings = self._get_default_settings() - self.setWindowTitle(self._get_title()) - self.setMinimumSize(550, 600) - self.resize(600, 650) - - # Подключаем сигналы - self.progress_updated.connect(self._on_progress_updated) - self.capture_completed.connect(self._on_capture_completed) + self.title(self._get_title()) + self.geometry("600x650") + self.minsize(550, 600) + self.transient(parent) + self.grab_set() self._create_ui() self._load_optics() self._update_recommendations() + self._center_window() - def _get_title(self) -> str: + def _get_title(self): titles = { - 'bias': '⚪ BIAS (Кадры смещения)', - 'dark': '🌑 DARK (Тёмные кадры)', - 'flat': '📖 FLAT (Плоские поля)' + 'bias': 'BIAS (Bias Frames)', + 'dark': 'DARK (Dark Frames)', + 'flat': 'FLAT (Flat Fields)' } - return titles.get(self.cal_type, 'Калибровочные кадры') + return titles.get(self.cal_type, 'Calibration Frames') - def _get_default_settings(self) -> dict: - """Возвращает настройки по умолчанию для типа калибровки""" + def _get_default_settings(self): base = { 'bias': { 'iso_values': [800, 1600, 3200], @@ -102,266 +83,152 @@ class CalibrationTypeDialog(QDialog): return base.get(self.cal_type, {}) def _create_ui(self): - layout = QVBoxLayout(self) - layout.setSpacing(15) - layout.setContentsMargins(20, 20, 20, 20) + # Main frame + main_frame = ttk.Frame(self, padding="20") + main_frame.pack(fill='both', expand=True) - # Заголовок с кнопкой справки - header_layout = QHBoxLayout() + # Title with help button + title_frame = ttk.Frame(main_frame) + title_frame.pack(fill='x', pady=(0, 10)) - title_label = QLabel(self._get_title()) - title_font = QFont() - title_font.setPointSize(16) - title_font.setBold(True) - title_label.setFont(title_font) - header_layout.addWidget(title_label) + ttk.Label(title_frame, text=self._get_title(), font=('Segoe UI', 16, 'bold')).pack(side='left') - help_btn = QPushButton("❓") - help_btn.setFixedSize(30, 30) - help_btn.setToolTip("Показать справку") - help_btn.clicked.connect(self._show_help) - header_layout.addWidget(help_btn) - header_layout.addStretch() + help_btn = ttk.Button(title_frame, text="?", width=3, command=self._show_help) + help_btn.pack(side='right') - layout.addLayout(header_layout) - - # Группа параметров - params_group = QGroupBox("⚙️ Параметры съёмки") - params_layout = QGridLayout(params_group) - params_layout.setVerticalSpacing(12) - params_layout.setHorizontalSpacing(15) - - row = 0 + # Parameters frame + params_frame = ttk.LabelFrame(main_frame, text="Camera Settings", padding="10") + params_frame.pack(fill='x', pady=10) # ISO - iso_label = QLabel("ISO:") - iso_label.setFont(QFont("", 10, QFont.Bold)) - params_layout.addWidget(iso_label, row, 0) + iso_row = ttk.Frame(params_frame) + iso_row.pack(fill='x', pady=5) + ttk.Label(iso_row, text="ISO:", width=15).pack(side='left') + self.iso_combo = ttk.Combobox(iso_row, values=self.settings['iso_values'], width=10) + self.iso_combo.set(str(self.settings['default_iso'])) + self.iso_combo.pack(side='left', padx=5) + self.iso_combo.bind('<>', lambda e: self._update_recommendations()) - self.iso_combo = QComboBox() - self.iso_combo.addItems([str(v) for v in self.settings['iso_values']]) - self.iso_combo.setCurrentText(str(self.settings['default_iso'])) - self.iso_combo.currentTextChanged.connect(self._update_recommendations) - params_layout.addWidget(self.iso_combo, row, 1) + ttk.Button(iso_row, text="Custom", width=8, command=self._add_custom_iso).pack(side='left', padx=5) - self.custom_iso_btn = QPushButton("➕ своё") - self.custom_iso_btn.setFixedWidth(60) - self.custom_iso_btn.clicked.connect(self._add_custom_iso) - params_layout.addWidget(self.custom_iso_btn, row, 2) - - row += 1 - - # Выдержка (только для DARK) + # Exposure (only for DARK) if self.cal_type == 'dark': - exposure_label = QLabel("Выдержка (сек):") - exposure_label.setFont(QFont("", 10, QFont.Bold)) - params_layout.addWidget(exposure_label, row, 0) + exp_row = ttk.Frame(params_frame) + exp_row.pack(fill='x', pady=5) + ttk.Label(exp_row, text="Exposure (sec):", width=15).pack(side='left') + self.exposure_combo = ttk.Combobox(exp_row, values=self.settings['exposure_values'], width=10) + self.exposure_combo.set(str(self.settings['default_exposure'])) + self.exposure_combo.pack(side='left', padx=5) + self.exposure_combo.bind('<>', lambda e: self._update_recommendations()) + ttk.Button(exp_row, text="Custom", width=8, command=self._add_custom_exposure).pack(side='left', padx=5) - self.exposure_combo = QComboBox() - self.exposure_combo.addItems([str(v) for v in self.settings['exposure_values']]) - self.exposure_combo.setCurrentText(str(self.settings['default_exposure'])) - self.exposure_combo.currentTextChanged.connect(self._update_recommendations) - params_layout.addWidget(self.exposure_combo, row, 1) - - self.custom_exposure_btn = QPushButton("➕ своё") - self.custom_exposure_btn.setFixedWidth(60) - self.custom_exposure_btn.clicked.connect(self._add_custom_exposure) - params_layout.addWidget(self.custom_exposure_btn, row, 2) - - row += 1 - - # Оптика (только для FLAT) + # Optics (only for FLAT) if self.cal_type == 'flat': - optics_label = QLabel("Оптика:") - optics_label.setFont(QFont("", 10, QFont.Bold)) - params_layout.addWidget(optics_label, row, 0) + optics_row = ttk.Frame(params_frame) + optics_row.pack(fill='x', pady=5) + ttk.Label(optics_row, text="Optics:", width=15).pack(side='left') + self.optics_combo = ttk.Combobox(optics_row, width=30) + self.optics_combo.pack(side='left', fill='x', expand=True, padx=5) - self.optics_combo = QComboBox() - self.optics_combo.setEditable(True) - params_layout.addWidget(self.optics_combo, row, 1, 1, 2) + aperture_row = ttk.Frame(params_frame) + aperture_row.pack(fill='x', pady=5) + ttk.Label(aperture_row, text="Aperture:", width=15).pack(side='left') + self.aperture_combo = ttk.Combobox(aperture_row, values=self.settings['aperture_values'], width=10) + self.aperture_combo.set('f/5.6') + self.aperture_combo.pack(side='left', padx=5) - row += 1 + ttk.Label(aperture_row, text="(Fixed for telescopes)", foreground='#888888').pack(side='left', padx=10) - aperture_label = QLabel("Диафрагма:") - aperture_label.setFont(QFont("", 10, QFont.Bold)) - params_layout.addWidget(aperture_label, row, 0) + # Count + count_row = ttk.Frame(params_frame) + count_row.pack(fill='x', pady=5) + ttk.Label(count_row, text="Number of frames:", width=15).pack(side='left') - self.aperture_combo = QComboBox() - self.aperture_combo.setEditable(True) - self.aperture_combo.addItems(self.settings['aperture_values']) - params_layout.addWidget(self.aperture_combo, row, 1, 1, 2) + self.count_spin = tk.Spinbox(count_row, from_=self.settings['min_count'], to=self.settings['max_count'], + width=8, font=('Segoe UI', 10)) + self.count_spin.delete(0, 'end') + self.count_spin.insert(0, str(self.settings['count'])) + self.count_spin.pack(side='left', padx=5) - row += 1 + ttk.Label(count_row, text=f"(recommended: {self.settings['recommended_count']})", foreground='#888888').pack(side='left', padx=5) - telescope_hint = QLabel("💡 Для телескопов диафрагма фиксированная и выбирается автоматически") - telescope_hint.setStyleSheet("color: #888888; font-size: 10px;") - params_layout.addWidget(telescope_hint, row, 0, 1, 3) + # Recommendations frame + tips_frame = tk.Frame(main_frame, bg='#2d2d2d', relief='groove', bd=1) + tips_frame.pack(fill='x', pady=10) - row += 1 + self.tips_text = tk.Text(tips_frame, height=12, wrap='word', bg='#2d2d2d', fg='#FFD700', + font=('Segoe UI', 9), relief='flat', padx=10, pady=10) + self.tips_text.pack(fill='both', expand=True) - # Количество кадров - count_label = QLabel("Количество кадров:") - count_label.setFont(QFont("", 10, QFont.Bold)) - params_layout.addWidget(count_label, row, 0) + # Progress frame + self.progress_frame = ttk.LabelFrame(main_frame, text="Progress", padding="10") - self.count_spin = QSpinBox() - self.count_spin.setMinimum(self.settings['min_count']) - self.count_spin.setMaximum(self.settings['max_count']) - self.count_spin.setValue(self.settings['count']) - self.count_spin.setSuffix(" кадров") - params_layout.addWidget(self.count_spin, row, 1) + self.progress_bar = ttk.Progressbar(self.progress_frame, orient='horizontal', length=400, mode='determinate') + self.progress_bar.pack(pady=5) - self.recommended_label = QLabel(f"(рекомендуется {self.settings['recommended_count']})") - self.recommended_label.setStyleSheet("color: #888888;") - params_layout.addWidget(self.recommended_label, row, 2) + self.progress_label = ttk.Label(self.progress_frame, text="Ready to shoot") + self.progress_label.pack() - layout.addWidget(params_group) + # Save path + save_frame = ttk.Frame(main_frame) + save_frame.pack(fill='x', pady=10) - # Группа рекомендаций - tips_group = QGroupBox("📖 Рекомендации") - tips_group.setStyleSheet(""" - QGroupBox { - font-weight: bold; - margin-top: 10px; - } - QGroupBox::title { - subcontrol-origin: margin; - left: 10px; - padding: 0 5px 0 5px; - } - """) - tips_layout = QVBoxLayout(tips_group) + ttk.Label(save_frame, text="Save to:", font=('Segoe UI', 9, 'bold')).pack(anchor='w') + self.save_path_label = ttk.Label(save_frame, foreground='#4CAF50') + self.save_path_label.pack(anchor='w') - self.tips_text = QLabel() - self.tips_text.setWordWrap(True) - self.tips_text.setStyleSheet("color: #FFD700; padding: 5px;") - tips_layout.addWidget(self.tips_text) + # Buttons + btn_frame = ttk.Frame(main_frame) + btn_frame.pack(pady=10) - layout.addWidget(tips_group) + self.back_btn = ttk.Button(btn_frame, text="Back", command=self._on_back) + self.back_btn.pack(side='left', padx=5) - # Группа прогресса - self.progress_group = QGroupBox("📊 Прогресс съёмки") - self.progress_group.setVisible(False) - progress_layout = QVBoxLayout(self.progress_group) + self.start_btn = ttk.Button(btn_frame, text="Start Shooting", command=self._start_capture, style='Green.TButton') + self.start_btn.pack(side='left', padx=5) - self.progress_bar = QProgressBar() - self.progress_bar.setMinimum(0) - progress_layout.addWidget(self.progress_bar) - - self.progress_status = QLabel("Готов к съёмке") - self.progress_status.setAlignment(Qt.AlignCenter) - progress_layout.addWidget(self.progress_status) - - layout.addWidget(self.progress_group) - - # Информация о сохранении - save_info = QFrame() - save_info.setStyleSheet(""" - QFrame { - background-color: #2d2d2d; - border-radius: 6px; - padding: 8px; - } - """) - save_layout = QVBoxLayout(save_info) - - save_label = QLabel("💾 Сохранить в:") - save_label.setFont(QFont("", 10, QFont.Bold)) - save_layout.addWidget(save_label) - - self.save_path_label = QLabel() - self.save_path_label.setWordWrap(True) - self.save_path_label.setStyleSheet("color: #4CAF50; font-family: monospace;") - save_layout.addWidget(self.save_path_label) - - layout.addWidget(save_info) - - # Кнопки действий - buttons_layout = QHBoxLayout() - buttons_layout.addStretch() - - self.back_btn = QPushButton("◀ Назад") - self.back_btn.clicked.connect(self._on_back_clicked) - buttons_layout.addWidget(self.back_btn) - - self.start_btn = QPushButton("▶ Начать съёмку") - self.start_btn.setStyleSheet(""" - QPushButton { - background-color: #4CAF50; - color: white; - font-weight: bold; - padding: 8px 20px; - border-radius: 4px; - } - QPushButton:hover { - background-color: #388E3C; - } - """) - self.start_btn.clicked.connect(self._start_capture) - buttons_layout.addWidget(self.start_btn) - - self.stop_btn = QPushButton("⏹️ Остановить") - self.stop_btn.setStyleSheet(""" - QPushButton { - background-color: #f44336; - color: white; - font-weight: bold; - padding: 8px 20px; - border-radius: 4px; - } - QPushButton:hover { - background-color: #d32f2f; - } - """) - self.stop_btn.clicked.connect(self._on_stop_clicked) - self.stop_btn.setVisible(False) - buttons_layout.addWidget(self.stop_btn) - - layout.addLayout(buttons_layout) + self.stop_btn = ttk.Button(btn_frame, text="Stop", command=self._on_stop, style='Red.TButton') + self.stop_btn.pack(side='left', padx=5) + self.stop_btn.config(state='disabled') self._update_save_path() def _load_optics(self): - """Загружает список оптики (объективы + телескопы) для FLAT режима""" if self.cal_type != 'flat': return lenses = self.config_service.get_lenses() telescopes = self.config_service.get_telescopes() - all_optics = [] - for lens in lenses: - all_optics.append(f"🔭 {lens}") - for telescope in telescopes: - all_optics.append(f"🪐 {telescope}") + all_optics = lenses + telescopes - self.optics_combo.addItems(all_optics) + self.optics_combo['values'] = all_optics + if all_optics: + self.optics_combo.set(all_optics[0]) - def on_optics_changed(): - current = self.optics_combo.currentText() - if current.startswith("🪐"): - self.aperture_combo.setEnabled(False) + def on_optics_change(*args): + current = self.optics_combo.get() + if 'f/' in current: + self.aperture_combo.config(state='disabled') match = re.search(r'f/(\d+\.?\d*)', current) if match: - self.aperture_combo.setCurrentText(f"f/{match.group(1)}") + self.aperture_combo.set(f"f/{match.group(1)}") else: - self.aperture_combo.setEnabled(True) + self.aperture_combo.config(state='normal') - if all_optics: - self.optics_combo.currentTextChanged.connect(on_optics_changed) + self.optics_combo.bind('<>', on_optics_change) def _update_save_path(self): - """Обновляет отображение пути сохранения""" - iso = int(self.iso_combo.currentText()) + iso = int(self.iso_combo.get()) if self.cal_type == 'bias': path = self.base_folder / "Calibration" / self.camera_name / "Bias" / f"ISO{iso}" elif self.cal_type == 'dark': - exposure = self.exposure_combo.currentText() + exposure = self.exposure_combo.get() path = self.base_folder / "Calibration" / self.camera_name / "Dark" / f"ISO{iso}_{exposure}s" elif self.cal_type == 'flat': - optics = self.optics_combo.currentText() - optics_name = optics.replace("🔭", "").replace("🪐", "").strip() + optics = self.optics_combo.get() + optics_name = optics invalid_chars = '<>:"/\\|?*' for char in invalid_chars: optics_name = optics_name.replace(char, '_') @@ -370,157 +237,127 @@ class CalibrationTypeDialog(QDialog): else: path = self.base_folder - self.save_path_label.setText(str(path)) + self.save_path_label.config(text=str(path)) return path def _update_recommendations(self): - """Обновляет рекомендации в зависимости от типа калибровки""" - if self.cal_type == 'bias': - self.tips_text.setText( - "⚪ BIAS (Кадры смещения)\n\n" - "📌 КАК СНИМАТЬ:\n" - "• Закройте крышку объектива\n" - "• Выдержка: САМАЯ КОРОТКАЯ (1/4000 или 1/8000)\n" - "• ISO: тот же, что при съёмке световых кадров\n\n" - "💡 СОВЕТ:\n" - "• Можно снять дома в любое время\n" - "• Используются для всех объективов\n" - "• 50 кадров оптимально для хорошего усреднения" - ) - elif self.cal_type == 'dark': - self.tips_text.setText( - "🌑 DARK (Тёмные кадры)\n\n" - "⚠️ ВАЖНО: Снимайте ПОСЛЕ сессии на месте!\n\n" - "📌 КАК СНИМАТЬ:\n" - "• Закройте крышку объектива\n" - "• ТЕ ЖЕ параметры ISO и выдержки, что при съёмке\n" - "• Дождитесь, пока камера прогреется до ночной температуры\n\n" - "🌡️ ТЕМПЕРАТУРА:\n" - "• Снимайте ПРИ ТОЙ ЖЕ температуре, что и Light кадры\n" - "• Разница >5°C делает кадры бесполезными!\n" - "• Лучше снять сразу после сессии, пока камера не остыла" - ) - elif self.cal_type == 'flat': - self.tips_text.setText( - "📖 FLAT (Плоские поля)\n\n" - "⚠️ ВАЖНО: НЕ меняйте фокус и зум после съёмки!\n\n" - "📌 КАК СНИМАТЬ:\n" - "• Способ 1: LED-планшет (рекомендуется)\n" - "• Способ 2: Рассвет/закат, камера в зенит\n" - "• Способ 3: Белая футболка на объектив\n\n" - "🎯 ЦЕЛЬ:\n" - "• Убрать виньетирование и пыль на оптике\n" - "• Гистограмма должна быть на 50-70%\n" - "• 30 кадров достаточно для хорошего результата" - ) + self.tips_text.config(state='normal') + self.tips_text.delete('1.0', 'end') + if self.cal_type == 'bias': + self.tips_text.insert('1.0', + "BIAS (Bias Frames)\n\n" + "HOW TO SHOOT:\n" + "• Close lens cap\n" + "• Shutter speed: FASTEST (1/4000 or 1/8000)\n" + "• ISO: same as light frames\n\n" + "TIPS:\n" + "• Can be taken at home anytime\n" + "• Works for all lenses/telescopes\n" + "• 50 frames recommended") + elif self.cal_type == 'dark': + self.tips_text.insert('1.0', + "DARK (Dark Frames)\n\n" + "IMPORTANT: Shoot AFTER session on site!\n\n" + "HOW TO SHOOT:\n" + "• Close lens cap\n" + "• SAME ISO and exposure as light frames\n" + "• Wait for camera to reach night temperature\n\n" + "TEMPERATURE:\n" + "• Shoot at the SAME temperature as light frames\n" + "• Difference >5C makes darks useless!\n" + "• Best taken immediately after session") + elif self.cal_type == 'flat': + self.tips_text.insert('1.0', + "FLAT (Flat Fields)\n\n" + "IMPORTANT: DON'T change focus or zoom!\n\n" + "HOW TO SHOOT:\n" + "• Method 1: LED panel (recommended)\n" + "• Method 2: Dawn/dusk sky, point at zenith\n" + "• Method 3: White T-shirt over lens\n\n" + "GOAL:\n" + "• Remove vignetting and dust\n" + "• Histogram at 50-70%\n" + "• 30 frames recommended") + + self.tips_text.config(state='disabled') self._update_save_path() def _start_capture(self): - """Начинает съёмку калибровочных кадров""" - self.target_count = self.count_spin.value() + self.target_count = int(self.count_spin.get()) self.current_count = 0 target_folder = self._update_save_path() - # Создаём папку с проверкой try: target_folder.mkdir(parents=True, exist_ok=True) - print(f"Папка создана: {target_folder}") except Exception as e: - QMessageBox.critical(self, "Ошибка", f"Не удалось создать папку:\n{target_folder}\n\nОшибка: {e}") + messagebox.showerror("Error", f"Failed to create folder:\n{target_folder}\n\n{str(e)}", parent=self) return - self.progress_group.setVisible(True) - self.progress_bar.setMaximum(self.target_count) - self.progress_bar.setValue(0) - self.progress_status.setText(f"0 из {self.target_count} кадров") + # Show progress frame + self.progress_frame.pack(fill='x', pady=10) + self.progress_bar['maximum'] = self.target_count + self.progress_bar['value'] = 0 + self.progress_label.config(text=f"0 / {self.target_count} frames") - # Меняем кнопки - self.start_btn.setVisible(False) - self.stop_btn.setVisible(True) - self.back_btn.setEnabled(False) - - # Блокируем изменение параметров - self.iso_combo.setEnabled(False) - self.count_spin.setEnabled(False) - if self.cal_type == 'dark': - self.exposure_combo.setEnabled(False) - if self.cal_type == 'flat': - self.optics_combo.setEnabled(False) - self.aperture_combo.setEnabled(False) + # Disable controls + self.start_btn.config(state='disabled') + self.stop_btn.config(state='normal') + self.back_btn.config(state='disabled') + self.iso_combo.config(state='disabled') + self.count_spin.config(state='disabled') + if hasattr(self, 'exposure_combo'): + self.exposure_combo.config(state='disabled') + if hasattr(self, 'optics_combo'): + self.optics_combo.config(state='disabled') + self.aperture_combo.config(state='disabled') self.is_capturing = True - # Получаем папку наблюдения + # Get watch folder from main window watch_folder = self._get_watch_folder() - print(f"Получена папка наблюдения: {watch_folder}") - if not watch_folder: - QMessageBox.critical(self, "Ошибка", - "Не удалось определить папку наблюдения!\nУбедитесь, что вы выбрали папку в главном окне.") + messagebox.showerror("Error", "Could not determine watch folder!\nPlease select a folder in the main window.", parent=self) self._stop_capture() return if not watch_folder.exists(): - QMessageBox.critical(self, "Ошибка", f"Папка наблюдения не существует:\n{watch_folder}") + messagebox.showerror("Error", f"Watch folder does not exist:\n{watch_folder}", parent=self) self._stop_capture() return - # Очищаем папку наблюдения от старых файлов FileService.clear_watch_folder(watch_folder) - # Создаём НОВЫЙ WatchService для калибровки - self._calibration_watch_service = WatchService() + self._watch_service = WatchService() - # Функция обратного вызова при получении файла (выполняется в потоке WatchService) - def on_file_received(file_path: Path): + def on_file_received(file_path): if not self.is_capturing: return - print(f"Обнаружен файл: {file_path}") if self._process_calibration_file(file_path, target_folder): - # Увеличиваем счётчик - new_count = self.current_count + 1 - # Отправляем сигнал для обновления UI в главном потоке - self.progress_updated.emit(new_count, self.target_count) + self.current_count += 1 + self.after(0, self._update_progress) - if new_count >= self.target_count: - # Отправляем сигнал о завершении - self.capture_completed.emit(str(target_folder)) - - print("Запуск WatchService для калибровки...") - success = self._calibration_watch_service.start(watch_folder, on_file_received) - print(f"Результат запуска: {success}") + if self.current_count >= self.target_count: + self.after(0, self._stop_capture) + self.after(0, lambda: messagebox.showinfo("Success", f"Capture completed!\nSaved {self.current_count} frames to:\n{target_folder}", parent=self)) + self.after(100, lambda: self.progress_frame.pack_forget()) + success = self._watch_service.start(watch_folder, on_file_received) if not success: - QMessageBox.critical(self, "Ошибка", "Не удалось запустить отслеживание папки!") + messagebox.showerror("Error", "Failed to start watching folder!", parent=self) self._stop_capture() return - self.progress_status.setText(f"Отслеживается папка: {watch_folder}\nОжидание новых файлов...") + self.progress_label.config(text=f"Watching: {watch_folder}\nWaiting for files...") - def _on_progress_updated(self, current: int, target: int): - """Обновляет прогресс (вызывается из основного потока по сигналу)""" - self.current_count = current - self.progress_bar.setValue(current) - self.progress_status.setText(f"Снято {current} из {target} кадров") - print(f"Прогресс: {current}/{target}") + def _update_progress(self): + self.progress_bar['value'] = self.current_count + self.progress_label.config(text=f"{self.current_count} / {self.target_count} frames") - def _on_capture_completed(self, target_folder: str): - """Обработчик завершения съёмки (вызывается из основного потока по сигналу)""" - if self.is_capturing: - self._stop_capture() - QMessageBox.information(self, "Успех", - f"✅ Съёмка завершена!\n" - f"Сохранено {self.current_count} кадров в:\n{target_folder}") - # Скрываем группу прогресса после сообщения - self.progress_group.setVisible(False) - - def _process_calibration_file(self, file_path: Path, target_folder: Path) -> bool: - """Обрабатывает файл из папки наблюдения""" + def _process_calibration_file(self, file_path, target_folder): if not FileService.is_photo(file_path): - print(f"Файл {file_path.name} не является фото, пропускаем") return False try: @@ -532,19 +369,19 @@ class CalibrationTypeDialog(QDialog): suffix = file_path.suffix if self.cal_type == 'bias': - iso = self.iso_combo.currentText() + iso = self.iso_combo.get() prefix = f"Bias_{self.camera_name}_ISO{iso}" elif self.cal_type == 'dark': - iso = self.iso_combo.currentText() - exposure = self.exposure_combo.currentText() + iso = self.iso_combo.get() + exposure = self.exposure_combo.get() prefix = f"Dark_{self.camera_name}_ISO{iso}_{exposure}s" elif self.cal_type == 'flat': - optics = self.optics_combo.currentText() - optics_name = optics.replace("🔭", "").replace("🪐", "").strip() + optics = self.optics_combo.get() + optics_name = optics invalid_chars = '<>:"/\\|?*' for char in invalid_chars: optics_name = optics_name.replace(char, '_') - aperture = self.aperture_combo.currentText() + aperture = self.aperture_combo.get() prefix = f"Flat_{optics_name}_{aperture}" else: prefix = "Calibration" @@ -557,174 +394,181 @@ class CalibrationTypeDialog(QDialog): target_path = FileService.resolve_conflict(target_path) shutil.move(str(file_path), str(target_path)) - print(f"Файл сохранён: {target_path}") return True except Exception as e: - print(f"Ошибка сохранения {file_path.name}: {e}") + print(f"Error saving {file_path.name}: {e}") return False def _stop_capture(self): - """Останавливает съёмку""" self.is_capturing = False - if self._calibration_watch_service: - self._calibration_watch_service.stop() - self._calibration_watch_service = None + if self._watch_service: + self._watch_service.stop() + self._watch_service = None - self.start_btn.setVisible(True) - self.stop_btn.setVisible(False) - self.back_btn.setEnabled(True) + self.start_btn.config(state='normal') + self.stop_btn.config(state='disabled') + self.back_btn.config(state='normal') + self.iso_combo.config(state='normal') + self.count_spin.config(state='normal') + if hasattr(self, 'exposure_combo'): + self.exposure_combo.config(state='normal') + if hasattr(self, 'optics_combo'): + self.optics_combo.config(state='normal') + self.aperture_combo.config(state='normal') - self.iso_combo.setEnabled(True) - self.count_spin.setEnabled(True) - if self.cal_type == 'dark': - self.exposure_combo.setEnabled(True) - if self.cal_type == 'flat': - self.optics_combo.setEnabled(True) - self.aperture_combo.setEnabled(True) + self.progress_label.config(text="Capture stopped") - self.progress_status.setText("Съёмка остановлена") - - # Скрываем группу прогресса через 2 секунды - QTimer.singleShot(2000, lambda: self.progress_group.setVisible(False)) - - def _on_back_clicked(self): - """Обработчик кнопки 'Назад'""" + def _on_back(self): if self.is_capturing: - QMessageBox.warning(self, "Внимание", "Сначала остановите съёмку!") + messagebox.showwarning("Warning", "Please stop the capture first!", parent=self) return - self.reject() + self.destroy() - def _on_stop_clicked(self): - """Обработчик кнопки 'Остановить' с подтверждением""" + def _on_stop(self): if self.current_count < self.target_count and self.current_count > 0: - reply = QMessageBox.question(self, "Прервать съёмку?", - f"Вы не закончили съёмку (снято {self.current_count} из {self.target_count} кадров).\n" - f"Вы действительно хотите прервать?", - QMessageBox.Yes | QMessageBox.No) - if reply == QMessageBox.Yes: + reply = messagebox.askyesno("Stop Capture", + f"You haven't finished capture ({self.current_count} of {self.target_count} frames).\n" + f"Are you sure you want to stop?", + parent=self) + if reply: self._stop_capture() - self.progress_group.setVisible(False) elif self.current_count == 0: - reply = QMessageBox.question(self, "Прервать съёмку?", - "Съёмка ещё не начата. Вы действительно хотите выйти?", - QMessageBox.Yes | QMessageBox.No) - if reply == QMessageBox.Yes: + reply = messagebox.askyesno("Stop Capture", + "Capture hasn't started yet. Are you sure?", + parent=self) + if reply: self._stop_capture() - self.progress_group.setVisible(False) else: self._stop_capture() - self.progress_group.setVisible(False) - - def _get_watch_folder(self) -> Optional[Path]: - """Возвращает папку наблюдения из главного окна""" - print("Поиск папки наблюдения...") - - parent = self.parent() - print(f"Родительское окно: {parent}") - - while parent and not hasattr(parent, 'folder_entry'): - parent = parent.parent() - print(f"Поднимаемся выше: {parent}") - - if parent and hasattr(parent, 'folder_entry'): - watch_folder = parent.folder_entry.text() - print(f"Нашли folder_entry, значение: {watch_folder}") - if watch_folder: - path = Path(watch_folder) - print(f"Папка наблюдения: {path}") - return path - - print("Не удалось найти папку наблюдения в родительском окне") - - folder = QFileDialog.getExistingDirectory(self, "Выберите папку наблюдения (куда камера сохраняет файлы)") - if folder: - print(f"Пользователь выбрал: {folder}") - return Path(folder) + def _get_watch_folder(self): + # Traverse to find main window's folder_entry + parent = self.parent + while parent: + if hasattr(parent, 'folder_entry'): + watch_folder = parent.folder_entry.get() + if watch_folder and watch_folder != "Select watch folder...": + return Path(watch_folder) + parent = parent.master if hasattr(parent, 'master') else parent.parent if hasattr(parent, 'parent') else None return None def _add_custom_iso(self): - custom_iso, ok = QInputDialog.getInt(self, "Свой ISO", - "Введите значение ISO:", 800, 100, 12800) - if ok and custom_iso: - iso_str = str(custom_iso) - if self.iso_combo.findText(iso_str) == -1: - self.iso_combo.addItem(iso_str) - self.iso_combo.setCurrentText(iso_str) + dialog = tk.Toplevel(self) + dialog.title("Custom ISO") + dialog.geometry("300x100") + dialog.transient(self) + dialog.grab_set() + + ttk.Label(dialog, text="Enter ISO value:").pack(pady=10) + entry = ttk.Entry(dialog) + entry.pack(pady=5) + entry.focus() + + def save(): + try: + value = int(entry.get()) + if 100 <= value <= 12800: + iso_str = str(value) + values = list(self.iso_combo['values']) + if iso_str not in values: + values.append(iso_str) + values.sort(key=int) + self.iso_combo['values'] = values + self.iso_combo.set(iso_str) + dialog.destroy() + else: + messagebox.showerror("Error", "ISO must be between 100 and 12800", parent=dialog) + except ValueError: + messagebox.showerror("Error", "Please enter a valid number", parent=dialog) + + ttk.Button(dialog, text="OK", command=save).pack(pady=10) + dialog.bind('', lambda e: save()) def _add_custom_exposure(self): - custom_exp, ok = QInputDialog.getInt(self, "Своя выдержка", - "Введите выдержку (секунд):", 120, 1, 3600) - if ok and custom_exp: - exp_str = str(custom_exp) - if self.exposure_combo.findText(exp_str) == -1: - self.exposure_combo.addItem(exp_str) - self.exposure_combo.setCurrentText(exp_str) + dialog = tk.Toplevel(self) + dialog.title("Custom Exposure") + dialog.geometry("300x100") + dialog.transient(self) + dialog.grab_set() + + ttk.Label(dialog, text="Enter exposure (seconds):").pack(pady=10) + entry = ttk.Entry(dialog) + entry.pack(pady=5) + entry.focus() + + def save(): + try: + value = int(entry.get()) + if 1 <= value <= 3600: + exp_str = str(value) + values = list(self.exposure_combo['values']) + if exp_str not in values: + values.append(exp_str) + values.sort(key=int) + self.exposure_combo['values'] = values + self.exposure_combo.set(exp_str) + dialog.destroy() + else: + messagebox.showerror("Error", "Exposure must be between 1 and 3600 seconds", parent=dialog) + except ValueError: + messagebox.showerror("Error", "Please enter a valid number", parent=dialog) + + ttk.Button(dialog, text="OK", command=save).pack(pady=10) + dialog.bind('', lambda e: save()) def _show_help(self): if self.cal_type == 'bias': help_text = ( - "Что такое BIAS?\n\n" - "Bias (кадры смещения) — это снимки с закрытой крышкой\n" - "на минимально возможной выдержке.\n\n" - "Зачем нужны:\n" - "• Убирают read noise (шум считывания)\n" - "• Корректируют смещение чёрного уровня\n\n" - "Сколько снимать:\n" - "• 50 кадров для хорошего усреднения\n" - "• Можно использовать весь месяц\n\n" - "Когда снимать:\n" - "• Дома в любое время\n" - "• Температура не важна" + "What are BIAS frames?\n\n" + "Bias frames are shots with the lens cap closed\n" + "at the shortest possible shutter speed.\n\n" + "Why they are needed:\n" + "• Remove read noise\n" + "• Correct black level offset\n\n" + "How many to take:\n" + "• 50 frames for good averaging\n" + "• Can be used for a whole month\n\n" + "When to take:\n" + "• At home anytime\n" + "• Temperature doesn't matter" ) elif self.cal_type == 'dark': help_text = ( - "Что такое DARK?\n\n" - "Dark (тёмные кадры) — это снимки с закрытой крышкой\n" - "с ТЕМИ ЖЕ параметрами ISO и выдержки, что и световые кадры.\n\n" - "Зачем нужны:\n" - "• Убирают тепловой шум матрицы\n" - "• Убирают горячие пиксели\n\n" - "Сколько снимать:\n" - "• 20-30 кадров для хорошего результата\n\n" - "⚠️ ВАЖНО про температуру:\n" - "• Снимайте ПОСЛЕ сессии на месте!\n" - "• Камера должна быть при той же температуре\n" - "• Разница >5°C делает кадры бесполезными!" + "What are DARK frames?\n\n" + "Dark frames are shots with the lens cap closed\n" + "with the SAME ISO and exposure as light frames.\n\n" + "Why they are needed:\n" + "• Remove thermal noise\n" + "• Remove hot pixels\n\n" + "IMPORTANT about temperature:\n" + "• Take AFTER the session on site!\n" + "• Camera must be at the same temperature\n" + "• Difference >5C makes darks useless!" ) else: help_text = ( - "Что такое FLAT?\n\n" - "Flat (плоские поля) — это снимки равномерно освещённой\n" - "поверхности с ТЕМИ ЖЕ фокусом и зумом.\n\n" - "Зачем нужны:\n" - "• Убирают виньетирование объектива\n" - "• Убирают пыль на матрице и оптике\n\n" - "Как снимать:\n" - "1. LED-планшет (лучший вариант)\n" - "2. Рассвет/закат, камера в зенит\n" - "3. Белая футболка на объектив\n\n" - "Сколько снимать:\n" - "• 30 кадров для хорошего усреднения\n\n" - "⚠️ ВАЖНО:\n" - "• НЕ меняйте фокус!\n" - "• НЕ меняйте зум!\n" - "• Снимайте в конце сессии" + "What are FLAT frames?\n\n" + "Flat frames are shots of an evenly lit surface\n" + "with the SAME focus and zoom.\n\n" + "Why they are needed:\n" + "• Remove lens vignetting\n" + "• Remove dust on sensor/lens\n\n" + "How to shoot:\n" + "1. LED panel (best option)\n" + "2. Dawn/dusk sky, point at zenith\n" + "3. White T-shirt over lens\n\n" + "IMPORTANT:\n" + "• DON'T change focus!\n" + "• DON'T change zoom!\n" + "• Take at the end of the session" ) - QMessageBox.information(self, "Справка", help_text) + messagebox.showinfo("Help", help_text, parent=self) - def closeEvent(self, event): - if self.is_capturing: - reply = QMessageBox.question(self, "Прервать съёмку?", - "Съёмка активна. Вы действительно хотите закрыть окно?", - QMessageBox.Yes | QMessageBox.No) - if reply == QMessageBox.Yes: - self._stop_capture() - event.accept() - else: - event.ignore() - else: - event.accept() \ No newline at end of file + def _center_window(self): + self.update_idletasks() + x = self.parent.winfo_x() + (self.parent.winfo_width() // 2) - (self.winfo_width() // 2) + y = self.parent.winfo_y() + (self.parent.winfo_height() // 2) - (self.winfo_height() // 2) + self.geometry(f'+{x}+{y}') \ No newline at end of file diff --git a/ui/dialogs/celestial_dialog.py b/ui/dialogs/celestial_dialog.py index 4798cf5..d9f120f 100644 --- a/ui/dialogs/celestial_dialog.py +++ b/ui/dialogs/celestial_dialog.py @@ -1,160 +1,153 @@ """ -CelestialDialog - диалог управления небесными телами -Аналог CelestialBodiesDialogController из JavaFX версии +CelestialDialog - диалог управления небесными телами (tkinter) """ -from PySide6.QtWidgets import ( - QDialog, QVBoxLayout, QHBoxLayout, QLabel, QListWidget, - QPushButton, QLineEdit, QInputDialog, QMessageBox -) -from PySide6.QtCore import Qt -from PySide6.QtGui import QFont -from services.config_service import ConfigService +import tkinter as tk +from tkinter import ttk, messagebox -class CelestialDialog(QDialog): +class CelestialDialog(tk.Toplevel): """Диалог для управления списком небесных тел""" - def __init__(self, parent, config_service: ConfigService): + def __init__(self, parent, config_service): super().__init__(parent) - + self.parent = parent self.config_service = config_service - self.setWindowTitle("Небесные тела") - self.setMinimumSize(400, 500) - self.resize(450, 550) - - # Загружаем текущий список self.celestial_bodies = self.config_service.get_celestial_bodies() + self.selected_body = None + + self.title("Celestial Bodies") + self.geometry("500x550") + self.minsize(450, 500) + self.transient(parent) + self.grab_set() self._create_ui() - self._update_list() + self._center_window() def _create_ui(self): - """Создаёт интерфейс диалога""" - layout = QVBoxLayout(self) - layout.setSpacing(15) - layout.setContentsMargins(20, 20, 20, 20) + # Main frame + main_frame = ttk.Frame(self, padding="20") + main_frame.pack(fill='both', expand=True) - # Заголовок - title_label = QLabel("Управление небесными телами") - title_font = QFont() - title_font.setPointSize(16) - title_font.setBold(True) - title_label.setFont(title_font) - title_label.setAlignment(Qt.AlignCenter) - layout.addWidget(title_label) + # Title + ttk.Label(main_frame, text="Celestial Bodies", font=('Segoe UI', 14, 'bold')).pack(pady=(0, 10)) + ttk.Label(main_frame, text="List of observation targets", font=('Segoe UI', 10)).pack(pady=(0, 15)) - # Подпись - subtitle_label = QLabel("Список объектов для наблюдения") - subtitle_font = QFont() - subtitle_font.setPointSize(11) - subtitle_font.setBold(True) - subtitle_label.setFont(subtitle_font) - layout.addWidget(subtitle_label) + # Listbox with scrollbar + list_frame = ttk.Frame(main_frame) + list_frame.pack(fill='both', expand=True, pady=(0, 10)) - # Список небесных тел - self.bodies_list = QListWidget() - self.bodies_list.itemClicked.connect(lambda item: self._select_body(item.text())) - layout.addWidget(self.bodies_list) + scrollbar = ttk.Scrollbar(list_frame) + scrollbar.pack(side='right', fill='y') - # Поле для добавления нового - add_layout = QHBoxLayout() + self.bodies_listbox = tk.Listbox(list_frame, yscrollcommand=scrollbar.set, height=15, + bg='#2d2d2d', fg='#e0e0e0', + selectbackground='#4CAF50', selectforeground='white', + font=('Segoe UI', 10)) + self.bodies_listbox.pack(fill='both', expand=True) + scrollbar.config(command=self.bodies_listbox.yview) - self.new_body_entry = QLineEdit() - self.new_body_entry.setPlaceholderText("Название объекта (например: M31, NGC 224)") - self.new_body_entry.returnPressed.connect(self._add_celestial_body) - add_layout.addWidget(self.new_body_entry) - - add_btn = QPushButton("➕ Добавить") - add_btn.clicked.connect(self._add_celestial_body) - add_layout.addWidget(add_btn) - - layout.addLayout(add_layout) - - # Кнопки удаления и редактирования - buttons_layout = QHBoxLayout() - - self.remove_btn = QPushButton("❌ Удалить выбранный") - self.remove_btn.setEnabled(False) - self.remove_btn.clicked.connect(self._remove_celestial_body) - buttons_layout.addWidget(self.remove_btn) - - self.edit_btn = QPushButton("✏ Редактировать") - self.edit_btn.setEnabled(False) - self.edit_btn.clicked.connect(self._edit_celestial_body) - buttons_layout.addWidget(self.edit_btn) - - layout.addLayout(buttons_layout) - - # Кнопка закрытия - close_btn = QPushButton("Закрыть") - close_btn.clicked.connect(self.accept) - close_layout = QHBoxLayout() - close_layout.addStretch() - close_layout.addWidget(close_btn) - layout.addLayout(close_layout) - - def _update_list(self): - """Обновляет отображение списка небесных тел""" - self.bodies_list.clear() for body in self.celestial_bodies: - self.bodies_list.addItem(body) - self._selected_body = None - self.remove_btn.setEnabled(False) - self.edit_btn.setEnabled(False) + self.bodies_listbox.insert('end', body) - def _select_body(self, body: str): - """Выделяет объект в списке""" - self._selected_body = body - self.remove_btn.setEnabled(True) - self.edit_btn.setEnabled(True) + self.bodies_listbox.bind('<>', self._on_body_select) + + # Add new body + add_frame = ttk.Frame(main_frame) + add_frame.pack(fill='x', pady=(0, 10)) + + self.new_body_entry = ttk.Entry(add_frame) + self.new_body_entry.pack(side='left', fill='x', expand=True, padx=(0, 10)) + ttk.Button(add_frame, text="Add", command=self._add_celestial_body).pack(side='right') + + # Buttons + btn_frame = ttk.Frame(main_frame) + btn_frame.pack(pady=(0, 10)) + + self.remove_btn = ttk.Button(btn_frame, text="Remove Selected", command=self._remove_celestial_body, state='disabled') + self.remove_btn.pack(side='left', padx=5) + + self.edit_btn = ttk.Button(btn_frame, text="Edit Selected", command=self._edit_celestial_body, state='disabled') + self.edit_btn.pack(side='left', padx=5) + + # Close button + ttk.Button(main_frame, text="Close", command=self.destroy).pack(pady=10) + + def _center_window(self): + self.update_idletasks() + x = self.parent.winfo_x() + (self.parent.winfo_width() // 2) - (self.winfo_width() // 2) + y = self.parent.winfo_y() + (self.parent.winfo_height() // 2) - (self.winfo_height() // 2) + self.geometry(f'+{x}+{y}') + + def _on_body_select(self, event): + selection = self.bodies_listbox.curselection() + if selection: + self.selected_body = self.bodies_listbox.get(selection[0]) + self.remove_btn.config(state='normal') + self.edit_btn.config(state='normal') def _add_celestial_body(self): - """Добавляет новое небесное тело""" - new_body = self.new_body_entry.text() - if not new_body or not new_body.strip(): - QMessageBox.warning(self, "Ошибка", "Введите название объекта") + new_body = self.new_body_entry.get().strip() + if not new_body: + messagebox.showwarning("Warning", "Please enter object name!", parent=self) return - new_name = new_body.strip() - if new_name in self.celestial_bodies: - QMessageBox.warning(self, "Ошибка", f"Объект '{new_name}' уже существует!") + if new_body in self.celestial_bodies: + messagebox.showwarning("Warning", f"Object '{new_body}' already exists!", parent=self) return - self.celestial_bodies.append(new_name) - self.config_service.add_celestial_body(new_name) - self._update_list() - self.new_body_entry.clear() - QMessageBox.information(self, "Успех", f"Объект '{new_name}' добавлен") + self.celestial_bodies.append(new_body) + self.config_service.add_celestial_body(new_body) + self.bodies_listbox.insert('end', new_body) + self.new_body_entry.delete(0, 'end') + messagebox.showinfo("Success", f"Object '{new_body}' added", parent=self) def _remove_celestial_body(self): - """Удаляет выбранное небесное тело""" - if hasattr(self, '_selected_body') and self._selected_body: - reply = QMessageBox.question(self, "Подтверждение", - f"Удалить объект '{self._selected_body}'?", - QMessageBox.Yes | QMessageBox.No) - if reply == QMessageBox.Yes: - self.celestial_bodies.remove(self._selected_body) - self.config_service.remove_celestial_body(self._selected_body) - self._update_list() - QMessageBox.information(self, "Успех", f"Объект '{self._selected_body}' удалён") + if self.selected_body: + reply = messagebox.askyesno("Remove Object", f"Remove '{self.selected_body}'?", parent=self) + if reply: + self.celestial_bodies.remove(self.selected_body) + self.config_service.remove_celestial_body(self.selected_body) + self.bodies_listbox.delete(0, 'end') + for body in self.celestial_bodies: + self.bodies_listbox.insert('end', body) + self.selected_body = None + self.remove_btn.config(state='disabled') + self.edit_btn.config(state='disabled') def _edit_celestial_body(self): - """Редактирует выбранное небесное тело""" - if hasattr(self, '_selected_body') and self._selected_body: - new_name, ok = QInputDialog.getText(self, "Редактировать", - f"Изменить '{self._selected_body}' на:", - text=self._selected_body) - if ok and new_name and new_name.strip(): - new_name = new_name.strip() - if new_name != self._selected_body: + if self.selected_body: + dialog = tk.Toplevel(self) + dialog.title("Edit Celestial Body") + dialog.geometry("350x120") + dialog.transient(self) + dialog.grab_set() + + ttk.Label(dialog, text="New name:", font=('Segoe UI', 10)).pack(pady=15) + entry = ttk.Entry(dialog, width=40) + entry.insert(0, self.selected_body) + entry.pack(pady=5) + entry.focus() + + def save(): + new_name = entry.get().strip() + if new_name and new_name != self.selected_body: if new_name in self.celestial_bodies: - QMessageBox.warning(self, "Ошибка", f"Объект '{new_name}' уже существует!") + messagebox.showerror("Error", f"Object '{new_name}' already exists!", parent=dialog) return - idx = self.celestial_bodies.index(self._selected_body) - old_name = self.celestial_bodies[idx] + idx = self.celestial_bodies.index(self.selected_body) self.celestial_bodies[idx] = new_name - self.config_service.update_celestial_body(old_name, new_name) - self._update_list() - QMessageBox.information(self, "Успех", f"Объект переименован в '{new_name}'") \ No newline at end of file + self.config_service.update_celestial_body(self.selected_body, new_name) + self.bodies_listbox.delete(0, 'end') + for body in self.celestial_bodies: + self.bodies_listbox.insert('end', body) + dialog.destroy() + elif new_name == self.selected_body: + dialog.destroy() + else: + messagebox.showwarning("Warning", "Please enter a name!", parent=dialog) + + ttk.Button(dialog, text="Save", command=save).pack(pady=10) + dialog.bind('', lambda e: save()) \ No newline at end of file diff --git a/ui/dialogs/equipment_dialog.py b/ui/dialogs/equipment_dialog.py index 0508566..655cdd3 100644 --- a/ui/dialogs/equipment_dialog.py +++ b/ui/dialogs/equipment_dialog.py @@ -1,361 +1,469 @@ """ -EquipmentDialog - диалог управления оборудованием -Камеры, объективы и телескопы +EquipmentDialog - диалог управления оборудованием (tkinter) """ -from PySide6.QtWidgets import ( - QDialog, QVBoxLayout, QHBoxLayout, QLabel, QListWidget, - QPushButton, QInputDialog, QMessageBox, QWidget, QTabWidget, - QFormLayout, QDoubleSpinBox, QSpinBox, QLineEdit -) -from PySide6.QtCore import Qt -from PySide6.QtGui import QFont -from services.config_service import ConfigService +import tkinter as tk +from tkinter import ttk, messagebox +from threading import Thread -class EquipmentDialog(QDialog): +class EquipmentDialog(tk.Toplevel): """Диалог для управления оборудованием""" - def __init__(self, parent, config_service: ConfigService): + def __init__(self, parent, config_service): super().__init__(parent) - + self.parent = parent self.config_service = config_service - self.setWindowTitle("Управление оборудованием") - self.setMinimumSize(700, 500) - self.resize(800, 550) - # Загружаем данные self.cameras = self.config_service.get_cameras() self.lenses = self.config_service.get_lenses() self.telescopes = self.config_service.get_telescopes() + self.selected_camera = None + self.selected_lens = None + self.selected_telescope = None + + self.title("Manage Equipment") + self.geometry("750x500") + self.minsize(700, 450) + self.transient(parent) + self.grab_set() + self._create_ui() - self._update_cameras_list() - self._update_lenses_list() - self._update_telescopes_list() + self._center_window() def _create_ui(self): - layout = QVBoxLayout(self) - layout.setSpacing(15) - layout.setContentsMargins(20, 20, 20, 20) + # Notebook for tabs + notebook = ttk.Notebook(self) + notebook.pack(fill='both', expand=True, padx=10, pady=10) - # Заголовок - title_label = QLabel("Управление оборудованием") - title_font = QFont() - title_font.setPointSize(16) - title_font.setBold(True) - title_label.setFont(title_font) - title_label.setAlignment(Qt.AlignCenter) - layout.addWidget(title_label) + # Tab 1: Cameras + cameras_frame = ttk.Frame(notebook) + notebook.add(cameras_frame, text="Cameras") + self._create_cameras_tab(cameras_frame) - # Используем QTabWidget для трёх вкладок - tab_widget = QTabWidget() + # Tab 2: Lenses + lenses_frame = ttk.Frame(notebook) + notebook.add(lenses_frame, text="Lenses") + self._create_lenses_tab(lenses_frame) - # Вкладка: Камеры - cameras_tab = self._create_cameras_tab() - tab_widget.addTab(cameras_tab, "📷 Камеры") + # Tab 3: Telescopes + telescopes_frame = ttk.Frame(notebook) + notebook.add(telescopes_frame, text="Telescopes") + self._create_telescopes_tab(telescopes_frame) - # Вкладка: Объективы - lenses_tab = self._create_lenses_tab() - tab_widget.addTab(lenses_tab, "🔭 Объективы") + # Close button + btn_frame = ttk.Frame(self) + btn_frame.pack(pady=10) + ttk.Button(btn_frame, text="Close", command=self.destroy).pack() - # Вкладка: Телескопы - telescopes_tab = self._create_telescopes_tab() - tab_widget.addTab(telescopes_tab, "🪐 Телескопы") + def _center_window(self): + self.update_idletasks() + x = self.parent.winfo_x() + (self.parent.winfo_width() // 2) - (self.winfo_width() // 2) + y = self.parent.winfo_y() + (self.parent.winfo_height() // 2) - (self.winfo_height() // 2) + self.geometry(f'+{x}+{y}') - layout.addWidget(tab_widget) + def _create_cameras_tab(self, parent): + # Listbox + list_frame = ttk.Frame(parent) + list_frame.pack(fill='both', expand=True, padx=10, pady=10) - # Кнопка закрытия - close_btn = QPushButton("Закрыть") - close_btn.clicked.connect(self.accept) - close_layout = QHBoxLayout() - close_layout.addStretch() - close_layout.addWidget(close_btn) - layout.addLayout(close_layout) + scrollbar = ttk.Scrollbar(list_frame) + scrollbar.pack(side='right', fill='y') - def _create_cameras_tab(self) -> QWidget: - """Создаёт вкладку с камерами""" - tab = QWidget() - layout = QVBoxLayout(tab) + self.cameras_listbox = tk.Listbox(list_frame, yscrollcommand=scrollbar.set, + bg='#2d2d2d', fg='#e0e0e0', + selectbackground='#4CAF50', selectforeground='white', + font=('Segoe UI', 10)) + self.cameras_listbox.pack(fill='both', expand=True) + scrollbar.config(command=self.cameras_listbox.yview) - # Список камер - self.cameras_list = QListWidget() - self.cameras_list.itemClicked.connect(lambda item: self._select_camera(item.text())) - layout.addWidget(self.cameras_list) - - # Кнопки - buttons_layout = QHBoxLayout() - - add_btn = QPushButton("➕ Добавить камеру") - add_btn.clicked.connect(self._add_camera) - buttons_layout.addWidget(add_btn) - - self.remove_camera_btn = QPushButton("❌ Удалить") - self.remove_camera_btn.setEnabled(False) - self.remove_camera_btn.clicked.connect(self._remove_camera) - buttons_layout.addWidget(self.remove_camera_btn) - - layout.addLayout(buttons_layout) - - return tab - - def _create_lenses_tab(self) -> QWidget: - """Создаёт вкладку с объективами""" - tab = QWidget() - layout = QVBoxLayout(tab) - - # Список объективов - self.lenses_list = QListWidget() - self.lenses_list.itemClicked.connect(lambda item: self._select_lens(item.text())) - layout.addWidget(self.lenses_list) - - # Кнопки - buttons_layout = QHBoxLayout() - - add_btn = QPushButton("➕ Добавить объектив") - add_btn.clicked.connect(self._add_lens) - buttons_layout.addWidget(add_btn) - - self.remove_lens_btn = QPushButton("❌ Удалить") - self.remove_lens_btn.setEnabled(False) - self.remove_lens_btn.clicked.connect(self._remove_lens) - buttons_layout.addWidget(self.remove_lens_btn) - - edit_btn = QPushButton("✏ Редактировать") - edit_btn.clicked.connect(self._edit_lens) - buttons_layout.addWidget(edit_btn) - - layout.addLayout(buttons_layout) - - return tab - - def _create_telescopes_tab(self) -> QWidget: - """Создаёт вкладку с телескопами""" - tab = QWidget() - layout = QVBoxLayout(tab) - - # Список телескопов - self.telescopes_list = QListWidget() - self.telescopes_list.itemClicked.connect(lambda item: self._select_telescope(item.text())) - layout.addWidget(self.telescopes_list) - - # Кнопки - buttons_layout = QHBoxLayout() - - add_btn = QPushButton("➕ Добавить телескоп") - add_btn.clicked.connect(self._add_telescope) - buttons_layout.addWidget(add_btn) - - self.remove_telescope_btn = QPushButton("❌ Удалить") - self.remove_telescope_btn.setEnabled(False) - self.remove_telescope_btn.clicked.connect(self._remove_telescope) - buttons_layout.addWidget(self.remove_telescope_btn) - - edit_btn = QPushButton("✏ Редактировать") - edit_btn.clicked.connect(self._edit_telescope) - buttons_layout.addWidget(edit_btn) - - layout.addLayout(buttons_layout) - - return tab - - # ===== Методы для камер ===== - - def _update_cameras_list(self): - self.cameras_list.clear() for camera in self.cameras: - self.cameras_list.addItem(camera) - self._selected_camera = None - self.remove_camera_btn.setEnabled(False) + self.cameras_listbox.insert('end', camera) - def _select_camera(self, camera: str): - self._selected_camera = camera - self.remove_camera_btn.setEnabled(True) - self.lenses_list.clearSelection() - self.telescopes_list.clearSelection() - self._selected_lens = None - self._selected_telescope = None - self.remove_lens_btn.setEnabled(False) - self.remove_telescope_btn.setEnabled(False) + self.cameras_listbox.bind('<>', self._on_camera_select) + + # Buttons + btn_frame = ttk.Frame(parent) + btn_frame.pack(pady=10) + + ttk.Button(btn_frame, text="Add Camera", command=self._add_camera).pack(side='left', padx=5) + self.remove_camera_btn = ttk.Button(btn_frame, text="Remove", command=self._remove_camera, state='disabled') + self.remove_camera_btn.pack(side='left', padx=5) + self.edit_camera_btn = ttk.Button(btn_frame, text="Edit", command=self._edit_camera, state='disabled') + self.edit_camera_btn.pack(side='left', padx=5) + + def _on_camera_select(self, event): + selection = self.cameras_listbox.curselection() + if selection: + self.selected_camera = self.cameras_listbox.get(selection[0]) + self.remove_camera_btn.config(state='normal') + self.edit_camera_btn.config(state='normal') def _add_camera(self): - name, ok = QInputDialog.getText(self, "Добавить камеру", "Введите название камеры:") - if ok and name and name.strip(): - new_name = name.strip() - if new_name in self.cameras: - QMessageBox.warning(self, "Ошибка", "Такая камера уже существует!") - return - self.cameras.append(new_name) - self.config_service.add_camera(new_name) - self._update_cameras_list() - QMessageBox.information(self, "Успех", f"Камера '{new_name}' добавлена") + dialog = tk.Toplevel(self) + dialog.title("Add Camera") + dialog.geometry("350x120") + dialog.transient(self) + dialog.grab_set() + + ttk.Label(dialog, text="Camera name:", font=('Segoe UI', 10)).pack(pady=15) + entry = ttk.Entry(dialog, width=40) + entry.pack(pady=5) + entry.focus() + + def save(): + new_camera = entry.get().strip() + if new_camera: + if new_camera in self.cameras: + messagebox.showerror("Error", "Camera already exists!", parent=dialog) + return + self.cameras.append(new_camera) + self.config_service.add_camera(new_camera) + self.cameras_listbox.insert('end', new_camera) + dialog.destroy() + else: + messagebox.showwarning("Warning", "Please enter camera name!", parent=dialog) + + ttk.Button(dialog, text="OK", command=save).pack(pady=10) + dialog.bind('', lambda e: save()) def _remove_camera(self): - if hasattr(self, '_selected_camera') and self._selected_camera: - reply = QMessageBox.question(self, "Подтверждение", - f"Удалить камеру '{self._selected_camera}'?", - QMessageBox.Yes | QMessageBox.No) - if reply == QMessageBox.Yes: - self.cameras.remove(self._selected_camera) - self.config_service.remove_camera(self._selected_camera) - self._update_cameras_list() + if self.selected_camera: + reply = messagebox.askyesno("Remove Camera", f"Remove '{self.selected_camera}'?", parent=self) + if reply: + self.cameras.remove(self.selected_camera) + self.config_service.remove_camera(self.selected_camera) + self.cameras_listbox.delete(0, 'end') + for camera in self.cameras: + self.cameras_listbox.insert('end', camera) + self.selected_camera = None + self.remove_camera_btn.config(state='disabled') + self.edit_camera_btn.config(state='disabled') - # ===== Методы для объективов ===== + def _edit_camera(self): + if self.selected_camera: + dialog = tk.Toplevel(self) + dialog.title("Edit Camera") + dialog.geometry("350x120") + dialog.transient(self) + dialog.grab_set() + + ttk.Label(dialog, text="New camera name:", font=('Segoe UI', 10)).pack(pady=15) + entry = ttk.Entry(dialog, width=40) + entry.insert(0, self.selected_camera) + entry.pack(pady=5) + entry.focus() + + def save(): + new_name = entry.get().strip() + if new_name and new_name != self.selected_camera: + if new_name in self.cameras: + messagebox.showerror("Error", "Camera already exists!", parent=dialog) + return + idx = self.cameras.index(self.selected_camera) + self.cameras[idx] = new_name + self.config_service.remove_camera(self.selected_camera) + self.config_service.add_camera(new_name) + self.cameras_listbox.delete(0, 'end') + for camera in self.cameras: + self.cameras_listbox.insert('end', camera) + self.selected_camera = new_name + dialog.destroy() + elif new_name == self.selected_camera: + dialog.destroy() + else: + messagebox.showwarning("Warning", "Please enter a name!", parent=dialog) + + ttk.Button(dialog, text="Save", command=save).pack(pady=10) + dialog.bind('', lambda e: save()) + + def _create_lenses_tab(self, parent): + # Listbox + list_frame = ttk.Frame(parent) + list_frame.pack(fill='both', expand=True, padx=10, pady=10) + + scrollbar = ttk.Scrollbar(list_frame) + scrollbar.pack(side='right', fill='y') + + self.lenses_listbox = tk.Listbox(list_frame, yscrollcommand=scrollbar.set, + bg='#2d2d2d', fg='#e0e0e0', + selectbackground='#4CAF50', selectforeground='white', + font=('Segoe UI', 10)) + self.lenses_listbox.pack(fill='both', expand=True) + scrollbar.config(command=self.lenses_listbox.yview) - def _update_lenses_list(self): - self.lenses_list.clear() for lens in self.lenses: - self.lenses_list.addItem(lens) - self._selected_lens = None - self.remove_lens_btn.setEnabled(False) + self.lenses_listbox.insert('end', lens) - def _select_lens(self, lens: str): - self._selected_lens = lens - self.remove_lens_btn.setEnabled(True) - self.cameras_list.clearSelection() - self.telescopes_list.clearSelection() - self._selected_camera = None - self._selected_telescope = None - self.remove_camera_btn.setEnabled(False) - self.remove_telescope_btn.setEnabled(False) + self.lenses_listbox.bind('<>', self._on_lens_select) + + # Buttons + btn_frame = ttk.Frame(parent) + btn_frame.pack(pady=10) + + ttk.Button(btn_frame, text="Add Lens", command=self._add_lens).pack(side='left', padx=5) + self.remove_lens_btn = ttk.Button(btn_frame, text="Remove", command=self._remove_lens, state='disabled') + self.remove_lens_btn.pack(side='left', padx=5) + self.edit_lens_btn = ttk.Button(btn_frame, text="Edit", command=self._edit_lens, state='disabled') + self.edit_lens_btn.pack(side='left', padx=5) + + def _on_lens_select(self, event): + selection = self.lenses_listbox.curselection() + if selection: + self.selected_lens = self.lenses_listbox.get(selection[0]) + self.remove_lens_btn.config(state='normal') + self.edit_lens_btn.config(state='normal') def _add_lens(self): - name, ok = QInputDialog.getText(self, "Добавить объектив", "Введите название объектива:") - if ok and name and name.strip(): - new_name = name.strip() - if new_name in self.lenses: - QMessageBox.warning(self, "Ошибка", "Такой объектив уже существует!") - return - self.lenses.append(new_name) - self.config_service.add_lens(new_name) - self._update_lenses_list() - QMessageBox.information(self, "Успех", f"Объектив '{new_name}' добавлен") + dialog = tk.Toplevel(self) + dialog.title("Add Lens") + dialog.geometry("350x120") + dialog.transient(self) + dialog.grab_set() + + ttk.Label(dialog, text="Lens name:", font=('Segoe UI', 10)).pack(pady=15) + entry = ttk.Entry(dialog, width=40) + entry.pack(pady=5) + entry.focus() + + def save(): + new_lens = entry.get().strip() + if new_lens: + if new_lens in self.lenses: + messagebox.showerror("Error", "Lens already exists!", parent=dialog) + return + self.lenses.append(new_lens) + self.config_service.add_lens(new_lens) + self.lenses_listbox.insert('end', new_lens) + dialog.destroy() + else: + messagebox.showwarning("Warning", "Please enter lens name!", parent=dialog) + + ttk.Button(dialog, text="OK", command=save).pack(pady=10) + dialog.bind('', lambda e: save()) def _remove_lens(self): - if hasattr(self, '_selected_lens') and self._selected_lens: - reply = QMessageBox.question(self, "Подтверждение", - f"Удалить объектив '{self._selected_lens}'?", - QMessageBox.Yes | QMessageBox.No) - if reply == QMessageBox.Yes: - self.lenses.remove(self._selected_lens) - self.config_service.remove_lens(self._selected_lens) - self._update_lenses_list() + if self.selected_lens: + reply = messagebox.askyesno("Remove Lens", f"Remove '{self.selected_lens}'?", parent=self) + if reply: + self.lenses.remove(self.selected_lens) + self.config_service.remove_lens(self.selected_lens) + self.lenses_listbox.delete(0, 'end') + for lens in self.lenses: + self.lenses_listbox.insert('end', lens) + self.selected_lens = None + self.remove_lens_btn.config(state='disabled') + self.edit_lens_btn.config(state='disabled') def _edit_lens(self): - if hasattr(self, '_selected_lens') and self._selected_lens: - new_name, ok = QInputDialog.getText(self, "Редактировать объектив", - f"Изменить '{self._selected_lens}' на:", - text=self._selected_lens) - if ok and new_name and new_name.strip(): - new_name = new_name.strip() - if new_name != self._selected_lens: + if self.selected_lens: + dialog = tk.Toplevel(self) + dialog.title("Edit Lens") + dialog.geometry("350x120") + dialog.transient(self) + dialog.grab_set() + + ttk.Label(dialog, text="New lens name:", font=('Segoe UI', 10)).pack(pady=15) + entry = ttk.Entry(dialog, width=40) + entry.insert(0, self.selected_lens) + entry.pack(pady=5) + entry.focus() + + def save(): + new_name = entry.get().strip() + if new_name and new_name != self.selected_lens: if new_name in self.lenses: - QMessageBox.warning(self, "Ошибка", "Такой объектив уже существует!") + messagebox.showerror("Error", "Lens already exists!", parent=dialog) return - idx = self.lenses.index(self._selected_lens) + idx = self.lenses.index(self.selected_lens) self.lenses[idx] = new_name - # Обновляем в конфиге (пока просто удаляем старый и добавляем новый) - self.config_service.remove_lens(self._selected_lens) + self.config_service.remove_lens(self.selected_lens) self.config_service.add_lens(new_name) - self._update_lenses_list() + self.lenses_listbox.delete(0, 'end') + for lens in self.lenses: + self.lenses_listbox.insert('end', lens) + self.selected_lens = new_name + dialog.destroy() + elif new_name == self.selected_lens: + dialog.destroy() + else: + messagebox.showwarning("Warning", "Please enter a name!", parent=dialog) - # ===== Методы для телескопов ===== + ttk.Button(dialog, text="Save", command=save).pack(pady=10) + dialog.bind('', lambda e: save()) + + def _create_telescopes_tab(self, parent): + # Listbox + list_frame = ttk.Frame(parent) + list_frame.pack(fill='both', expand=True, padx=10, pady=10) + + scrollbar = ttk.Scrollbar(list_frame) + scrollbar.pack(side='right', fill='y') + + self.telescopes_listbox = tk.Listbox(list_frame, yscrollcommand=scrollbar.set, + bg='#2d2d2d', fg='#e0e0e0', + selectbackground='#4CAF50', selectforeground='white', + font=('Segoe UI', 10)) + self.telescopes_listbox.pack(fill='both', expand=True) + scrollbar.config(command=self.telescopes_listbox.yview) - def _update_telescopes_list(self): - self.telescopes_list.clear() for telescope in self.telescopes: - self.telescopes_list.addItem(telescope) - self._selected_telescope = None - self.remove_telescope_btn.setEnabled(False) + self.telescopes_listbox.insert('end', telescope) - def _select_telescope(self, telescope: str): - self._selected_telescope = telescope - self.remove_telescope_btn.setEnabled(True) - self.cameras_list.clearSelection() - self.lenses_list.clearSelection() - self._selected_camera = None - self._selected_lens = None - self.remove_camera_btn.setEnabled(False) - self.remove_lens_btn.setEnabled(False) + self.telescopes_listbox.bind('<>', self._on_telescope_select) + + # Buttons + btn_frame = ttk.Frame(parent) + btn_frame.pack(pady=10) + + ttk.Button(btn_frame, text="Add Telescope", command=self._add_telescope).pack(side='left', padx=5) + self.remove_telescope_btn = ttk.Button(btn_frame, text="Remove", command=self._remove_telescope, state='disabled') + self.remove_telescope_btn.pack(side='left', padx=5) + self.edit_telescope_btn = ttk.Button(btn_frame, text="Edit", command=self._edit_telescope, state='disabled') + self.edit_telescope_btn.pack(side='left', padx=5) + + def _on_telescope_select(self, event): + selection = self.telescopes_listbox.curselection() + if selection: + self.selected_telescope = self.telescopes_listbox.get(selection[0]) + self.remove_telescope_btn.config(state='normal') + self.edit_telescope_btn.config(state='normal') def _add_telescope(self): - """Добавляет телескоп с указанием диафрагмы (фиксированной)""" - dialog = QDialog(self) - dialog.setWindowTitle("Добавить телескоп") - dialog.setMinimumWidth(400) + dialog = tk.Toplevel(self) + dialog.title("Add Telescope") + dialog.geometry("400x320") + dialog.transient(self) + dialog.grab_set() - layout = QVBoxLayout(dialog) + ttk.Label(dialog, text="Telescope name:").pack(pady=(15, 5)) + name_entry = ttk.Entry(dialog, width=40) + name_entry.pack() - form_layout = QFormLayout() + ttk.Label(dialog, text="Aperture (f/):").pack(pady=(10, 5)) + aperture_entry = ttk.Entry(dialog, width=20) + aperture_entry.insert(0, "5.0") + aperture_entry.pack() - name_edit = QLineEdit() - name_edit.setPlaceholderText("例如: Celestron 8\"") - form_layout.addRow("Название:", name_edit) + ttk.Label(dialog, text="Focal length (mm):").pack(pady=(10, 5)) + focal_entry = ttk.Entry(dialog, width=20) + focal_entry.insert(0, "1000") + focal_entry.pack() - aperture_spin = QDoubleSpinBox() - aperture_spin.setRange(0.5, 20.0) - aperture_spin.setSingleStep(0.1) - aperture_spin.setValue(5.0) - aperture_spin.setSuffix(" (f/)") - form_layout.addRow("Диафрагма (f/):", aperture_spin) + ttk.Label(dialog, text="Diameter (mm):").pack(pady=(10, 5)) + diameter_entry = ttk.Entry(dialog, width=20) + diameter_entry.insert(0, "200") + diameter_entry.pack() - focal_spin = QSpinBox() - focal_spin.setRange(100, 5000) - focal_spin.setSingleStep(50) - focal_spin.setSuffix(" мм") - form_layout.addRow("Фокусное расстояние:", focal_spin) + def save(): + name = name_entry.get().strip() + if not name: + messagebox.showerror("Error", "Please enter telescope name!", parent=dialog) + return - diameter_spin = QSpinBox() - diameter_spin.setRange(50, 500) - diameter_spin.setSingleStep(10) - diameter_spin.setSuffix(" мм") - form_layout.addRow("Диаметр объектива:", diameter_spin) + try: + aperture = float(aperture_entry.get()) + focal = int(focal_entry.get()) + diameter = int(diameter_entry.get()) + except ValueError: + messagebox.showerror("Error", "Invalid numeric values!", parent=dialog) + return - layout.addLayout(form_layout) + telescope_info = f"{name} (f/{aperture}, F={focal}mm, D={diameter}mm)" + if telescope_info in self.telescopes: + messagebox.showerror("Error", "Telescope already exists!", parent=dialog) + return - buttons_layout = QHBoxLayout() - ok_btn = QPushButton("OK") - cancel_btn = QPushButton("Отмена") - buttons_layout.addWidget(ok_btn) - buttons_layout.addWidget(cancel_btn) - layout.addLayout(buttons_layout) + self.telescopes.append(telescope_info) + self.config_service.add_telescope(telescope_info) + self.telescopes_listbox.insert('end', telescope_info) + dialog.destroy() - ok_btn.clicked.connect(dialog.accept) - cancel_btn.clicked.connect(dialog.reject) - - if dialog.exec(): - name = name_edit.text().strip() - if name: - telescope_info = f"{name} (f/{aperture_spin.value()}, F={focal_spin.value()}mm, D={diameter_spin.value()}mm)" - if telescope_info not in self.telescopes: - self.telescopes.append(telescope_info) - self.config_service.add_telescope(telescope_info) - self._update_telescopes_list() - QMessageBox.information(self, "Успех", f"Телескоп '{name}' добавлен") + btn_frame = ttk.Frame(dialog) + btn_frame.pack(pady=20) + ttk.Button(btn_frame, text="OK", command=save).pack(side='left', padx=10) + ttk.Button(btn_frame, text="Cancel", command=dialog.destroy).pack(side='left', padx=10) def _remove_telescope(self): - if hasattr(self, '_selected_telescope') and self._selected_telescope: - reply = QMessageBox.question(self, "Подтверждение", - f"Удалить телескоп '{self._selected_telescope}'?", - QMessageBox.Yes | QMessageBox.No) - if reply == QMessageBox.Yes: - self.telescopes.remove(self._selected_telescope) - self.config_service.remove_telescope(self._selected_telescope) - self._update_telescopes_list() + if self.selected_telescope: + reply = messagebox.askyesno("Remove Telescope", f"Remove '{self.selected_telescope}'?", parent=self) + if reply: + self.telescopes.remove(self.selected_telescope) + self.config_service.remove_telescope(self.selected_telescope) + self.telescopes_listbox.delete(0, 'end') + for telescope in self.telescopes: + self.telescopes_listbox.insert('end', telescope) + self.selected_telescope = None + self.remove_telescope_btn.config(state='disabled') + self.edit_telescope_btn.config(state='disabled') def _edit_telescope(self): - if hasattr(self, '_selected_telescope') and self._selected_telescope: - new_name, ok = QInputDialog.getText(self, "Редактировать телескоп", - f"Изменить '{self._selected_telescope}' на:", - text=self._selected_telescope) - if ok and new_name and new_name.strip(): - new_name = new_name.strip() - if new_name != self._selected_telescope: - if new_name in self.telescopes: - QMessageBox.warning(self, "Ошибка", "Такой телескоп уже существует!") - return - idx = self.telescopes.index(self._selected_telescope) - self.telescopes[idx] = new_name - self.config_service.remove_telescope(self._selected_telescope) - self.config_service.add_telescope(new_name) - self._update_telescopes_list() \ No newline at end of file + if self.selected_telescope: + import re + match = re.search(r'(.+?) \(f/([\d\.]+), F=(\d+)mm, D=(\d+)mm\)', self.selected_telescope) + if match: + old_name = match.group(1) + old_aperture = match.group(2) + old_focal = match.group(3) + old_diameter = match.group(4) + else: + old_name = self.selected_telescope + old_aperture = "5.0" + old_focal = "1000" + old_diameter = "200" + + dialog = tk.Toplevel(self) + dialog.title("Edit Telescope") + dialog.geometry("400x320") + dialog.transient(self) + dialog.grab_set() + + ttk.Label(dialog, text="Telescope name:").pack(pady=(15, 5)) + name_entry = ttk.Entry(dialog, width=40) + name_entry.insert(0, old_name) + name_entry.pack() + + ttk.Label(dialog, text="Aperture (f/):").pack(pady=(10, 5)) + aperture_entry = ttk.Entry(dialog, width=20) + aperture_entry.insert(0, old_aperture) + aperture_entry.pack() + + ttk.Label(dialog, text="Focal length (mm):").pack(pady=(10, 5)) + focal_entry = ttk.Entry(dialog, width=20) + focal_entry.insert(0, old_focal) + focal_entry.pack() + + ttk.Label(dialog, text="Diameter (mm):").pack(pady=(10, 5)) + diameter_entry = ttk.Entry(dialog, width=20) + diameter_entry.insert(0, old_diameter) + diameter_entry.pack() + + def save(): + new_name = name_entry.get().strip() + try: + aperture = float(aperture_entry.get()) + focal = int(focal_entry.get()) + diameter = int(diameter_entry.get()) + except ValueError: + messagebox.showerror("Error", "Invalid numeric values!", parent=dialog) + return + + new_info = f"{new_name} (f/{aperture}, F={focal}mm, D={diameter}mm)" + if new_info != self.selected_telescope and new_info in self.telescopes: + messagebox.showerror("Error", "Telescope already exists!", parent=dialog) + return + + idx = self.telescopes.index(self.selected_telescope) + self.telescopes[idx] = new_info + self.config_service.remove_telescope(self.selected_telescope) + self.config_service.add_telescope(new_info) + self.telescopes_listbox.delete(0, 'end') + for telescope in self.telescopes: + self.telescopes_listbox.insert('end', telescope) + dialog.destroy() + + btn_frame = ttk.Frame(dialog) + btn_frame.pack(pady=20) + ttk.Button(btn_frame, text="Save", command=save).pack(side='left', padx=10) + ttk.Button(btn_frame, text="Cancel", command=dialog.destroy).pack(side='left', padx=10) \ No newline at end of file diff --git a/ui/dialogs/instructions_dialog.py b/ui/dialogs/instructions_dialog.py index 64b8b28..4ac959f 100644 --- a/ui/dialogs/instructions_dialog.py +++ b/ui/dialogs/instructions_dialog.py @@ -1,164 +1,152 @@ """ -InstructionsDialog - диалог с инструкцией по использованию +InstructionsDialog - диалог с инструкцией на английском (tkinter) """ -from PySide6.QtWidgets import ( - QDialog, QVBoxLayout, QHBoxLayout, QLabel, QTextEdit, - QPushButton, QScrollArea -) -from PySide6.QtCore import Qt -from PySide6.QtGui import QFont + +import tkinter as tk +from tkinter import ttk -class InstructionsDialog(QDialog): - """Диалог с подробной инструкцией пользователя""" +class InstructionsDialog(tk.Toplevel): + """Диалог с подробной инструкцией пользователя на английском""" def __init__(self, parent): super().__init__(parent) + self.parent = parent - self.setWindowTitle("Инструкция по использованию") - self.setMinimumSize(700, 500) - self.resize(750, 550) + self.title("Instructions") + self.geometry("750x600") + self.minsize(700, 500) + self.transient(parent) + self.grab_set() self._create_ui() + self._center_window() def _create_ui(self): - """Создаёт интерфейс диалога""" - layout = QVBoxLayout(self) - layout.setSpacing(10) - layout.setContentsMargins(15, 15, 15, 15) + # Main frame + main_frame = ttk.Frame(self, padding="15") + main_frame.pack(fill='both', expand=True) - # Заголовок - title_label = QLabel("Astro Session Watcher - Руководство пользователя") - title_font = QFont() - title_font.setPointSize(16) - title_font.setBold(True) - title_label.setFont(title_font) - title_label.setAlignment(Qt.AlignCenter) - layout.addWidget(title_label) + # Title + ttk.Label(main_frame, text="Astro Session Watcher - User Guide", font=('Segoe UI', 16, 'bold')).pack(pady=(0, 10)) - # Текст инструкции - text_edit = QTextEdit() - text_edit.setReadOnly(True) - text_edit.setFont(QFont("Consolas", 10)) + # Text with scrollbar + text_frame = ttk.Frame(main_frame) + text_frame.pack(fill='both', expand=True) - instructions = """ -======================= ASTRO SESSION WATCHER ======================= + scrollbar = ttk.Scrollbar(text_frame) + scrollbar.pack(side='right', fill='y') -Приложение автоматически отслеживает появление новых фотографий в указанной папке, -сортирует их по объектам съемки и ведет подробный лог всего процесса. + self.text_widget = tk.Text(text_frame, yscrollcommand=scrollbar.set, wrap='word', + bg='#1e1e1e', fg='#e0e0e0', font=('Consolas', 10)) + self.text_widget.pack(fill='both', expand=True) + scrollbar.config(command=self.text_widget.yview) -📸 ДЛЯ ЧЕГО ЭТО НУЖНО? -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + # Instructions text + instructions = """======================= ASTRO SESSION WATCHER ======================= -Когда вы снимаете астрономические объекты через EOS Utility или аналогичное ПО, -все фотографии сохраняются в одну папку. Astro Session Watcher помогает: +The application automatically tracks new photos in the selected folder, +sorts them by observation targets and maintains detailed logs. -• Автоматически распределять снимки по папкам объектов -• Вести лог каждой сессии -• Не пропустить ни одного кадра при смене объекта -• Хранить историю оборудования и небесных тел +📸 WHAT IS IT FOR? +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -🚀 КАК ЭТО РАБОТАЕТ? -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +• Automatically distribute shots into target folders +• Keep a log of each session +• Don't miss a single frame when changing targets +• Store equipment and celestial bodies history -1. Вы выбираете папку, куда камера сохраняет снимки -2. Приложение создает папку сессии: "AstroSession_ГГГГ-ММ-ДД" -3. Каждый объект съемки получает свою подпапку внутри папки сессии -4. Когда вы меняете объект (нажимаете "Новая цель"), все накопленные - в папке наблюдения файлы автоматически ПЕРЕМЕЩАЮТСЯ в папку предыдущего объекта -5. При завершении сессии оставшиеся файлы также перемещаются в папку последнего объекта +🚀 HOW IT WORKS? +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -📝 ПОШАГОВАЯ ИНСТРУКЦИЯ -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +1. Select the folder where your camera saves photos +2. The app creates a session folder: "AstroSession_YYYY-MM-DD" +3. Each target gets its own subfolder inside the session folder +4. When you change target (press "New Target"), all accumulated files are moved +5. When you end the session, remaining files are moved to the last target -█ 1. ПЕРВЫЙ ЗАПУСК (НАСТРОЙКА) -─────────────────────────────────────────────────────────────────────────────── +📝 STEP-BY-STEP GUIDE +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -• Откройте меню "Файл" → "Оборудование" и добавьте ваши камеры и объективы -• Откройте меню "Файл" → "Небесные тела" и добавьте объекты для наблюдения -• Все данные сохраняются автоматически в файлах настроек +█ 1. FIRST LAUNCH (SETUP) +─────────────────────────────────────────────────────────────────── -█ 2. ЗАПУСК СЕССИИ -─────────────────────────────────────────────────────────────────────────────── +• Go to "File" → "Equipment" and add your cameras and lenses/telescopes +• Go to "File" → "Celestial Bodies" and add your observation targets +• All data is saved automatically in config files -1. Нажмите "Обзор" и выберите папку, куда камера сохраняет снимки -2. Выберите камеру и объектив из выпадающих списков -3. Введите название цели (или выберите из списка небесных тел) -4. Нажмите ▶ "Начать отслеживание" +█ 2. STARTING A SESSION +─────────────────────────────────────────────────────────────────── -✅ После запуска: - • Статус изменится на "● ON AIR" с мигающим красным текстом - • Кнопка "Новая цель" начнет мигать красным контуром - • В папке наблюдения создастся папка "AstroSession_дата" - • Внутри - папка с вашей первой целью +1. Click "Browse" and select the folder where your camera saves photos +2. Select camera and lens/telescope from dropdowns +3. Enter target name (or select from celestial bodies list) +4. Click "▶ Start Tracking" -█ 3. СМЕНА ОБЪЕКТА ВО ВРЕМЯ СЕССИИ -─────────────────────────────────────────────────────────────────────────────── +✅ After launch: + • Status changes to "● ON AIR" with blinking + • "New Target" button becomes active + • Session folder "AstroSession_date" is created + • Inside - folder with your first target -Когда вы заканчиваете снимать один объект и переходите к другому: +█ 3. CHANGING TARGET DURING SESSION +─────────────────────────────────────────────────────────────────── -1. Нажмите кнопку "Новая цель" (или Ctrl+Shift+N) -2. Введите название нового объекта -3. Приложение автоматически: - • Переместит все накопленные файлы в папку предыдущего объекта - • Создаст новую папку для следующего объекта - • Сбросит счетчик файлов - • Продолжит отслеживание +1. Click "New Target" button (or Ctrl+Shift+N) +2. Enter new target name +3. The app automatically moves all accumulated files to the previous target +4. Creates new folder for the next target +5. Resets file counter +6. Continues tracking -💡 ВАЖНО: Если перед сменой объекта в папке наблюдения уже есть файлы, - они НЕ ПОТЕРЯЮТСЯ - все будут перемещены в папку текущего объекта! +💡 IMPORTANT: If there are files in the watch folder before changing target, + they will NOT be lost - all will be moved! -█ 4. ЗАВЕРШЕНИЕ СЕССИИ -─────────────────────────────────────────────────────────────────────────────── +█ 4. ENDING A SESSION +─────────────────────────────────────────────────────────────────── -1. Нажмите ■ "Остановить" (или Ctrl+X) -2. Приложение: - • Переместит все оставшиеся файлы в папку последнего объекта - • Запишет итоговый лог сессии - • Покажет диалог с предложением открыть папку сессии - • Восстановит интерфейс для новой сессии +1. Click "■ Stop" (or Ctrl+X) +2. The app moves all remaining files to the last target +3. Writes final session log +4. Shows dialog with option to open session folder +5. Restores interface for new session -⌨️ ГОРЯЧИЕ КЛАВИШИ -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +⌨️ HOTKEYS +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -Ctrl + O → Выбрать папку наблюдения -Ctrl + E → Управление оборудованием -Ctrl + B → Управление небесными телами -Ctrl + S → Начать сессию -Ctrl + X → Остановить сессию -Ctrl + F → Открыть папку текущей сессии -Ctrl + Shift+N → Создать новый объект -F1 → О программе -F2 → Эта инструкция +Ctrl + O → Select watch folder +Ctrl + E → Equipment management +Ctrl + B → Celestial bodies management +Ctrl + S → Start session +Ctrl + X → Stop session +Ctrl + F → Open current session folder +Ctrl + Shift+N → Create new target +F1 → About +F2 → This instruction -🔧 ФАЙЛЫ НАСТРОЕК -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +🔧 CONFIG FILES +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -📄 astro_settings.json ← камеры, объективы, последняя папка -📄 celestial_bodies.json ← список небесных тел +📄 astro_settings.json ← cameras, lenses, last folder +📄 celestial_bodies.json ← list of celestial bodies -Все файлы хранятся в папке с программой. Вы можете редактировать их вручную. +All files are stored in the program folder. You can edit them manually. -📧 ТЕХНИЧЕСКАЯ ПОДДЕРЖКА -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -Разработчик: Vic Sergeev -Версия: 0.3.0-alpha - -При обнаружении ошибок или для предложений по улучшению: -• Сообщите разработчику -• Приложите файлы логов (SessionLog.txt, ObjectLog.txt) +📧 TECHNICAL SUPPORT +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Text me: norvicdev@gmail.com +Developer: Vic Sergeev +Version: 0.4.0-alpha """ - text_edit.setText(instructions) - layout.addWidget(text_edit) + self.text_widget.insert('1.0', instructions) + self.text_widget.config(state='disabled') - # Кнопка закрытия - close_layout = QHBoxLayout() - close_layout.addStretch() + # Close button + ttk.Button(main_frame, text="Close", command=self.destroy).pack(pady=10) - close_btn = QPushButton("Закрыть") - close_btn.clicked.connect(self.accept) - close_layout.addWidget(close_btn) - - layout.addLayout(close_layout) \ No newline at end of file + def _center_window(self): + self.update_idletasks() + x = self.parent.winfo_x() + (self.parent.winfo_width() // 2) - (self.winfo_width() // 2) + y = self.parent.winfo_y() + (self.parent.winfo_height() // 2) - (self.winfo_height() // 2) + self.geometry(f'+{x}+{y}') \ No newline at end of file diff --git a/ui/main_window.py b/ui/main_window.py index edb8183..2b7cee6 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -1,20 +1,14 @@ """ -MainWindow - главное окно приложения на PySide6 +MainWindow - главное окно приложения на tkinter """ -import sys + +import tkinter as tk +from tkinter import ttk, messagebox, filedialog, simpledialog +from pathlib import Path +from threading import Thread import subprocess import platform -from pathlib import Path from datetime import datetime -from typing import Optional - -from PySide6.QtWidgets import ( - QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, - QLabel, QLineEdit, QComboBox, QPushButton, QMenuBar, QMenu, - QMessageBox, QFileDialog, QInputDialog, QFrame, QApplication -) -from PySide6.QtCore import Qt, QTimer, Signal -from PySide6.QtGui import QFont, QIcon, QAction from services.config_service import ConfigService from services.session_service import SessionService @@ -22,11 +16,15 @@ from services.watch_service import WatchService from services.file_service import FileService -class MainWindow(QMainWindow): +class MainWindow: """Главное окно приложения""" - def __init__(self): - super().__init__() + def __init__(self, root): + self.root = root + self.root.title("Astro Session Watcher v0.4.0") + self.root.geometry("800x550") + self.root.minsize(700, 500) + self.center_window() # Сервисы self.config_service = ConfigService() @@ -36,483 +34,377 @@ class MainWindow(QMainWindow): # Переменные состояния self.running = False self.file_count = 0 - self._blink_timer = None - self._new_object_blink_timer = None + self.current_target = "" + self.current_session_folder = "" + self._blink_active = False - # Настройка окна - self.setWindowTitle("Astro Session Watcher v0.3.0") - self.setMinimumSize(700, 500) - self.resize(800, 550) + # Стили + self._setup_styles() - self.center_window() + # Создаём интерфейс self._create_menu_bar() self._create_main_content() self._load_saved_settings() self._setup_hotkeys() + + # Обновление счётчика self._update_file_count_display() - self.setAttribute(Qt.WA_DeleteOnClose) + # Обработчик закрытия + self.root.protocol("WM_DELETE_WINDOW", self._on_closing) def center_window(self): - screen = QApplication.primaryScreen().availableGeometry() - self.setGeometry( - (screen.width() - self.width()) // 2, - (screen.height() - self.height()) // 2, - self.width(), - self.height() - ) + self.root.update_idletasks() + x = (self.root.winfo_screenwidth() // 2) - (self.root.winfo_width() // 2) + y = (self.root.winfo_screenheight() // 2) - (self.root.winfo_height() // 2) + self.root.geometry(f'+{x}+{y}') + + def _setup_styles(self): + style = ttk.Style() + style.theme_use('clam') + + # Тёмная тема + style.configure('.', background='#1e1e1e', foreground='#e0e0e0') + style.configure('TLabel', background='#1e1e1e', foreground='#e0e0e0') + style.configure('TFrame', background='#1e1e1e') + style.configure('TLabelframe', background='#1e1e1e', foreground='#e0e0e0') + style.configure('TLabelframe.Label', background='#1e1e1e', foreground='#e0e0e0') + + # Кнопки + style.configure('TButton', background='#3c3c3c', foreground='#e0e0e0', borderwidth=1) + style.map('TButton', + background=[('active', '#4c4c4c')], + foreground=[('active', '#ffffff')]) + + # Поля ввода + style.configure('TEntry', fieldbackground='#3c3c3c', foreground='#e0e0e0') + style.configure('TCombobox', fieldbackground='#3c3c3c', foreground='#e0e0e0') + + # Специальные кнопки + style.configure('Green.TButton', background='#4CAF50', foreground='white') + style.map('Green.TButton', background=[('active', '#45a049')]) + + style.configure('Red.TButton', background='#f44336', foreground='black') + style.map('Red.TButton', background=[('active', '#d32f2f')]) + + self.root.configure(bg='#1e1e1e') def _create_menu_bar(self): - menubar = self.menuBar() + menubar = tk.Menu(self.root) + self.root.config(menu=menubar) - # Меню Файл - file_menu = menubar.addMenu("Файл") + # File menu + file_menu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label="File", menu=file_menu) + file_menu.add_command(label="Select Folder...", command=self.select_folder, accelerator="Ctrl+O") + file_menu.add_separator() + file_menu.add_command(label="Equipment...", command=self.open_equipment_dialog, accelerator="Ctrl+E") + file_menu.add_command(label="Celestial Bodies...", command=self.open_celestial_dialog, accelerator="Ctrl+B") + file_menu.add_separator() + file_menu.add_command(label="Exit", command=self._on_closing, accelerator="Ctrl+Q") - select_folder_action = QAction("Выбрать папку...", self) - select_folder_action.setShortcut("Ctrl+O") - select_folder_action.triggered.connect(self.select_folder) - file_menu.addAction(select_folder_action) + # Session menu + session_menu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label="Session", menu=session_menu) + session_menu.add_command(label="Start Tracking", command=self.start, accelerator="Ctrl+S") + session_menu.add_command(label="Stop", command=self.stop, accelerator="Ctrl+X") + session_menu.add_separator() + session_menu.add_command(label="Open Session Folder", command=self.open_session_folder, accelerator="Ctrl+F") + session_menu.add_separator() + session_menu.add_command(label="New Target...", command=self.set_new_object, accelerator="Ctrl+Shift+N") + session_menu.add_separator() + session_menu.add_command(label="Calibration Frames...", command=self.open_calibration_dialog, accelerator="Ctrl+K") - file_menu.addSeparator() - - equipment_action = QAction("Оборудование...", self) - equipment_action.setShortcut("Ctrl+E") - equipment_action.triggered.connect(self.open_equipment_dialog) - file_menu.addAction(equipment_action) - - celestial_action = QAction("Небесные тела...", self) - celestial_action.setShortcut("Ctrl+B") - celestial_action.triggered.connect(self.open_celestial_dialog) - file_menu.addAction(celestial_action) - - file_menu.addSeparator() - - exit_action = QAction("Выход", self) - exit_action.setShortcut("Ctrl+Q") - exit_action.triggered.connect(self.close) - file_menu.addAction(exit_action) - - # Меню Сессия - session_menu = menubar.addMenu("Сессия") - - start_action = QAction("Начать наблюдение", self) - start_action.setShortcut("Ctrl+S") - start_action.triggered.connect(self.start) - session_menu.addAction(start_action) - - stop_action = QAction("Остановить наблюдение", self) - stop_action.setShortcut("Ctrl+X") - stop_action.triggered.connect(self.stop) - session_menu.addAction(stop_action) - - session_menu.addSeparator() - - open_folder_action = QAction("Открыть папку сессии", self) - open_folder_action.setShortcut("Ctrl+F") - open_folder_action.triggered.connect(self.open_session_folder) - session_menu.addAction(open_folder_action) - - session_menu.addSeparator() - - new_object_action = QAction("Новая цель...", self) - new_object_action.setShortcut("Ctrl+Shift+N") - new_object_action.triggered.connect(self.set_new_object) - session_menu.addAction(new_object_action) - - session_menu.addSeparator() - - calibration_action = QAction("🌑 Калибровочные кадры...", self) - calibration_action.setShortcut("Ctrl+K") - calibration_action.triggered.connect(self.open_calibration_dialog) - session_menu.addAction(calibration_action) - - # Меню Помощь - help_menu = menubar.addMenu("Помощь") - - instructions_action = QAction("Инструкция", self) - instructions_action.setShortcut("F2") - instructions_action.triggered.connect(self.show_instructions) - help_menu.addAction(instructions_action) - - help_menu.addSeparator() - - about_action = QAction("О программе", self) - about_action.setShortcut("F1") - about_action.triggered.connect(self.show_info) - help_menu.addAction(about_action) - - def open_calibration_dialog(self): - """Открывает диалог калибровочных кадров""" - from ui.dialogs.calibration_dialog import CalibrationDialog - dialog = CalibrationDialog(self, self.config_service) - dialog.exec() + # Help menu + help_menu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label="Help", menu=help_menu) + help_menu.add_command(label="Instructions", command=self.show_instructions, accelerator="F2") + help_menu.add_separator() + help_menu.add_command(label="About", command=self.show_info, accelerator="F1") def _create_main_content(self): - central_widget = QWidget() - self.setCentralWidget(central_widget) + # Main frame + main_frame = ttk.Frame(self.root, padding="20") + main_frame.pack(fill="both", expand=True) - main_layout = QVBoxLayout(central_widget) - main_layout.setContentsMargins(20, 20, 20, 20) - main_layout.setSpacing(15) + # Grid layout + main_frame.grid_columnconfigure(0, weight=0, minsize=100) + main_frame.grid_columnconfigure(1, weight=1) - grid_layout = QGridLayout() - grid_layout.setVerticalSpacing(12) - grid_layout.setHorizontalSpacing(15) + # Row 0: Folder + ttk.Label(main_frame, text="Folder:", font=('Segoe UI', 10, 'bold')).grid(row=0, column=0, sticky='w', pady=5) + folder_frame = ttk.Frame(main_frame) + folder_frame.grid(row=0, column=1, sticky='ew', pady=5) + folder_frame.grid_columnconfigure(0, weight=1) - # Row 0: Папка - folder_label = QLabel("Папка:") - folder_label.setFont(QFont("", 10, QFont.Bold)) - grid_layout.addWidget(folder_label, 0, 0, Qt.AlignRight | Qt.AlignVCenter) + self.folder_entry = ttk.Entry(folder_frame) + self.folder_entry.grid(row=0, column=0, sticky='ew', padx=(0, 10)) + self.folder_entry.insert(0, "Select watch folder...") + self.folder_entry.bind('', lambda e: self._clear_placeholder(self.folder_entry, "Select watch folder...")) + self.folder_entry.bind('', lambda e: self._restore_placeholder(self.folder_entry, "Select watch folder...")) - folder_widget = QWidget() - folder_layout = QHBoxLayout(folder_widget) - folder_layout.setContentsMargins(0, 0, 0, 0) - folder_layout.setSpacing(10) + self.browse_btn = ttk.Button(folder_frame, text="Browse...", width=10, command=self.select_folder) + self.browse_btn.grid(row=0, column=1) - self.folder_entry = QLineEdit() - self.folder_entry.setPlaceholderText("Выберите папку для отслеживания") - folder_layout.addWidget(self.folder_entry) + # Row 1: Equipment + ttk.Label(main_frame, text="Equipment:", font=('Segoe UI', 10, 'bold')).grid(row=1, column=0, sticky='w', pady=5) + equipment_frame = ttk.Frame(main_frame) + equipment_frame.grid(row=1, column=1, sticky='ew', pady=5) + equipment_frame.grid_columnconfigure(0, weight=1) + equipment_frame.grid_columnconfigure(1, weight=1) - self.folder_button = QPushButton("Обзор...") - self.folder_button.setFixedWidth(80) - self.folder_button.clicked.connect(self.select_folder) - folder_layout.addWidget(self.folder_button) + self.camera_combo = ttk.Combobox(equipment_frame, values=[]) + self.camera_combo.grid(row=0, column=0, sticky='ew', padx=(0, 10)) + self.camera_combo.set("Select or enter camera...") + self.camera_combo.bind('', lambda e: self._clear_combo(self.camera_combo, "Select or enter camera...")) + self.camera_combo.bind('', lambda e: self._restore_combo(self.camera_combo, "Select or enter camera...")) - grid_layout.addWidget(folder_widget, 0, 1) + self.lens_combo = ttk.Combobox(equipment_frame, values=[]) + self.lens_combo.grid(row=0, column=1, sticky='ew') + self.lens_combo.set("Select or enter lens/telescope...") + self.lens_combo.bind('', lambda e: self._clear_combo(self.lens_combo, "Select or enter lens/telescope...")) + self.lens_combo.bind('', lambda e: self._restore_combo(self.lens_combo, "Select or enter lens/telescope...")) - # Row 1: Оборудование - equipment_label = QLabel("Оборудование:") - equipment_label.setFont(QFont("", 10, QFont.Bold)) - grid_layout.addWidget(equipment_label, 1, 0, Qt.AlignRight | Qt.AlignVCenter) + # Row 2: Target + ttk.Label(main_frame, text="Target:", font=('Segoe UI', 10, 'bold')).grid(row=2, column=0, sticky='w', pady=5) + target_frame = ttk.Frame(main_frame) + target_frame.grid(row=2, column=1, sticky='ew', pady=5) + target_frame.grid_columnconfigure(0, weight=1) - equipment_widget = QWidget() - equipment_layout = QHBoxLayout(equipment_widget) - equipment_layout.setContentsMargins(0, 0, 0, 0) - equipment_layout.setSpacing(10) + self.target_combo = ttk.Combobox(target_frame, values=[]) + self.target_combo.grid(row=0, column=0, sticky='ew', padx=(0, 10)) + self.target_combo.set("Enter target name...") + self.target_combo.bind('', lambda e: self._clear_combo(self.target_combo, "Enter target name...")) + self.target_combo.bind('', lambda e: self._restore_combo(self.target_combo, "Enter target name...")) - self.camera_combo = QComboBox() - self.camera_combo.setEditable(False) - equipment_layout.addWidget(self.camera_combo) + self.new_target_btn = ttk.Button(target_frame, text="New Target", width=12, command=self.set_new_object) + self.new_target_btn.grid(row=0, column=1) + self.new_target_btn.configure(state='disabled') - self.lens_combo = QComboBox() - self.lens_combo.setEditable(False) - equipment_layout.addWidget(self.lens_combo) + # Row 3: Statistics + ttk.Label(main_frame, text="Statistics:", font=('Segoe UI', 10, 'bold')).grid(row=3, column=0, sticky='w', pady=5) + self.stats_label = ttk.Label(main_frame, text="Files received: 0", font=('Segoe UI', 11)) + self.stats_label.grid(row=3, column=1, sticky='w', pady=5) - grid_layout.addWidget(equipment_widget, 1, 1) + # Row 4: Status + ttk.Label(main_frame, text="Status:", font=('Segoe UI', 10, 'bold')).grid(row=4, column=0, sticky='w', pady=5) + self.status_label = ttk.Label(main_frame, text="IDLE", font=('Segoe UI', 12, 'bold'), foreground='#666666') + self.status_label.grid(row=4, column=1, sticky='w', pady=5) - # Row 2: Цель - target_label = QLabel("Цель:") - target_label.setFont(QFont("", 10, QFont.Bold)) - grid_layout.addWidget(target_label, 2, 0, Qt.AlignRight | Qt.AlignVCenter) + # Separator + separator = ttk.Separator(main_frame, orient='horizontal') + separator.grid(row=5, column=0, columnspan=2, sticky='ew', pady=15) - target_widget = QWidget() - target_layout = QHBoxLayout(target_widget) - target_layout.setContentsMargins(0, 0, 0, 0) - target_layout.setSpacing(10) + # Buttons + buttons_frame = ttk.Frame(main_frame) + buttons_frame.grid(row=6, column=0, columnspan=2, pady=10) - self.object_combo = QComboBox() - self.object_combo.setEditable(True) - self.object_combo.setInsertPolicy(QComboBox.NoInsert) - # Настройка плейсхолдера - self.object_combo.lineEdit().setPlaceholderText("Введите название цели") - # Автодополнение при вводе - self.object_combo.lineEdit().textChanged.connect(self._on_object_text_changed) - target_layout.addWidget(self.object_combo) + self.start_btn = ttk.Button(buttons_frame, text="▶ Start Tracking", width=18, command=self.start, style='Green.TButton') + self.start_btn.pack(side='left', padx=10) - self.new_object_button = QPushButton("Новая цель") - self.new_object_button.setFixedWidth(100) - self.new_object_button.setEnabled(False) - self.new_object_button.clicked.connect(self.set_new_object) - target_layout.addWidget(self.new_object_button) + self.stop_btn = ttk.Button(buttons_frame, text="■ Stop", width=12, command=self.stop, style='Red.TButton') + self.stop_btn.pack(side='left', padx=10) + self.stop_btn.configure(state='disabled') - grid_layout.addWidget(target_widget, 2, 1) + # Footer + footer_frame = ttk.Frame(self.root) + footer_frame.pack(side='bottom', fill='x', padx=20, pady=(0, 10)) - # Row 3: Статистика - stats_label = QLabel("Статистика:") - stats_label.setFont(QFont("", 10, QFont.Bold)) - grid_layout.addWidget(stats_label, 3, 0, Qt.AlignRight | Qt.AlignVCenter) + ttk.Label(footer_frame, text="v0.4.0-alpha", foreground='#666666').pack(side='left') + ttk.Label(footer_frame, text="Made by Vic Sergeev 2026", foreground='#666666').pack(side='right') - self.file_count_label = QLabel("Файлов получено: 0") - self.file_count_label.setFont(QFont("", 11)) - grid_layout.addWidget(self.file_count_label, 3, 1, Qt.AlignLeft) + def _clear_placeholder(self, entry, placeholder): + if entry.get() == placeholder: + entry.delete(0, 'end') - # Row 4: Статус - status_label = QLabel("Статус:") - status_label.setFont(QFont("", 10, QFont.Bold)) - grid_layout.addWidget(status_label, 4, 0, Qt.AlignRight | Qt.AlignVCenter) + def _restore_placeholder(self, entry, placeholder): + if entry.get() == '': + entry.insert(0, placeholder) - self.status_label = QLabel("IDLE") - self.status_label.setFont(QFont("", 12, QFont.Bold)) - self.status_label.setStyleSheet("color: #666666;") - grid_layout.addWidget(self.status_label, 4, 1, Qt.AlignLeft) + def _clear_combo(self, combo, placeholder): + if combo.get() == placeholder: + combo.set('') - main_layout.addLayout(grid_layout) - - separator = QFrame() - separator.setFrameShape(QFrame.HLine) - separator.setStyleSheet("background-color: #333333; max-height: 1px;") - main_layout.addWidget(separator) - - buttons_layout = QHBoxLayout() - buttons_layout.setSpacing(15) - buttons_layout.setAlignment(Qt.AlignCenter) - - self.start_button = QPushButton("▶ Начать отслеживание") - self.start_button.setFixedSize(180, 35) - self.start_button.setStyleSheet(""" - QPushButton { - background-color: #4CAF50; - color: white; - font-weight: bold; - border-radius: 4px; - } - QPushButton:hover { - background-color: #45a049; - } - """) - self.start_button.clicked.connect(self.start) - buttons_layout.addWidget(self.start_button) - - self.stop_button = QPushButton("■ Остановить") - self.stop_button.setFixedSize(180, 35) - self.stop_button.setEnabled(False) - self.stop_button.setStyleSheet(""" - QPushButton { - background-color: #f44336; - color: white; - font-weight: bold; - border-radius: 4px; - } - QPushButton:hover { - background-color: #d32f2f; - } - """) - self.stop_button.clicked.connect(self.stop) - buttons_layout.addWidget(self.stop_button) - - main_layout.addLayout(buttons_layout) - - footer_layout = QHBoxLayout() - - version_label = QLabel("v0.3.0-alpha") - version_label.setStyleSheet("color: #666666; font-size: 11px;") - footer_layout.addWidget(version_label) - - footer_layout.addStretch() - - copyright_label = QLabel("Made by Vic Sergeev 2026") - copyright_label.setStyleSheet("color: #666666; font-size: 11px;") - footer_layout.addWidget(copyright_label) - - main_layout.addLayout(footer_layout) - - def _on_object_text_changed(self, text): - """Автодополнение при вводе названия цели""" - if not text: - return - - # Поиск совпадений в списке небесных тел - celestial_bodies = self.config_service.get_celestial_bodies() - matches = [body for body in celestial_bodies if body.lower().startswith(text.lower())] - - if matches and matches[0] != text: - # Временно отключаем сигнал, чтобы избежать рекурсии - self.object_combo.lineEdit().blockSignals(True) - self.object_combo.lineEdit().setText(matches[0]) - self.object_combo.lineEdit().setSelection(len(text), len(matches[0])) - self.object_combo.lineEdit().blockSignals(False) + def _restore_combo(self, combo, placeholder): + if combo.get() == '': + combo.set(placeholder) def _load_saved_settings(self): - """Загружает сохранённые настройки""" cameras = self.config_service.get_cameras() lenses = self.config_service.get_lenses() - telescopes = self.config_service.get_telescopes() # <-- добавить + telescopes = self.config_service.get_telescopes() celestial_bodies = self.config_service.get_celestial_bodies() - # Объединяем объективы и телескопы для выбора оптики all_optics = [] for lens in lenses: - all_optics.append(f"🔭 {lens}") + all_optics.append(lens) for telescope in telescopes: - all_optics.append(f"🪐 {telescope}") + all_optics.append(telescope) if cameras: - self.camera_combo.addItems(cameras) + self.camera_combo['values'] = cameras last_camera = self.config_service.get_last_camera() if last_camera and last_camera in cameras: - self.camera_combo.setCurrentText(last_camera) + self.camera_combo.set(last_camera) if all_optics: - self.lens_combo.addItems(all_optics) + self.lens_combo['values'] = all_optics last_lens = self.config_service.get_last_lens() - if last_lens: - # Ищем последнюю использованную оптику - for opt in all_optics: - if last_lens in opt: - self.lens_combo.setCurrentText(opt) - break + if last_lens and last_lens in all_optics: + self.lens_combo.set(last_lens) if celestial_bodies: - self.object_combo.addItems(celestial_bodies) + self.target_combo['values'] = celestial_bodies last_folder = self.config_service.get_last_watch_folder() if last_folder: - self.folder_entry.setText(last_folder) + self.folder_entry.delete(0, 'end') + self.folder_entry.insert(0, last_folder) def _setup_hotkeys(self): - pass + def on_key(event): + if event.state & 0x4: # Ctrl + if event.keysym == 'o': + self.select_folder() + elif event.keysym == 'e': + self.open_equipment_dialog() + elif event.keysym == 'b': + self.open_celestial_dialog() + elif event.keysym == 's': + self.start() + elif event.keysym == 'x': + self.stop() + elif event.keysym == 'f': + self.open_session_folder() + elif event.keysym == 'k': + self.open_calibration_dialog() + elif event.state & 0x6: # Ctrl+Shift + if event.keysym == 'N': + self.set_new_object() + elif event.keysym == 'F1': + self.show_info() + elif event.keysym == 'F2': + self.show_instructions() - def _set_running_state(self, state: bool): + self.root.bind_all('', on_key) + + def _set_running_state(self, state): self.running = state if state: - self.start_button.setEnabled(False) - self.stop_button.setEnabled(True) - self.new_object_button.setEnabled(True) - self.status_label.setText("● ON AIR") - self.status_label.setStyleSheet("color: #ff0000; font-weight: bold;") + self.start_btn.configure(state='disabled') + self.stop_btn.configure(state='normal') + self.new_target_btn.configure(state='normal') + self.status_label.configure(text="● ON AIR", foreground='#ff0000') self._start_blinking() - self._start_new_object_blinking() else: - self.start_button.setEnabled(True) - self.stop_button.setEnabled(False) - self.new_object_button.setEnabled(False) - self.status_label.setText("IDLE") - self.status_label.setStyleSheet("color: #666666; font-weight: bold;") + self.start_btn.configure(state='normal') + self.stop_btn.configure(state='disabled') + self.new_target_btn.configure(state='disabled') + self.status_label.configure(text="IDLE", foreground='#666666') self._stop_blinking() - self._stop_new_object_blinking() def _start_blinking(self): - self._blink_timer = QTimer() - self._blink_timer.timeout.connect(self._do_blink) - self._blink_timer.start(500) + self._blink_active = True + self._do_blink() def _do_blink(self): - if not self.running: + if not self._blink_active or not self.running: return - current_style = self.status_label.styleSheet() - if "color: #ff0000" in current_style: - self.status_label.setStyleSheet("color: #ffffff; font-weight: bold;") - else: - self.status_label.setStyleSheet("color: #ff0000; font-weight: bold;") + current = self.status_label.cget('foreground') + new_color = '#ffffff' if current == '#ff0000' else '#ff0000' + self.status_label.configure(foreground=new_color) + self.root.after(500, self._do_blink) def _stop_blinking(self): - if self._blink_timer: - self._blink_timer.stop() - self._blink_timer = None - self.status_label.setStyleSheet("color: #666666; font-weight: bold;") - - def _start_new_object_blinking(self): - self._new_object_blink_timer = QTimer() - self._new_object_blink_timer.timeout.connect(self._do_new_object_blink) - self._new_object_blink_timer.start(500) - - def _do_new_object_blink(self): - if not self.running: - return - current_style = self.new_object_button.styleSheet() - if "border: 2px solid red" in current_style: - self.new_object_button.setStyleSheet("") - else: - self.new_object_button.setStyleSheet("border: 2px solid red; border-radius: 4px;") - - def _stop_new_object_blinking(self): - if self._new_object_blink_timer: - self._new_object_blink_timer.stop() - self._new_object_blink_timer = None - self.new_object_button.setStyleSheet("") + self._blink_active = False + self.status_label.configure(foreground='#666666') def _update_file_count_display(self): if self.running and self.session_service.get_current_object(): current_obj = self.session_service.get_current_object() self.file_count = current_obj.photo_count - self.file_count_label.setText(f"Файлов получено: {self.file_count}") - QTimer.singleShot(1000, self._update_file_count_display) + self.stats_label.configure(text=f"Files received: {self.file_count}") + self.root.after(1000, self._update_file_count_display) def _on_file_received(self, file_path: Path): - """Обработчик получения нового файла""" - print(f"Обнаружен файл: {file_path}") if self.session_service.handle_file(file_path): self.file_count += 1 - self.file_count_label.setText(f"Файлов получено: {self.file_count}") - print(f"Файл обработан: {file_path.name}") - else: - print(f"Не удалось обработать файл: {file_path}") + self.stats_label.configure(text=f"Files received: {self.file_count}") + print(f"File processed: {file_path.name}") def select_folder(self): - folder = QFileDialog.getExistingDirectory(self, "Выберите папку для отслеживания") + folder = filedialog.askdirectory(title="Select watch folder") if folder: - self.folder_entry.setText(folder) + self.folder_entry.delete(0, 'end') + self.folder_entry.insert(0, folder) self.config_service.set_last_watch_folder(folder) def start(self): - watch_folder = self.folder_entry.text() - object_name = self.object_combo.currentText() + watch_folder = self.folder_entry.get() + target_name = self.target_combo.get() + camera = self.camera_combo.get() + lens = self.lens_combo.get() + + # Skip placeholders + if watch_folder == "Select watch folder...": + watch_folder = "" + if target_name == "Enter target name...": + target_name = "" + if camera == "Select or enter camera...": + camera = "" + if lens == "Select or enter lens/telescope...": + lens = "" if not watch_folder: - QMessageBox.critical(self, "Ошибка", "Папка для отслеживания не выбрана") + messagebox.showerror("Error", "Please select a folder to watch!", parent=self.root) return - if not object_name: - QMessageBox.critical(self, "Ошибка", "Цель не указана") + if not target_name: + messagebox.showerror("Error", "Please enter a target name!", parent=self.root) return - # Проверка, существует ли объект в списке небесных тел celestial_bodies = self.config_service.get_celestial_bodies() - if object_name not in celestial_bodies: - reply = QMessageBox.question(self, "Новый объект", - f"Объект '{object_name}' не найден в списке.\nДобавить его в список?", - QMessageBox.Yes | QMessageBox.No) - if reply == QMessageBox.Yes: - self.config_service.add_celestial_body(object_name) - self.object_combo.addItem(object_name) + if target_name not in celestial_bodies: + reply = messagebox.askyesno("New Target", + f"Target '{target_name}' not found in list.\nAdd it to the list?", + parent=self.root) + if reply: + self.config_service.add_celestial_body(target_name) + self.target_combo['values'] = self.config_service.get_celestial_bodies() + self.target_combo.set(target_name) else: return - camera = self.camera_combo.currentText() - lens = self.lens_combo.currentText() - if not camera or not lens: - reply = QMessageBox.question(self, "Предупреждение", - "Камера или объектив не выбраны. Продолжить?", - QMessageBox.Yes | QMessageBox.No) - if reply == QMessageBox.No: + reply = messagebox.askyesno("Warning", "Camera or lens not selected. Continue?", + parent=self.root) + if not reply: return try: watch_path = Path(watch_folder) - - # Очищаем папку наблюдения от старых файлов FileService.clear_watch_folder(watch_path) camera_val = camera if camera else "Unknown" lens_val = lens if lens else "Unknown" - optics_value = lens - if optics_value.startswith("🔭 "): - optics_value = optics_value[2:] - elif optics_value.startswith("🪐 "): - optics_value = optics_value[2:] - self.config_service.set_last_lens(optics_value) - - self.session_service.start_session(watch_path, object_name, camera_val, lens_val) + self.session_service.start_session(watch_path, target_name, camera_val, lens_val) + self.current_target = target_name + self.current_session_folder = str(self.session_service.get_current_session().session_folder) self.config_service.set_last_camera(camera_val) self.config_service.set_last_lens(lens_val) - # Запускаем отслеживание success = self.watch_service.start(watch_path, self._on_file_received) if not success: - QMessageBox.critical(self, "Ошибка", "Не удалось запустить отслеживание папки") + messagebox.showerror("Error", "Failed to start watching folder!", parent=self.root) return self._set_running_state(True) - print(f"Отслеживание начато! Папка наблюдения: {watch_path}") - print(f"Папка сессии: {self.session_service.get_current_session().session_folder}") - except Exception as e: - QMessageBox.critical(self, "Ошибка", f"Не удалось начать сессию: {e}") + messagebox.showerror("Error", f"Failed to start session: {e}", parent=self.root) import traceback traceback.print_exc() @@ -521,114 +413,122 @@ class MainWindow(QMainWindow): return try: - watch_folder = Path(self.folder_entry.text()) - - print(f"Остановка сессии. Перемещаем файлы из {watch_folder}") - - # Перемещаем все оставшиеся файлы - moved_count = self.watch_service.move_all_existing_files(watch_folder, self._on_file_received) - print(f"Перемещено файлов: {moved_count}") - - # Останавливаем отслеживание + watch_folder = Path(self.folder_entry.get()) + self.watch_service.move_all_existing_files(watch_folder, self._on_file_received) self.watch_service.stop() - - # Завершаем сессию session = self.session_service.finish_session() - self._set_running_state(False) - - # Показываем диалог завершения self._show_session_end_dialog(session) - except Exception as e: - QMessageBox.critical(self, "Ошибка", f"Ошибка при завершении сессии: {e}") - import traceback - traceback.print_exc() + messagebox.showerror("Error", f"Error stopping session: {e}", parent=self.root) def set_new_object(self): if not self.running: - QMessageBox.critical(self, "Ошибка", "Сессия не активна") + messagebox.showerror("Error", "Session is not active!", parent=self.root) return - # Перемещаем все накопленные файлы в папку текущего объекта - watch_folder = Path(self.folder_entry.text()) - moved_count = self.watch_service.move_all_existing_files(watch_folder, self._on_file_received) + watch_folder = Path(self.folder_entry.get()) + self.watch_service.move_all_existing_files(watch_folder, self._on_file_received) - if moved_count > 0: - print(f"Перемещено файлов перед сменой объекта: {moved_count}") + dialog = tk.Toplevel(self.root) + dialog.title("New Target") + dialog.geometry("400x150") + dialog.transient(self.root) + dialog.grab_set() - new_object, ok = QInputDialog.getText(self, "Новый объект", "Введите название объекта:") + ttk.Label(dialog, text="Enter new target name:", font=('Segoe UI', 11)).pack(pady=20) - if ok and new_object and new_object.strip(): - new_name = new_object.strip() + entry = ttk.Entry(dialog, width=40) + entry.pack(pady=10) + entry.focus() - # Проверка, существует ли объект в списке - celestial_bodies = self.config_service.get_celestial_bodies() - if new_name not in celestial_bodies: - reply = QMessageBox.question(self, "Новый объект", - f"Объект '{new_name}' не найден в списке.\nДобавить его в список?", - QMessageBox.Yes | QMessageBox.No) - if reply == QMessageBox.Yes: - self.config_service.add_celestial_body(new_name) - self.object_combo.addItem(new_name) - else: - return + def confirm(): + new_target = entry.get().strip() + if new_target: + dialog.destroy() + self._create_new_target(new_target) + else: + messagebox.showwarning("Warning", "Please enter a target name!", parent=dialog) - self.session_service.create_new_object(new_name) - self.object_combo.setCurrentText(new_name) - QMessageBox.information(self, "Успех", f"Объект изменён на: {new_name}") + ttk.Button(dialog, text="OK", command=confirm).pack(pady=10) + dialog.bind('', lambda e: confirm()) + + self.root.wait_window(dialog) + + def _create_new_target(self, new_name): + celestial_bodies = self.config_service.get_celestial_bodies() + if new_name not in celestial_bodies: + reply = messagebox.askyesno("New Target", + f"Target '{new_name}' not found in list.\nAdd it to the list?", + parent=self.root) + if reply: + self.config_service.add_celestial_body(new_name) + self.target_combo['values'] = self.config_service.get_celestial_bodies() + else: + return + + self.session_service.create_new_object(new_name) + self.target_combo.set(new_name) + self.file_count = 0 + self.stats_label.configure(text="Files received: 0") def open_equipment_dialog(self): from ui.dialogs.equipment_dialog import EquipmentDialog - dialog = EquipmentDialog(self, self.config_service) - dialog.exec() + dialog = EquipmentDialog(self.root, self.config_service) + self.root.wait_window(dialog) - self.camera_combo.clear() - self.lens_combo.clear() - self.camera_combo.addItems(self.config_service.get_cameras()) - self.lens_combo.addItems(self.config_service.get_lenses()) + # Refresh comboboxes + cameras = self.config_service.get_cameras() + lenses = self.config_service.get_lenses() + telescopes = self.config_service.get_telescopes() + + all_optics = lenses + telescopes + + self.camera_combo['values'] = cameras + self.lens_combo['values'] = all_optics def open_celestial_dialog(self): from ui.dialogs.celestial_dialog import CelestialDialog - dialog = CelestialDialog(self, self.config_service) - dialog.exec() + dialog = CelestialDialog(self.root, self.config_service) + self.root.wait_window(dialog) - self.object_combo.clear() - self.object_combo.addItems(self.config_service.get_celestial_bodies()) + celestial_bodies = self.config_service.get_celestial_bodies() + self.target_combo['values'] = celestial_bodies + + def open_calibration_dialog(self): + from ui.dialogs.calibration_dialog import CalibrationDialog + dialog = CalibrationDialog(self.root, self.config_service) + self.root.wait_window(dialog) def open_session_folder(self): - if self.running and self.session_service.get_current_session(): - folder = self.session_service.get_current_session().session_folder - if folder and folder.exists(): - try: - if platform.system() == "Windows": - subprocess.Popen(['explorer', str(folder)]) - elif platform.system() == "Darwin": - subprocess.Popen(['open', str(folder)]) - else: - subprocess.Popen(['xdg-open', str(folder)]) - except Exception as e: - QMessageBox.critical(self, "Ошибка", f"Не удалось открыть папку: {e}") - else: - QMessageBox.critical(self, "Ошибка", "Папка сессии не найдена") + if self.running and self.current_session_folder: + try: + if platform.system() == "Windows": + subprocess.Popen(['explorer', self.current_session_folder]) + elif platform.system() == "Darwin": + subprocess.Popen(['open', self.current_session_folder]) + else: + subprocess.Popen(['xdg-open', self.current_session_folder]) + except Exception as e: + messagebox.showerror("Error", f"Failed to open folder: {e}", parent=self.root) else: - QMessageBox.information(self, "Информация", "Нет активной сессии") + messagebox.showinfo("Info", "No active session", parent=self.root) def show_instructions(self): from ui.dialogs.instructions_dialog import InstructionsDialog - dialog = InstructionsDialog(self) - dialog.exec() + InstructionsDialog(self.root) def show_info(self): - QMessageBox.about(self, "О программе", - "Astro Session Watcher\nВерсия: 0.3.0-alpha\n\n" - "Приложение для автоматической сортировки астрофотографий\n\n" - "Особенности:\n" - "• Автоматическое отслеживание новых файлов\n" - "• Сортировка по объектам съёмки\n" - "• Ведение детальных логов\n" - "• Сохранение истории оборудования\n\n" - "Разработчик: Vic Sergeev\n2026") + messagebox.showinfo("About", + "Astro Session Watcher v0.4.0\n\n" + "Application for astrophotographers\n\n" + "Features:\n" + "• Automatic file tracking\n" + "• Sorting by targets\n" + "• Session logging\n" + "• Equipment management\n\n" + "Made by Vic Sergeev\n2026", + parent=self.root) def _show_session_end_dialog(self, session): current_object = session.get_current_object() @@ -636,17 +536,18 @@ class MainWindow(QMainWindow): photo_count = current_object.photo_count if current_object else 0 session_folder = session.session_folder - msg_box = QMessageBox(self) - msg_box.setWindowTitle("Сессия завершена") - msg_box.setIcon(QMessageBox.Information) - msg_box.setText(f"Наблюдение остановлено\n\nСессия для объекта '{object_name}' завершена.\nПолучено файлов: {photo_count}") - msg_box.setInformativeText(f"Папка с данными:\n{session_folder}") + dialog = tk.Toplevel(self.root) + dialog.title("Session Completed") + dialog.geometry("500x250") + dialog.transient(self.root) + dialog.grab_set() - open_folder_btn = msg_box.addButton("📁 Открыть папку", QMessageBox.AcceptRole) - close_btn = msg_box.addButton("Закрыть", QMessageBox.RejectRole) + ttk.Label(dialog, text="✅ Session finished!", font=('Segoe UI', 14, 'bold')).pack(pady=15) + ttk.Label(dialog, text=f"Target: {object_name}").pack() + ttk.Label(dialog, text=f"Files received: {photo_count}").pack() + ttk.Label(dialog, text=f"Saved to: {session_folder}", wraplength=450).pack(pady=10) - msg_box.exec() - if msg_box.clickedButton() == open_folder_btn: + def open_folder(): if session_folder and session_folder.exists(): try: if platform.system() == "Windows": @@ -656,20 +557,23 @@ class MainWindow(QMainWindow): else: subprocess.Popen(['xdg-open', str(session_folder)]) except Exception as e: - QMessageBox.critical(self, "Ошибка", f"Не удалось открыть папку: {e}") + messagebox.showerror("Error", f"Failed to open folder: {e}", parent=dialog) + dialog.destroy() - def closeEvent(self, event): + def close(): + dialog.destroy() + + btn_frame = ttk.Frame(dialog) + btn_frame.pack(pady=20) + ttk.Button(btn_frame, text="Open Folder", command=open_folder).pack(side='left', padx=10) + ttk.Button(btn_frame, text="Close", command=close).pack(side='left', padx=10) + + def _on_closing(self): if self.running: - reply = QMessageBox.question(self, "Выход", - "Сессия активна. Остановить сессию и выйти?", - QMessageBox.Yes | QMessageBox.No) - if reply == QMessageBox.Yes: - try: - self.stop() - except: - pass - event.accept() - else: - event.ignore() + reply = messagebox.askyesno("Exit", "Session is active. Stop session and exit?", + parent=self.root) + if reply: + self.stop() + self.root.destroy() else: - event.accept() \ No newline at end of file + self.root.destroy() \ No newline at end of file