persian-clock-widget 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README-LINUX.md +128 -0
- package/README.md +263 -2
- package/assets/fonts/BYekan+ Bold.ttf +0 -0
- package/assets/fonts/Iranian Sans.ttf +0 -0
- package/assets/fonts/Vazir-Medium.ttf +0 -0
- package/assets/icons/app_icon.ico +0 -0
- package/assets/icons/app_icon.png +0 -0
- package/assets/sounds/alarm.mp3 +0 -0
- package/assets/sounds/axel.mp3 +0 -0
- package/assets/sounds/heeeeyyyyy.mp3 +0 -0
- package/assets/sounds/liilililililililii.mp3 +0 -0
- package/assets/sounds/morning_glory.mp3 +0 -0
- package/assets/sounds/peter_axel_f.mp3 +0 -0
- package/bin/persian-clock.js +60 -0
- package/main.py +26 -0
- package/package.json +39 -4
- package/release/windows/PersianClockWidget.exe +0 -0
- package/requirements.txt +3 -0
- package/src/__init__.py +8 -0
- package/src/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/__pycache__/alarm_manager.cpython-314.pyc +0 -0
- package/src/__pycache__/calendar_data.cpython-314.pyc +0 -0
- package/src/__pycache__/taskbar_clock.cpython-314.pyc +0 -0
- package/src/__pycache__/widget.cpython-314.pyc +0 -0
- package/src/__pycache__/win_utils.cpython-314.pyc +0 -0
- package/src/alarm_manager.py +1314 -0
- package/src/calendar_data.py +519 -0
- package/src/icon_generator.py +106 -0
- package/src/taskbar_clock.py +834 -0
- package/src/widget.py +3351 -0
- package/src/win_utils.py +541 -0
package/src/widget.py
ADDED
|
@@ -0,0 +1,3351 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
ویجت حرفهای ساعت تهران، تقویم شمسی، مناسبتها، ابزار کپی زمان برنامهنویسی (۱۰ فرمت API + تولیدکننده GUID) و سیستم آلارم/رویداد
|
|
4
|
+
طراحی و توسعه: شایان بامیار (® Shayan Bamyar)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import time
|
|
12
|
+
import tkinter as tk
|
|
13
|
+
from tkinter import colorchooser, font as tkfont, ttk
|
|
14
|
+
import uuid
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from src.alarm_manager import (
|
|
18
|
+
AlarmManagerWindow,
|
|
19
|
+
AlarmStore,
|
|
20
|
+
AnimatedBellPopup,
|
|
21
|
+
draw_outlined_canvas_text,
|
|
22
|
+
)
|
|
23
|
+
from src.calendar_data import (
|
|
24
|
+
PERSIAN_MONTHS,
|
|
25
|
+
PERSIAN_WEEK_SHORT,
|
|
26
|
+
TEHRAN_TZ,
|
|
27
|
+
get_jalali_month_days,
|
|
28
|
+
get_month_calendar,
|
|
29
|
+
get_today_info,
|
|
30
|
+
gregorian_to_jalali,
|
|
31
|
+
)
|
|
32
|
+
from src.win_utils import (
|
|
33
|
+
SystemTrayIcon,
|
|
34
|
+
apply_rounded_corners,
|
|
35
|
+
apply_z_order_and_clickthrough,
|
|
36
|
+
enable_dpi_awareness,
|
|
37
|
+
get_data_dir,
|
|
38
|
+
get_resource_path,
|
|
39
|
+
hide_from_taskbar,
|
|
40
|
+
is_startup_enabled,
|
|
41
|
+
register_bundled_fonts,
|
|
42
|
+
set_startup_enabled,
|
|
43
|
+
)
|
|
44
|
+
from src.taskbar_clock import (
|
|
45
|
+
TaskbarButtonProxy,
|
|
46
|
+
TaskbarClockOverlay,
|
|
47
|
+
WindowsClockReplacer,
|
|
48
|
+
get_windows11_tray_clock_hidden,
|
|
49
|
+
set_windows11_tray_clock_hidden,
|
|
50
|
+
)
|
|
51
|
+
except ImportError:
|
|
52
|
+
from alarm_manager import (
|
|
53
|
+
AlarmManagerWindow,
|
|
54
|
+
AlarmStore,
|
|
55
|
+
AnimatedBellPopup,
|
|
56
|
+
draw_outlined_canvas_text,
|
|
57
|
+
)
|
|
58
|
+
from calendar_data import (
|
|
59
|
+
PERSIAN_MONTHS,
|
|
60
|
+
PERSIAN_WEEK_SHORT,
|
|
61
|
+
TEHRAN_TZ,
|
|
62
|
+
get_jalali_month_days,
|
|
63
|
+
get_month_calendar,
|
|
64
|
+
get_today_info,
|
|
65
|
+
gregorian_to_jalali,
|
|
66
|
+
)
|
|
67
|
+
from win_utils import (
|
|
68
|
+
SystemTrayIcon,
|
|
69
|
+
apply_rounded_corners,
|
|
70
|
+
apply_z_order_and_clickthrough,
|
|
71
|
+
enable_dpi_awareness,
|
|
72
|
+
get_data_dir,
|
|
73
|
+
get_resource_path,
|
|
74
|
+
hide_from_taskbar,
|
|
75
|
+
is_startup_enabled,
|
|
76
|
+
register_bundled_fonts,
|
|
77
|
+
set_startup_enabled,
|
|
78
|
+
)
|
|
79
|
+
from taskbar_clock import (
|
|
80
|
+
TaskbarButtonProxy,
|
|
81
|
+
TaskbarClockOverlay,
|
|
82
|
+
WindowsClockReplacer,
|
|
83
|
+
get_windows11_tray_clock_hidden,
|
|
84
|
+
set_windows11_tray_clock_hidden,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
DATA_DIR = get_data_dir()
|
|
88
|
+
CONFIG_PATH = os.path.join(DATA_DIR, "config.json")
|
|
89
|
+
CUSTOM_OCCASIONS_PATH = os.path.join(DATA_DIR, "custom_occasions.json")
|
|
90
|
+
ALARMS_PATH = os.path.join(DATA_DIR, "alarms.json")
|
|
91
|
+
|
|
92
|
+
TRANSPARENT_KEY = "#010203"
|
|
93
|
+
|
|
94
|
+
DEFAULT_CONFIG = {
|
|
95
|
+
"x": None,
|
|
96
|
+
"y": None,
|
|
97
|
+
"locked": False, # False = آزاد (🔓) | True = فیکس و Click-Through (🔒)
|
|
98
|
+
"z_mode": "topmost", # 'topmost' (همیشه روی برنامهها) یا 'desktop' (چسبیده به دسکتاپ)
|
|
99
|
+
"opacity": 0.90,
|
|
100
|
+
"transparent_bg": False,
|
|
101
|
+
"persian_digits": False,
|
|
102
|
+
"calendar_expanded": False,
|
|
103
|
+
"calendar_position": "beside",
|
|
104
|
+
# تنظیمات رنگها
|
|
105
|
+
"bg_color": "#141821",
|
|
106
|
+
"border_color": "#2A3244",
|
|
107
|
+
"clock_color": "#F8FAFC",
|
|
108
|
+
"date_color": "#CBD5E1",
|
|
109
|
+
"occasion_color": "#94A3B8",
|
|
110
|
+
"holiday_color": "#F87171",
|
|
111
|
+
"accent_color": "#38BDF8",
|
|
112
|
+
# تنظیمات سایز ویجت و فونتها (با پیشفرض فونت وزیر Vazir)
|
|
113
|
+
"widget_scale": 100,
|
|
114
|
+
"font_family": "Vazir",
|
|
115
|
+
"clock_font_family": "Vazir",
|
|
116
|
+
"clock_font_size": 26,
|
|
117
|
+
"date_font_size": 11,
|
|
118
|
+
"occasion_font_size": 9,
|
|
119
|
+
# نمایش و جایگزینی در تسکبار ویندوز
|
|
120
|
+
"replace_windows_clock": False, # ⭐ جایگزینی کامل و پوشاندن ساعت پیشفرض ویندوز
|
|
121
|
+
"replace_clock_hide_widget": False, # مخفی کردن ویجت دسکتاپ هنگام فعال بودن جایگزین
|
|
122
|
+
"replace_clock_width": 118, # عرض باکس پوششدهنده ساعت ویندوز
|
|
123
|
+
"replace_clock_offset": 8, # فاصله از گوشه راست تسکبار
|
|
124
|
+
"replace_clock_bg": "auto", # تشخیص خودکار رنگ تسکبار ویندوز (یا رنگ دلخواه)
|
|
125
|
+
"taskbar_clock": False, # نمایش ساعت و تاریخ شمسی روی خود نوار تسکبار (شناور)
|
|
126
|
+
"taskbar_clock_seconds": False, # نمایش ثانیه در ساعت تسکبار
|
|
127
|
+
"taskbar_clock_offset": None, # مکان ذخیرهشده ساعت روی تسکبار (فاصله از لبه راست)
|
|
128
|
+
"taskbar_button": False, # نمایش دکمه/آیکون برنامه در تسکبار
|
|
129
|
+
"snap_side": "right", # جهت چسبیدن به لبه صفحه در حالت قفل ('right' یا 'left')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def format_digits(text: str, use_persian: bool) -> str:
|
|
134
|
+
"""تبدیل اعداد انگلیسی به فارسی در صورت فعال بودن گزینه مربوطه"""
|
|
135
|
+
if not use_persian:
|
|
136
|
+
return str(text)
|
|
137
|
+
en = "0123456789"
|
|
138
|
+
fa = "۰۱۲۳۴۵۶۷۸۹"
|
|
139
|
+
return str(text).translate(str.maketrans(en, fa))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class HoverTooltip:
|
|
143
|
+
"""
|
|
144
|
+
افکت Hover فوری و نمایش Tooltip شناور همیشه در بالاترین لایه (Topmost)
|
|
145
|
+
هنگام نگه داشتن ماوس روی آیکونها
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
def __init__(
|
|
149
|
+
self,
|
|
150
|
+
widget: tk.Widget,
|
|
151
|
+
text_or_fn,
|
|
152
|
+
base_bg_fn,
|
|
153
|
+
base_fg_fn,
|
|
154
|
+
hover_bg: str = "#0284C7",
|
|
155
|
+
hover_fg: str = "#FFFFFF",
|
|
156
|
+
font_family: str = "Vazir",
|
|
157
|
+
):
|
|
158
|
+
self.widget = widget
|
|
159
|
+
self.text_or_fn = text_or_fn
|
|
160
|
+
self.base_bg_fn = base_bg_fn
|
|
161
|
+
self.base_fg_fn = base_fg_fn
|
|
162
|
+
self.hover_bg = hover_bg
|
|
163
|
+
self.hover_fg = hover_fg
|
|
164
|
+
self.font_family = font_family
|
|
165
|
+
self.tip_win: tk.Toplevel | None = None
|
|
166
|
+
self._after_id: str | None = None
|
|
167
|
+
self.is_hovered = False
|
|
168
|
+
|
|
169
|
+
self.widget.bind("<Enter>", self._on_enter, add="+")
|
|
170
|
+
self.widget.bind("<Leave>", self._on_leave, add="+")
|
|
171
|
+
self.widget.bind("<ButtonPress>", self._on_press, add="+")
|
|
172
|
+
|
|
173
|
+
def _on_enter(self, event=None):
|
|
174
|
+
self.is_hovered = True
|
|
175
|
+
try:
|
|
176
|
+
self.widget.configure(bg=self.hover_bg, fg=self.hover_fg)
|
|
177
|
+
except Exception:
|
|
178
|
+
pass
|
|
179
|
+
self._cancel_timer()
|
|
180
|
+
self._after_id = self.widget.after(90, self._show_tip)
|
|
181
|
+
|
|
182
|
+
def _on_leave(self, event=None):
|
|
183
|
+
self.is_hovered = False
|
|
184
|
+
self._cancel_timer()
|
|
185
|
+
try:
|
|
186
|
+
self.widget.configure(bg=self.base_bg_fn(), fg=self.base_fg_fn())
|
|
187
|
+
except Exception:
|
|
188
|
+
pass
|
|
189
|
+
self._hide_tip()
|
|
190
|
+
|
|
191
|
+
def _on_press(self, event=None):
|
|
192
|
+
self._cancel_timer()
|
|
193
|
+
self._hide_tip()
|
|
194
|
+
|
|
195
|
+
def _cancel_timer(self):
|
|
196
|
+
if self._after_id is not None:
|
|
197
|
+
try:
|
|
198
|
+
self.widget.after_cancel(self._after_id)
|
|
199
|
+
except Exception:
|
|
200
|
+
pass
|
|
201
|
+
self._after_id = None
|
|
202
|
+
|
|
203
|
+
def _show_tip(self):
|
|
204
|
+
if not self.is_hovered or self.tip_win is not None:
|
|
205
|
+
return
|
|
206
|
+
txt = self.text_or_fn() if callable(self.text_or_fn) else str(self.text_or_fn)
|
|
207
|
+
if not txt:
|
|
208
|
+
return
|
|
209
|
+
try:
|
|
210
|
+
wx = self.widget.winfo_rootx()
|
|
211
|
+
wy = self.widget.winfo_rooty()
|
|
212
|
+
wh = self.widget.winfo_height()
|
|
213
|
+
|
|
214
|
+
win = tk.Toplevel(self.widget)
|
|
215
|
+
self.tip_win = win
|
|
216
|
+
win.overrideredirect(True)
|
|
217
|
+
win.attributes("-topmost", True)
|
|
218
|
+
win.configure(bg="#0F172A")
|
|
219
|
+
|
|
220
|
+
frm = tk.Frame(
|
|
221
|
+
win,
|
|
222
|
+
bg="#0F172A",
|
|
223
|
+
highlightbackground="#38BDF8",
|
|
224
|
+
highlightthickness=1,
|
|
225
|
+
padx=8,
|
|
226
|
+
pady=4,
|
|
227
|
+
)
|
|
228
|
+
frm.pack()
|
|
229
|
+
lbl = tk.Label(
|
|
230
|
+
frm,
|
|
231
|
+
text=txt,
|
|
232
|
+
font=(self.font_family, 8, "bold"),
|
|
233
|
+
fg="#F8FAFC",
|
|
234
|
+
bg="#0F172A",
|
|
235
|
+
)
|
|
236
|
+
lbl.pack()
|
|
237
|
+
|
|
238
|
+
win.update_idletasks()
|
|
239
|
+
tw = win.winfo_reqwidth()
|
|
240
|
+
th = win.winfo_reqheight()
|
|
241
|
+
screen_w = win.winfo_screenwidth()
|
|
242
|
+
screen_h = win.winfo_screenheight()
|
|
243
|
+
|
|
244
|
+
# نمایش در بالای آیکون یا پایین آیکون بهگونهای که همیشه کامل دیده شود
|
|
245
|
+
pos_x = wx - (tw // 3)
|
|
246
|
+
if pos_x + tw > screen_w - 8:
|
|
247
|
+
pos_x = screen_w - tw - 8
|
|
248
|
+
pos_x = max(8, pos_x)
|
|
249
|
+
|
|
250
|
+
if wy - th - 6 >= 8:
|
|
251
|
+
pos_y = wy - th - 6
|
|
252
|
+
else:
|
|
253
|
+
pos_y = min(screen_h - th - 8, wy + wh + 6)
|
|
254
|
+
|
|
255
|
+
win.geometry(f"+{int(pos_x)}+{int(pos_y)}")
|
|
256
|
+
hide_from_taskbar(win)
|
|
257
|
+
apply_z_order_and_clickthrough(win, "topmost", click_through=True)
|
|
258
|
+
except Exception:
|
|
259
|
+
self._hide_tip()
|
|
260
|
+
|
|
261
|
+
def _hide_tip(self):
|
|
262
|
+
if self.tip_win is not None:
|
|
263
|
+
try:
|
|
264
|
+
self.tip_win.destroy()
|
|
265
|
+
except Exception:
|
|
266
|
+
pass
|
|
267
|
+
self.tip_win = None
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class PersianClockWidget:
|
|
271
|
+
BASE_MIN_W = 290
|
|
272
|
+
BASE_CALENDAR_W = 310
|
|
273
|
+
BASE_CALENDAR_H = 385
|
|
274
|
+
|
|
275
|
+
LOCK_BTN_SIZE = 24
|
|
276
|
+
LOCK_OFFSET_X = 8
|
|
277
|
+
LOCK_OFFSET_Y = 8
|
|
278
|
+
GAP = 8
|
|
279
|
+
|
|
280
|
+
def __init__(self):
|
|
281
|
+
enable_dpi_awareness()
|
|
282
|
+
register_bundled_fonts()
|
|
283
|
+
|
|
284
|
+
self.config = self._load_config()
|
|
285
|
+
if self.config.get("z_mode") == "overlay_locked":
|
|
286
|
+
self.config["z_mode"] = "topmost"
|
|
287
|
+
self.config["locked"] = True
|
|
288
|
+
|
|
289
|
+
# پنجره ریشه مخفی (بدون هیچ آیکونی در Taskbar)
|
|
290
|
+
self.root = tk.Tk()
|
|
291
|
+
self.root.title("PersianClockHiddenRoot")
|
|
292
|
+
self.root.attributes("-alpha", 0.0)
|
|
293
|
+
self.root.withdraw()
|
|
294
|
+
|
|
295
|
+
sys_fams = set(tkfont.families())
|
|
296
|
+
if self.config["font_family"] not in sys_fams:
|
|
297
|
+
for candidate in ("Vazir", "B Yekan+", "A Iranian Sans", "Tahoma"):
|
|
298
|
+
if candidate in sys_fams:
|
|
299
|
+
self.config["font_family"] = candidate
|
|
300
|
+
break
|
|
301
|
+
if self.config["clock_font_family"] not in sys_fams:
|
|
302
|
+
for candidate in ("Vazir", "B Yekan+", "A Iranian Sans", "Segoe UI Semibold", "Tahoma"):
|
|
303
|
+
if candidate in sys_fams:
|
|
304
|
+
self.config["clock_font_family"] = candidate
|
|
305
|
+
break
|
|
306
|
+
|
|
307
|
+
ico_path = get_resource_path(os.path.join("assets", "icons", "app_icon.ico"))
|
|
308
|
+
if os.path.exists(ico_path):
|
|
309
|
+
try:
|
|
310
|
+
self.root.iconbitmap(default=ico_path)
|
|
311
|
+
except Exception:
|
|
312
|
+
pass
|
|
313
|
+
|
|
314
|
+
self.alarm_store = AlarmStore(ALARMS_PATH)
|
|
315
|
+
|
|
316
|
+
today = get_today_info(CUSTOM_OCCASIONS_PATH)
|
|
317
|
+
self.current_jy = today["jy"]
|
|
318
|
+
self.current_jm = today["jm"]
|
|
319
|
+
self.current_jd = today["jd"]
|
|
320
|
+
|
|
321
|
+
self.view_jy = today["jy"]
|
|
322
|
+
self.view_jm = today["jm"]
|
|
323
|
+
self.selected_day_info = today
|
|
324
|
+
|
|
325
|
+
self._computed_w = self.BASE_MIN_W
|
|
326
|
+
self._computed_h = 118
|
|
327
|
+
self._toast_text = ""
|
|
328
|
+
|
|
329
|
+
self._drag_start_x = 0
|
|
330
|
+
self._drag_start_y = 0
|
|
331
|
+
self._is_dragging = False
|
|
332
|
+
self._is_minimized = False
|
|
333
|
+
self._unlocked_x = self.config.get("x")
|
|
334
|
+
self._unlocked_y = self.config.get("y")
|
|
335
|
+
self._tooltips: list[HoverTooltip] = []
|
|
336
|
+
|
|
337
|
+
self.widget_win: tk.Toplevel | None = None
|
|
338
|
+
self.lock_win: tk.Toplevel | None = None
|
|
339
|
+
self.cal_win: tk.Toplevel | None = None
|
|
340
|
+
self.dev_popup_win: tk.Toplevel | None = None
|
|
341
|
+
self.settings_win: tk.Toplevel | None = None
|
|
342
|
+
self.alarm_win: AlarmManagerWindow | None = None
|
|
343
|
+
self.custom_occ_win: tk.Toplevel | None = None
|
|
344
|
+
|
|
345
|
+
self._build_compact_ui()
|
|
346
|
+
self._build_lock_badge_window()
|
|
347
|
+
self._build_calendar_window()
|
|
348
|
+
self._build_context_menu()
|
|
349
|
+
self._build_dev_copy_menu()
|
|
350
|
+
|
|
351
|
+
self._init_position()
|
|
352
|
+
self.root.after(60, self._apply_all_styles)
|
|
353
|
+
|
|
354
|
+
self.tray = SystemTrayIcon(
|
|
355
|
+
tooltip=self._format_tray_tooltip(today),
|
|
356
|
+
on_left_click=self._on_tray_left_click,
|
|
357
|
+
on_right_click=self._on_tray_right_click,
|
|
358
|
+
)
|
|
359
|
+
self.tray.start()
|
|
360
|
+
|
|
361
|
+
self.taskbar_clock: TaskbarClockOverlay | None = None
|
|
362
|
+
self.taskbar_proxy: TaskbarButtonProxy | None = None
|
|
363
|
+
self.clock_replacer: WindowsClockReplacer | None = None
|
|
364
|
+
self.root.after(300, self.sync_taskbar_features)
|
|
365
|
+
|
|
366
|
+
self._update_clock_loop()
|
|
367
|
+
self._keep_z_order_loop()
|
|
368
|
+
|
|
369
|
+
# ------------------------------------------------------------------
|
|
370
|
+
# نمایش در تسکبار (ساعت روی تسکبار + دکمه برنامه در تسکبار)
|
|
371
|
+
# ------------------------------------------------------------------
|
|
372
|
+
def format_digits_public(self, text: str, use_fa: bool) -> str:
|
|
373
|
+
return format_digits(text, use_fa)
|
|
374
|
+
|
|
375
|
+
def sync_taskbar_features(self):
|
|
376
|
+
"""فعال/غیرفعال کردن جایگزین ساعت ویندوز، ساعت روی تسکبار و دکمه تسکبار بر اساس تنظیمات"""
|
|
377
|
+
want_replacer = bool(self.config.get("replace_windows_clock", False))
|
|
378
|
+
want_clock = bool(self.config.get("taskbar_clock", False))
|
|
379
|
+
want_btn = bool(self.config.get("taskbar_button", False))
|
|
380
|
+
hide_desktop = bool(self.config.get("replace_clock_hide_widget", False)) and want_replacer
|
|
381
|
+
|
|
382
|
+
# ۱. جایگزین کامل ساعت و تاریخ ویندوز
|
|
383
|
+
if want_replacer and self.clock_replacer is None:
|
|
384
|
+
self.clock_replacer = WindowsClockReplacer(self)
|
|
385
|
+
self.clock_replacer.refresh(get_today_info(CUSTOM_OCCASIONS_PATH))
|
|
386
|
+
elif not want_replacer and self.clock_replacer is not None:
|
|
387
|
+
self.clock_replacer.destroy()
|
|
388
|
+
self.clock_replacer = None
|
|
389
|
+
|
|
390
|
+
# ۲. ساعت و تاریخ شناور روی تسکبار
|
|
391
|
+
if want_clock and self.taskbar_clock is None:
|
|
392
|
+
self.taskbar_clock = TaskbarClockOverlay(self)
|
|
393
|
+
self.taskbar_clock.refresh(get_today_info(CUSTOM_OCCASIONS_PATH))
|
|
394
|
+
elif not want_clock and self.taskbar_clock is not None:
|
|
395
|
+
self.taskbar_clock.destroy()
|
|
396
|
+
self.taskbar_clock = None
|
|
397
|
+
|
|
398
|
+
# ۳. دکمه/آیکون برنامه در تسکبار
|
|
399
|
+
if want_btn and self.taskbar_proxy is None:
|
|
400
|
+
self.taskbar_proxy = TaskbarButtonProxy(self)
|
|
401
|
+
elif not want_btn and self.taskbar_proxy is not None:
|
|
402
|
+
self.taskbar_proxy.destroy()
|
|
403
|
+
self.taskbar_proxy = None
|
|
404
|
+
|
|
405
|
+
# ۴. مدیریت نمایش/مخفیسازی ویجت دسکتاپ بر اساس تنظیمات جایگزینی
|
|
406
|
+
if hide_desktop:
|
|
407
|
+
if self.widget_win and self.widget_win.winfo_viewable():
|
|
408
|
+
self.widget_win.withdraw()
|
|
409
|
+
if self.lock_win and self.lock_win.winfo_viewable():
|
|
410
|
+
self.lock_win.withdraw()
|
|
411
|
+
else:
|
|
412
|
+
if not getattr(self, "_is_minimized", False):
|
|
413
|
+
if self.widget_win and not self.widget_win.winfo_viewable():
|
|
414
|
+
self.widget_win.deiconify()
|
|
415
|
+
if self.lock_win and not self.lock_win.winfo_viewable() and self.config.get("locked"):
|
|
416
|
+
self.lock_win.deiconify()
|
|
417
|
+
|
|
418
|
+
for var_name, key in (
|
|
419
|
+
("_settings_tb_clock_var", "taskbar_clock"),
|
|
420
|
+
("_settings_tb_btn_var", "taskbar_button"),
|
|
421
|
+
("_settings_replace_clock_var", "replace_windows_clock"),
|
|
422
|
+
("_settings_hide_widget_var", "replace_clock_hide_widget"),
|
|
423
|
+
):
|
|
424
|
+
var = getattr(self, var_name, None)
|
|
425
|
+
if var is not None:
|
|
426
|
+
try:
|
|
427
|
+
var.set(bool(self.config.get(key, False)))
|
|
428
|
+
except Exception:
|
|
429
|
+
pass
|
|
430
|
+
|
|
431
|
+
def _toggle_taskbar_setting(self, key: str):
|
|
432
|
+
self.config[key] = not self.config.get(key, False)
|
|
433
|
+
self._save_config()
|
|
434
|
+
self.sync_taskbar_features()
|
|
435
|
+
|
|
436
|
+
def minimize_widget(self):
|
|
437
|
+
"""مخفی کردن ویجت از صفحه دسکتاپ و نمایش فقط در تسکبار و Hidden Icons"""
|
|
438
|
+
self._is_minimized = True
|
|
439
|
+
self._close_dev_popup_window()
|
|
440
|
+
if self.widget_win:
|
|
441
|
+
self.widget_win.withdraw()
|
|
442
|
+
if self.lock_win:
|
|
443
|
+
self.lock_win.withdraw()
|
|
444
|
+
if self.cal_win:
|
|
445
|
+
self.cal_win.withdraw()
|
|
446
|
+
# اطمینان از حضور دکمه در تسکبار تا کاربر بتواند با یک کلیک آن را بازگرداند
|
|
447
|
+
if self.taskbar_proxy is None:
|
|
448
|
+
self.taskbar_proxy = TaskbarButtonProxy(self)
|
|
449
|
+
info = get_today_info(CUSTOM_OCCASIONS_PATH)
|
|
450
|
+
use_fa = self.config.get("persian_digits", False)
|
|
451
|
+
self.taskbar_proxy.update_title(
|
|
452
|
+
format_digits(f"{info['time_str'][:5]} | {info['date_text']}", use_fa)
|
|
453
|
+
)
|
|
454
|
+
if getattr(self, "taskbar_clock", None) is not None:
|
|
455
|
+
try:
|
|
456
|
+
self.taskbar_clock.refresh(info)
|
|
457
|
+
except Exception:
|
|
458
|
+
pass
|
|
459
|
+
if getattr(self, "clock_replacer", None) is not None:
|
|
460
|
+
try:
|
|
461
|
+
self.clock_replacer.refresh(info)
|
|
462
|
+
except Exception:
|
|
463
|
+
pass
|
|
464
|
+
|
|
465
|
+
def restore_widget(self):
|
|
466
|
+
"""نمایش مجدد ویجت روی صفحه دسکتاپ"""
|
|
467
|
+
self._is_minimized = False
|
|
468
|
+
if self.widget_win:
|
|
469
|
+
self.widget_win.deiconify()
|
|
470
|
+
self.widget_win.lift()
|
|
471
|
+
if self.lock_win:
|
|
472
|
+
self.lock_win.deiconify()
|
|
473
|
+
self.lock_win.lift()
|
|
474
|
+
self._position_lock_window()
|
|
475
|
+
self._apply_all_styles()
|
|
476
|
+
self.sync_taskbar_features()
|
|
477
|
+
|
|
478
|
+
def on_taskbar_clicked(self):
|
|
479
|
+
"""کلیک روی ساعت تسکبار یا دکمه تسکبار: بازیابی از مینیمایز یا باز/بسته کردن تقویم"""
|
|
480
|
+
now_ts = time.monotonic()
|
|
481
|
+
if now_ts - getattr(self, "_last_tb_click_ts", 0.0) < 0.22:
|
|
482
|
+
return
|
|
483
|
+
self._last_tb_click_ts = now_ts
|
|
484
|
+
|
|
485
|
+
hide_desktop = (
|
|
486
|
+
bool(self.config.get("replace_windows_clock", False))
|
|
487
|
+
and bool(self.config.get("replace_clock_hide_widget", False))
|
|
488
|
+
)
|
|
489
|
+
if self._is_minimized:
|
|
490
|
+
if not hide_desktop:
|
|
491
|
+
self.restore_widget()
|
|
492
|
+
return
|
|
493
|
+
self._is_minimized = False
|
|
494
|
+
|
|
495
|
+
if not hide_desktop:
|
|
496
|
+
if self.widget_win and not self.widget_win.winfo_viewable():
|
|
497
|
+
self.widget_win.deiconify()
|
|
498
|
+
if self.lock_win and not self.lock_win.winfo_viewable() and self.config.get("locked"):
|
|
499
|
+
self.lock_win.deiconify()
|
|
500
|
+
|
|
501
|
+
self._toggle_calendar(None)
|
|
502
|
+
if self.config.get("calendar_expanded") and self.cal_win:
|
|
503
|
+
self.cal_win.lift()
|
|
504
|
+
|
|
505
|
+
def show_menu_at_pointer(self):
|
|
506
|
+
self._populate_context_menu()
|
|
507
|
+
x = self.root.winfo_pointerx()
|
|
508
|
+
y = self.root.winfo_pointery()
|
|
509
|
+
try:
|
|
510
|
+
self.menu.tk_popup(x, y)
|
|
511
|
+
finally:
|
|
512
|
+
self.menu.grab_release()
|
|
513
|
+
|
|
514
|
+
def _get_btn_base_bg(self) -> str:
|
|
515
|
+
return "#1E2638" if not self.config.get("transparent_bg", False) else "#141821"
|
|
516
|
+
|
|
517
|
+
def _attach_hover(
|
|
518
|
+
self,
|
|
519
|
+
widget: tk.Widget,
|
|
520
|
+
tooltip_text,
|
|
521
|
+
base_fg_fn,
|
|
522
|
+
hover_bg: str = "#0284C7",
|
|
523
|
+
hover_fg: str = "#FFFFFF",
|
|
524
|
+
base_bg_fn=None,
|
|
525
|
+
):
|
|
526
|
+
tip = HoverTooltip(
|
|
527
|
+
widget=widget,
|
|
528
|
+
text_or_fn=tooltip_text,
|
|
529
|
+
base_bg_fn=base_bg_fn or self._get_btn_base_bg,
|
|
530
|
+
base_fg_fn=base_fg_fn,
|
|
531
|
+
hover_bg=hover_bg,
|
|
532
|
+
hover_fg=hover_fg,
|
|
533
|
+
font_family=self.config.get("font_family", "Vazir"),
|
|
534
|
+
)
|
|
535
|
+
self._tooltips.append(tip)
|
|
536
|
+
|
|
537
|
+
def _format_tray_tooltip(self, info: dict) -> str:
|
|
538
|
+
base = f"{info['time_str']} | {info['date_text']}"
|
|
539
|
+
if info.get("has_occasion") and info.get("occasion"):
|
|
540
|
+
return f"{base} - {info['occasion']}"
|
|
541
|
+
return base
|
|
542
|
+
|
|
543
|
+
def _load_config(self) -> dict:
|
|
544
|
+
cfg = DEFAULT_CONFIG.copy()
|
|
545
|
+
if os.path.exists(CONFIG_PATH):
|
|
546
|
+
try:
|
|
547
|
+
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
|
|
548
|
+
saved = json.load(f)
|
|
549
|
+
if isinstance(saved, dict):
|
|
550
|
+
cfg.update(saved)
|
|
551
|
+
except Exception:
|
|
552
|
+
pass
|
|
553
|
+
return cfg
|
|
554
|
+
|
|
555
|
+
def _save_config(self):
|
|
556
|
+
try:
|
|
557
|
+
os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
|
|
558
|
+
with open(CONFIG_PATH, "w", encoding="utf-8") as f:
|
|
559
|
+
json.dump(self.config, f, ensure_ascii=False, indent=2)
|
|
560
|
+
except Exception:
|
|
561
|
+
pass
|
|
562
|
+
|
|
563
|
+
@property
|
|
564
|
+
def compact_w(self) -> int:
|
|
565
|
+
return self._computed_w
|
|
566
|
+
|
|
567
|
+
@property
|
|
568
|
+
def compact_h(self) -> int:
|
|
569
|
+
return self._computed_h
|
|
570
|
+
|
|
571
|
+
@property
|
|
572
|
+
def calendar_w(self) -> int:
|
|
573
|
+
scale = max(80, min(150, int(self.config.get("widget_scale", 100)))) / 100.0
|
|
574
|
+
return int(self.BASE_CALENDAR_W * scale)
|
|
575
|
+
|
|
576
|
+
@property
|
|
577
|
+
def calendar_h(self) -> int:
|
|
578
|
+
scale = max(80, min(150, int(self.config.get("widget_scale", 100)))) / 100.0
|
|
579
|
+
return int(self.BASE_CALENDAR_H * scale)
|
|
580
|
+
|
|
581
|
+
def _build_compact_ui(self):
|
|
582
|
+
self.widget_win = tk.Toplevel(self.root)
|
|
583
|
+
self.widget_win.title("ویجت ساعت و تقویم")
|
|
584
|
+
self.widget_win.overrideredirect(True)
|
|
585
|
+
|
|
586
|
+
bg = TRANSPARENT_KEY if self.config["transparent_bg"] else self.config["bg_color"]
|
|
587
|
+
self.widget_win.configure(bg=bg)
|
|
588
|
+
|
|
589
|
+
self.main_frame = tk.Frame(
|
|
590
|
+
self.widget_win,
|
|
591
|
+
bg=bg,
|
|
592
|
+
highlightbackground=self.config["border_color"] if not self.config["transparent_bg"] else bg,
|
|
593
|
+
highlightthickness=0 if self.config["transparent_bg"] else 1,
|
|
594
|
+
)
|
|
595
|
+
self.main_frame.pack(fill="both", expand=True)
|
|
596
|
+
|
|
597
|
+
self.canvas = tk.Canvas(
|
|
598
|
+
self.main_frame,
|
|
599
|
+
bg=bg,
|
|
600
|
+
highlightthickness=0,
|
|
601
|
+
bd=0,
|
|
602
|
+
)
|
|
603
|
+
self.canvas.pack(fill="both", expand=True)
|
|
604
|
+
|
|
605
|
+
self.btn_container = tk.Frame(self.canvas, bg=bg)
|
|
606
|
+
|
|
607
|
+
# ۱. دکمه باز/بسته کردن تقویم (Extend)
|
|
608
|
+
self.extend_btn = tk.Label(
|
|
609
|
+
self.btn_container,
|
|
610
|
+
text="◂ تقویم",
|
|
611
|
+
font=(self.config["font_family"], 8, "bold"),
|
|
612
|
+
fg=self.config["accent_color"],
|
|
613
|
+
bg="#1E2638",
|
|
614
|
+
padx=5,
|
|
615
|
+
pady=2,
|
|
616
|
+
cursor="hand2",
|
|
617
|
+
)
|
|
618
|
+
self.extend_btn.pack(side="left", padx=(0, 3))
|
|
619
|
+
self.extend_btn.bind("<Button-1>", self._toggle_calendar)
|
|
620
|
+
self.extend_btn.bind("<Double-Button-1>", lambda e: "break")
|
|
621
|
+
self._attach_hover(
|
|
622
|
+
self.extend_btn,
|
|
623
|
+
lambda: "بستن تقویم ماهانه" if self.config.get("calendar_expanded") else "نمایش تقویم ماهانه و مناسبتها",
|
|
624
|
+
base_fg_fn=lambda: self.config.get("accent_color", "#38BDF8"),
|
|
625
|
+
hover_bg="#0284C7",
|
|
626
|
+
hover_fg="#FFFFFF",
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
# ۲. دکمه کپی ۱۰ فرمت زمان برنامهنویسی برای API + GUID
|
|
630
|
+
self.copy_time_btn = tk.Label(
|
|
631
|
+
self.btn_container,
|
|
632
|
+
text="📋",
|
|
633
|
+
font=("Segoe UI Emoji", 8),
|
|
634
|
+
fg="#A7F3D0",
|
|
635
|
+
bg="#1E2638",
|
|
636
|
+
padx=5,
|
|
637
|
+
pady=2,
|
|
638
|
+
cursor="hand2",
|
|
639
|
+
)
|
|
640
|
+
self.copy_time_btn.pack(side="left", padx=(0, 3))
|
|
641
|
+
self.copy_time_btn.bind("<Button-1>", self._toggle_dev_popup_window)
|
|
642
|
+
self.copy_time_btn.bind("<Double-Button-1>", lambda e: "break")
|
|
643
|
+
self._attach_hover(
|
|
644
|
+
self.copy_time_btn,
|
|
645
|
+
"کپی زمان (۱۰ فرمت API: تاریخ+ساعت / تاریخ جدا / ساعت جدا) + GUID",
|
|
646
|
+
base_fg_fn=lambda: "#A7F3D0",
|
|
647
|
+
hover_bg="#059669",
|
|
648
|
+
hover_fg="#FFFFFF",
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
# ۳. دکمه اختصاصی رندر و کپی سریع GUID (UUID v4)
|
|
652
|
+
self.guid_btn = tk.Label(
|
|
653
|
+
self.btn_container,
|
|
654
|
+
text="GUID",
|
|
655
|
+
font=("Consolas", 8, "bold"),
|
|
656
|
+
fg="#C4B5FD",
|
|
657
|
+
bg="#1E2638",
|
|
658
|
+
padx=5,
|
|
659
|
+
pady=2,
|
|
660
|
+
cursor="hand2",
|
|
661
|
+
)
|
|
662
|
+
self.guid_btn.pack(side="left", padx=(0, 3))
|
|
663
|
+
self.guid_btn.bind("<Button-1>", self._quick_copy_guid)
|
|
664
|
+
self.guid_btn.bind("<Double-Button-1>", lambda e: "break")
|
|
665
|
+
self.guid_btn.bind("<Button-3>", self._toggle_dev_popup_window)
|
|
666
|
+
self._attach_hover(
|
|
667
|
+
self.guid_btn,
|
|
668
|
+
"کلیک چپ: رندر و کپی فوری GUID جدید | کلیک راست: منوی کامل",
|
|
669
|
+
base_fg_fn=lambda: "#C4B5FD",
|
|
670
|
+
hover_bg="#7C3AED",
|
|
671
|
+
hover_fg="#FFFFFF",
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
# ۴. دکمه مدیریت رویدادها و آلارمها
|
|
675
|
+
self.alarm_btn = tk.Label(
|
|
676
|
+
self.btn_container,
|
|
677
|
+
text="⏰",
|
|
678
|
+
font=("Segoe UI Emoji", 8),
|
|
679
|
+
fg="#FBBF24",
|
|
680
|
+
bg="#1E2638",
|
|
681
|
+
padx=5,
|
|
682
|
+
pady=2,
|
|
683
|
+
cursor="hand2",
|
|
684
|
+
)
|
|
685
|
+
self.alarm_btn.pack(side="left", padx=(0, 3))
|
|
686
|
+
self.alarm_btn.bind("<Button-1>", lambda e: (self.open_alarm_manager(), "break")[1])
|
|
687
|
+
self.alarm_btn.bind("<Double-Button-1>", lambda e: "break")
|
|
688
|
+
self._attach_hover(
|
|
689
|
+
self.alarm_btn,
|
|
690
|
+
"مدیریت رویدادها و آلارمها (تقویم چندانتخابی)",
|
|
691
|
+
base_fg_fn=lambda: "#FBBF24",
|
|
692
|
+
hover_bg="#D97706",
|
|
693
|
+
hover_fg="#FFFFFF",
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
# ۵. دکمه تنظیمات ظاهری
|
|
697
|
+
self.settings_btn = tk.Label(
|
|
698
|
+
self.btn_container,
|
|
699
|
+
text="⚙",
|
|
700
|
+
font=("Segoe UI", 8),
|
|
701
|
+
fg=self.config["date_color"],
|
|
702
|
+
bg="#1E2638",
|
|
703
|
+
padx=5,
|
|
704
|
+
pady=2,
|
|
705
|
+
cursor="hand2",
|
|
706
|
+
)
|
|
707
|
+
self.settings_btn.pack(side="left", padx=(0, 2))
|
|
708
|
+
self.settings_btn.bind("<Button-1>", lambda e: (self.open_settings_window(), "break")[1])
|
|
709
|
+
self.settings_btn.bind("<Double-Button-1>", lambda e: "break")
|
|
710
|
+
self._attach_hover(
|
|
711
|
+
self.settings_btn,
|
|
712
|
+
"تنظیمات ظاهری، رنگ، سایز و فونت",
|
|
713
|
+
base_fg_fn=lambda: self.config.get("date_color", "#CBD5E1"),
|
|
714
|
+
hover_bg="#475569",
|
|
715
|
+
hover_fg="#FFFFFF",
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
# ۶. دکمه مینیمایز (مخفی کردن از صفحه؛ فقط در تسکبار و Hidden Icons)
|
|
719
|
+
self.min_btn = tk.Label(
|
|
720
|
+
self.btn_container,
|
|
721
|
+
text="🗕",
|
|
722
|
+
font=("Segoe UI", 8, "bold"),
|
|
723
|
+
fg=self.config["date_color"],
|
|
724
|
+
bg="#1E2638",
|
|
725
|
+
padx=5,
|
|
726
|
+
pady=2,
|
|
727
|
+
cursor="hand2",
|
|
728
|
+
)
|
|
729
|
+
self.min_btn.pack(side="left")
|
|
730
|
+
self.min_btn.bind("<Button-1>", lambda e: (self.minimize_widget(), "break")[1])
|
|
731
|
+
self.min_btn.bind("<Double-Button-1>", lambda e: "break")
|
|
732
|
+
self._attach_hover(
|
|
733
|
+
self.min_btn,
|
|
734
|
+
"مینیمایز ویجت (نمایش فقط در تسکبار و Hidden Icons)",
|
|
735
|
+
base_fg_fn=lambda: self.config.get("date_color", "#CBD5E1"),
|
|
736
|
+
hover_bg="#475569",
|
|
737
|
+
hover_fg="#FFFFFF",
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
# ایجاد یکباره پنجره نوار ابزار روی Canvas تا در رفرش ثانیهای حذف نشود و Hover پایدار بماند
|
|
741
|
+
toolbar_x = self.LOCK_OFFSET_X + self.LOCK_BTN_SIZE + 8
|
|
742
|
+
toolbar_y = self.LOCK_OFFSET_Y
|
|
743
|
+
self._toolbar_win_id = self.canvas.create_window(
|
|
744
|
+
toolbar_x,
|
|
745
|
+
toolbar_y,
|
|
746
|
+
window=self.btn_container,
|
|
747
|
+
anchor="nw",
|
|
748
|
+
tags="toolbar",
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
# بایندینگ درگ و دابلکلیک فقط روی بوم و قاب اصلی (نه روی toplevel تا دکمههای نوار ابزار را نگیرد)
|
|
752
|
+
for w in (self.main_frame, self.canvas):
|
|
753
|
+
w.bind("<ButtonPress-1>", self._on_drag_start)
|
|
754
|
+
w.bind("<B1-Motion>", self._on_drag_motion)
|
|
755
|
+
w.bind("<ButtonRelease-1>", self._on_drag_end)
|
|
756
|
+
w.bind("<Button-3>", self._show_context_menu)
|
|
757
|
+
w.bind("<Double-Button-1>", self._toggle_calendar)
|
|
758
|
+
|
|
759
|
+
def _quick_copy_guid(self, event=None):
|
|
760
|
+
"""تولید و کپی فوری یک GUID (UUID v4) استاندارد با یک کلیک روی دکمه GUID"""
|
|
761
|
+
if self.config.get("locked", False):
|
|
762
|
+
return "break"
|
|
763
|
+
new_guid = str(uuid.uuid4())
|
|
764
|
+
self._copy_to_clipboard(new_guid, f"✓ GUID کپی شد!")
|
|
765
|
+
return "break"
|
|
766
|
+
|
|
767
|
+
def _build_lock_badge_window(self):
|
|
768
|
+
self.lock_win = tk.Toplevel(self.root)
|
|
769
|
+
self.lock_win.overrideredirect(True)
|
|
770
|
+
self.lock_win.configure(bg="#1E293B")
|
|
771
|
+
|
|
772
|
+
self.lock_frame = tk.Frame(
|
|
773
|
+
self.lock_win,
|
|
774
|
+
bg="#1E293B",
|
|
775
|
+
highlightbackground=self.config["border_color"],
|
|
776
|
+
highlightthickness=1,
|
|
777
|
+
)
|
|
778
|
+
self.lock_frame.pack(fill="both", expand=True)
|
|
779
|
+
|
|
780
|
+
self.lock_btn_label = tk.Label(
|
|
781
|
+
self.lock_frame,
|
|
782
|
+
text="🔒" if self.config.get("locked") else "🔓",
|
|
783
|
+
font=("Segoe UI Emoji", 8),
|
|
784
|
+
fg="#FBBF24" if self.config.get("locked") else self.config["accent_color"],
|
|
785
|
+
bg="#1E293B",
|
|
786
|
+
cursor="hand2",
|
|
787
|
+
)
|
|
788
|
+
self.lock_btn_label.pack(fill="both", expand=True)
|
|
789
|
+
|
|
790
|
+
self.lock_btn_label.bind("<Button-1>", lambda e: self._toggle_lock())
|
|
791
|
+
self.lock_frame.bind("<Button-1>", lambda e: self._toggle_lock())
|
|
792
|
+
self.lock_btn_label.bind("<Button-3>", self._on_lock_btn_right_click)
|
|
793
|
+
self._attach_hover(
|
|
794
|
+
self.lock_btn_label,
|
|
795
|
+
lambda: "باز کردن قفل ویجت (🔓)" if self.config.get("locked") else "قفل روی صفحه و عبوردهی کلیک (🔒)",
|
|
796
|
+
base_fg_fn=lambda: "#FBBF24" if self.config.get("locked") else self.config.get("accent_color", "#38BDF8"),
|
|
797
|
+
hover_bg="#334155",
|
|
798
|
+
hover_fg="#FDE047",
|
|
799
|
+
)
|
|
800
|
+
|
|
801
|
+
def _on_lock_btn_right_click(self, event):
|
|
802
|
+
if not self.config.get("locked", False):
|
|
803
|
+
self._show_context_menu(event)
|
|
804
|
+
|
|
805
|
+
def _position_lock_window(self):
|
|
806
|
+
if not self.lock_win or not self.widget_win:
|
|
807
|
+
return
|
|
808
|
+
wx = self.widget_win.winfo_x()
|
|
809
|
+
wy = self.widget_win.winfo_y()
|
|
810
|
+
sz = self.LOCK_BTN_SIZE
|
|
811
|
+
lx = wx + self.LOCK_OFFSET_X
|
|
812
|
+
ly = wy + self.LOCK_OFFSET_Y
|
|
813
|
+
self.lock_win.geometry(f"{sz}x{sz}+{int(lx)}+{int(ly)}")
|
|
814
|
+
|
|
815
|
+
def _get_dev_copy_data(self) -> dict:
|
|
816
|
+
"""محاسبه لحظهای ۱۰ فرمت پرکاربرد تاریخ و ساعت برای API + مقادیر GUID"""
|
|
817
|
+
now_utc = datetime.now(timezone.utc)
|
|
818
|
+
now_teh = now_utc.astimezone(TEHRAN_TZ)
|
|
819
|
+
jy, jm, jd = gregorian_to_jalali(now_teh.year, now_teh.month, now_teh.day)
|
|
820
|
+
|
|
821
|
+
ms_teh = now_teh.microsecond // 1000
|
|
822
|
+
ms_utc = now_utc.microsecond // 1000
|
|
823
|
+
|
|
824
|
+
fmt_1_utc_iso = f"{now_utc.strftime('%Y-%m-%dT%H:%M:%S')}.{ms_utc:03d}Z"
|
|
825
|
+
fmt_2_teh_iso = f"{now_teh.strftime('%Y-%m-%dT%H:%M:%S')}.{ms_teh:03d}+03:30"
|
|
826
|
+
fmt_3_sql_dt = f"{now_teh.strftime('%Y-%m-%d %H:%M:%S')}.{ms_teh:03d}"
|
|
827
|
+
fmt_4_jalali_dt = f"{jy:04d}/{jm:02d}/{jd:02d} {now_teh.strftime('%H:%M:%S')}.{ms_teh:03d}"
|
|
828
|
+
|
|
829
|
+
fmt_5_greg_date = now_teh.strftime("%Y-%m-%d")
|
|
830
|
+
fmt_6_jalali_date = f"{jy:04d}/{jm:02d}/{jd:02d}"
|
|
831
|
+
|
|
832
|
+
fmt_7_time_ms = f"{now_teh.strftime('%H:%M:%S')}.{ms_teh:03d}"
|
|
833
|
+
fmt_8_time_std = now_teh.strftime("%H:%M:%S")
|
|
834
|
+
|
|
835
|
+
unix_ms = int(now_utc.timestamp() * 1000)
|
|
836
|
+
unix_sec = int(now_utc.timestamp())
|
|
837
|
+
fmt_9_unix_ms = str(unix_ms)
|
|
838
|
+
fmt_10_unix_sec = str(unix_sec)
|
|
839
|
+
|
|
840
|
+
new_guid_lower = str(uuid.uuid4())
|
|
841
|
+
new_guid_upper = new_guid_lower.upper()
|
|
842
|
+
new_guid_no_dash = new_guid_lower.replace("-", "")
|
|
843
|
+
empty_guid = "00000000-0000-0000-0000-000000000000"
|
|
844
|
+
|
|
845
|
+
json_payload = json.dumps(
|
|
846
|
+
{
|
|
847
|
+
"utc_iso": fmt_1_utc_iso,
|
|
848
|
+
"tehran_iso": fmt_2_teh_iso,
|
|
849
|
+
"sql_datetime": fmt_3_sql_dt,
|
|
850
|
+
"jalali_datetime": fmt_4_jalali_dt,
|
|
851
|
+
"date_gregorian": fmt_5_greg_date,
|
|
852
|
+
"date_jalali": fmt_6_jalali_date,
|
|
853
|
+
"time_ms": fmt_7_time_ms,
|
|
854
|
+
"time_std": fmt_8_time_std,
|
|
855
|
+
"unix_ms": unix_ms,
|
|
856
|
+
"unix_sec": unix_sec,
|
|
857
|
+
"guid": new_guid_lower,
|
|
858
|
+
},
|
|
859
|
+
ensure_ascii=False,
|
|
860
|
+
)
|
|
861
|
+
|
|
862
|
+
return {
|
|
863
|
+
"fmt_1_utc_iso": fmt_1_utc_iso,
|
|
864
|
+
"fmt_2_teh_iso": fmt_2_teh_iso,
|
|
865
|
+
"fmt_3_sql_dt": fmt_3_sql_dt,
|
|
866
|
+
"fmt_4_jalali_dt": fmt_4_jalali_dt,
|
|
867
|
+
"fmt_5_greg_date": fmt_5_greg_date,
|
|
868
|
+
"fmt_6_jalali_date": fmt_6_jalali_date,
|
|
869
|
+
"fmt_7_time_ms": fmt_7_time_ms,
|
|
870
|
+
"fmt_8_time_std": fmt_8_time_std,
|
|
871
|
+
"fmt_9_unix_ms": fmt_9_unix_ms,
|
|
872
|
+
"fmt_10_unix_sec": fmt_10_unix_sec,
|
|
873
|
+
"new_guid_lower": new_guid_lower,
|
|
874
|
+
"new_guid_upper": new_guid_upper,
|
|
875
|
+
"new_guid_no_dash": new_guid_no_dash,
|
|
876
|
+
"empty_guid": empty_guid,
|
|
877
|
+
"json_payload": json_payload,
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
def _toggle_dev_popup_window(self, event=None):
|
|
881
|
+
"""
|
|
882
|
+
نمایش پنجره شناور اختصاصی کپی زمان برنامهنویسی (۱۰ فرمت API) و GUID
|
|
883
|
+
با فاصله استاندارد از ویجت و در بالاترین لایه (Topmost) تا هرگز زیر ویجت نرود.
|
|
884
|
+
"""
|
|
885
|
+
if self.config.get("locked", False):
|
|
886
|
+
return "break"
|
|
887
|
+
if self.dev_popup_win and self.dev_popup_win.winfo_exists():
|
|
888
|
+
self._close_dev_popup_window()
|
|
889
|
+
return "break"
|
|
890
|
+
# جلوگیری از باز شدن مجدد در همان کلیک وقتی FocusOut پنجره را تازه بسته است
|
|
891
|
+
if time.monotonic() - getattr(self, "_dev_popup_closed_ts", 0.0) < 0.28:
|
|
892
|
+
return "break"
|
|
893
|
+
|
|
894
|
+
d = self._get_dev_copy_data()
|
|
895
|
+
font_fa = self.config.get("font_family", "Vazir")
|
|
896
|
+
|
|
897
|
+
win = tk.Toplevel(self.root)
|
|
898
|
+
self.dev_popup_win = win
|
|
899
|
+
win.title("ابزار کپی زمان API و GUID")
|
|
900
|
+
win.overrideredirect(True)
|
|
901
|
+
win.attributes("-topmost", True)
|
|
902
|
+
win.configure(bg="#0F172A")
|
|
903
|
+
|
|
904
|
+
outer = tk.Frame(
|
|
905
|
+
win,
|
|
906
|
+
bg="#0F172A",
|
|
907
|
+
highlightbackground="#38BDF8",
|
|
908
|
+
highlightthickness=1,
|
|
909
|
+
padx=10,
|
|
910
|
+
pady=8,
|
|
911
|
+
)
|
|
912
|
+
outer.pack(fill="both", expand=True)
|
|
913
|
+
|
|
914
|
+
# هدر پنل
|
|
915
|
+
hdr = tk.Frame(outer, bg="#0F172A")
|
|
916
|
+
hdr.pack(fill="x", pady=(0, 6))
|
|
917
|
+
tk.Label(
|
|
918
|
+
hdr,
|
|
919
|
+
text="📋 کپی سریع تاریخ، ساعت API و تولید GUID",
|
|
920
|
+
font=(font_fa, 9, "bold"),
|
|
921
|
+
fg="#38BDF8",
|
|
922
|
+
bg="#0F172A",
|
|
923
|
+
).pack(side="right")
|
|
924
|
+
|
|
925
|
+
close_lbl = tk.Label(
|
|
926
|
+
hdr,
|
|
927
|
+
text="✕",
|
|
928
|
+
font=("Segoe UI", 9, "bold"),
|
|
929
|
+
fg="#94A3B8",
|
|
930
|
+
bg="#1E293B",
|
|
931
|
+
padx=6,
|
|
932
|
+
pady=1,
|
|
933
|
+
cursor="hand2",
|
|
934
|
+
)
|
|
935
|
+
close_lbl.pack(side="left")
|
|
936
|
+
close_lbl.bind("<Button-1>", lambda e: self._close_dev_popup_window())
|
|
937
|
+
close_lbl.bind("<Enter>", lambda e: close_lbl.configure(bg="#EF4444", fg="#FFFFFF"))
|
|
938
|
+
close_lbl.bind("<Leave>", lambda e: close_lbl.configure(bg="#1E293B", fg="#94A3B8"))
|
|
939
|
+
|
|
940
|
+
def add_section_title(txt: str):
|
|
941
|
+
tk.Label(
|
|
942
|
+
outer,
|
|
943
|
+
text=txt,
|
|
944
|
+
font=(font_fa, 8, "bold"),
|
|
945
|
+
fg="#FBBF24",
|
|
946
|
+
bg="#0F172A",
|
|
947
|
+
anchor="e",
|
|
948
|
+
).pack(fill="x", pady=(4, 2))
|
|
949
|
+
|
|
950
|
+
def add_copy_row(title_txt: str, value_txt: str, copy_fn_or_val, toast_msg: str = "✓ کپی شد!"):
|
|
951
|
+
row = tk.Frame(outer, bg="#1E293B", padx=6, pady=3, cursor="hand2")
|
|
952
|
+
row.pack(fill="x", pady=1)
|
|
953
|
+
|
|
954
|
+
val_lbl = tk.Label(
|
|
955
|
+
row,
|
|
956
|
+
text=value_txt,
|
|
957
|
+
font=("Consolas", 8, "bold"),
|
|
958
|
+
fg="#38BDF8",
|
|
959
|
+
bg="#1E293B",
|
|
960
|
+
anchor="w",
|
|
961
|
+
cursor="hand2",
|
|
962
|
+
)
|
|
963
|
+
val_lbl.pack(side="left")
|
|
964
|
+
|
|
965
|
+
t_lbl = tk.Label(
|
|
966
|
+
row,
|
|
967
|
+
text=title_txt,
|
|
968
|
+
font=(font_fa, 8),
|
|
969
|
+
fg="#E2E8F0",
|
|
970
|
+
bg="#1E293B",
|
|
971
|
+
anchor="e",
|
|
972
|
+
cursor="hand2",
|
|
973
|
+
)
|
|
974
|
+
t_lbl.pack(side="right")
|
|
975
|
+
|
|
976
|
+
def on_click(e=None):
|
|
977
|
+
val = copy_fn_or_val() if callable(copy_fn_or_val) else copy_fn_or_val
|
|
978
|
+
self._copy_to_clipboard(val, toast_msg)
|
|
979
|
+
self._close_dev_popup_window()
|
|
980
|
+
|
|
981
|
+
def on_enter(e=None):
|
|
982
|
+
row.configure(bg="#0284C7")
|
|
983
|
+
val_lbl.configure(bg="#0284C7", fg="#FFFFFF")
|
|
984
|
+
t_lbl.configure(bg="#0284C7", fg="#FFFFFF")
|
|
985
|
+
|
|
986
|
+
def on_leave(e=None):
|
|
987
|
+
row.configure(bg="#1E293B")
|
|
988
|
+
val_lbl.configure(bg="#1E293B", fg="#38BDF8")
|
|
989
|
+
t_lbl.configure(bg="#1E293B", fg="#E2E8F0")
|
|
990
|
+
|
|
991
|
+
for w in (row, val_lbl, t_lbl):
|
|
992
|
+
w.bind("<Button-1>", on_click)
|
|
993
|
+
w.bind("<Enter>", on_enter)
|
|
994
|
+
w.bind("<Leave>", on_leave)
|
|
995
|
+
|
|
996
|
+
# بخش اول: تولید و کپی GUID (UUID v4)
|
|
997
|
+
add_section_title("🔑 تولید و کپی GUID (UUID v4):")
|
|
998
|
+
add_copy_row("🎲 رندر GUID جدید (استاندارد)", d["new_guid_lower"], lambda: str(uuid.uuid4()), "✓ GUID کپی شد!")
|
|
999
|
+
add_copy_row("🎲 رندر GUID (حروف بزرگ)", d["new_guid_upper"], lambda: str(uuid.uuid4()).upper(), "✓ GUID (Upper) کپی شد!")
|
|
1000
|
+
add_copy_row("🎲 رندر GUID (بدون خط تیره)", d["new_guid_no_dash"], lambda: uuid.uuid4().hex, "✓ GUID (Hex) کپی شد!")
|
|
1001
|
+
add_copy_row("🎲 کپی Empty GUID (صفر)", d["empty_guid"], d["empty_guid"], "✓ Empty GUID کپی شد!")
|
|
1002
|
+
|
|
1003
|
+
# بخش دوم: ۱۰ فرمت پرکاربرد تاریخ و ساعت برای API
|
|
1004
|
+
add_section_title("📅+🕒 تاریخ و ساعت با هم (Date & Time - API):")
|
|
1005
|
+
add_copy_row("۱. ISO-8601 UTC (با میلیثانیه)", d["fmt_1_utc_iso"], d["fmt_1_utc_iso"])
|
|
1006
|
+
add_copy_row("۲. ISO-8601 تهران (با میلیثانیه)", d["fmt_2_teh_iso"], d["fmt_2_teh_iso"])
|
|
1007
|
+
add_copy_row("۳. SQL / API DateTime", d["fmt_3_sql_dt"], d["fmt_3_sql_dt"])
|
|
1008
|
+
add_copy_row("۴. تاریخ و ساعت شمسی (با ms)", d["fmt_4_jalali_dt"], d["fmt_4_jalali_dt"])
|
|
1009
|
+
|
|
1010
|
+
add_section_title("📅 فقط تاریخ جدا (Date Only):")
|
|
1011
|
+
add_copy_row("۵. فقط تاریخ میلادی (Gregorian)", d["fmt_5_greg_date"], d["fmt_5_greg_date"])
|
|
1012
|
+
add_copy_row("۶. فقط تاریخ شمسی (Jalali)", d["fmt_6_jalali_date"], d["fmt_6_jalali_date"])
|
|
1013
|
+
|
|
1014
|
+
add_section_title("🕒 فقط ساعت جدا (Time Only) و تایماستمپ:")
|
|
1015
|
+
add_copy_row("۷. فقط ساعت + میلیثانیه (تهران)", d["fmt_7_time_ms"], d["fmt_7_time_ms"])
|
|
1016
|
+
add_copy_row("۸. فقط ساعت استاندارد (HH:MM:SS)", d["fmt_8_time_std"], d["fmt_8_time_std"])
|
|
1017
|
+
add_copy_row("۹. Unix Timestamp (میلیثانیه ms)", d["fmt_9_unix_ms"], d["fmt_9_unix_ms"])
|
|
1018
|
+
add_copy_row("۱۰. Unix Timestamp (ثانیه sec)", d["fmt_10_unix_sec"], d["fmt_10_unix_sec"])
|
|
1019
|
+
|
|
1020
|
+
add_copy_row("📦 کپی همه موارد به صورت JSON", "{ ... }", d["json_payload"], "✓ JSON کپی شد!")
|
|
1021
|
+
|
|
1022
|
+
win.update_idletasks()
|
|
1023
|
+
pw = max(370, win.winfo_reqwidth())
|
|
1024
|
+
ph = win.winfo_reqheight()
|
|
1025
|
+
|
|
1026
|
+
wx = self.widget_win.winfo_x()
|
|
1027
|
+
wy = self.widget_win.winfo_y()
|
|
1028
|
+
ww = self.compact_w
|
|
1029
|
+
wh = self.compact_h
|
|
1030
|
+
screen_w = self.root.winfo_screenwidth()
|
|
1031
|
+
screen_h = self.root.winfo_screenheight()
|
|
1032
|
+
|
|
1033
|
+
# قرارگیری با فاصله ۱۲ پیکسلی در پایین یا بالای ویجت (بدون همپوشانی با خود ویجت)
|
|
1034
|
+
px = wx + ww - pw
|
|
1035
|
+
px = max(10, min(px, screen_w - pw - 10))
|
|
1036
|
+
if wy + wh + 12 + ph <= screen_h - 40:
|
|
1037
|
+
py = wy + wh + 12
|
|
1038
|
+
elif wy - ph - 12 >= 10:
|
|
1039
|
+
py = wy - ph - 12
|
|
1040
|
+
else:
|
|
1041
|
+
# در کنار ویجت با فاصله ۱۲ پیکسل
|
|
1042
|
+
if wx - pw - 12 >= 10:
|
|
1043
|
+
px = wx - pw - 12
|
|
1044
|
+
else:
|
|
1045
|
+
px = min(screen_w - pw - 10, wx + ww + 12)
|
|
1046
|
+
py = max(10, min(wy, screen_h - ph - 40))
|
|
1047
|
+
|
|
1048
|
+
win.geometry(f"{pw}x{ph}+{int(px)}+{int(py)}")
|
|
1049
|
+
hide_from_taskbar(win)
|
|
1050
|
+
apply_rounded_corners(win, pw, ph, radius=12, enabled=True)
|
|
1051
|
+
apply_z_order_and_clickthrough(win, "topmost", click_through=False)
|
|
1052
|
+
win.lift()
|
|
1053
|
+
win.bind("<Escape>", lambda e: self._close_dev_popup_window())
|
|
1054
|
+
win.after(60, lambda: win.winfo_exists() and win.focus_force())
|
|
1055
|
+
win.after(180, lambda: win.winfo_exists() and win.bind("<FocusOut>", self._on_dev_popup_focus_out))
|
|
1056
|
+
return "break"
|
|
1057
|
+
|
|
1058
|
+
def _on_dev_popup_focus_out(self, event=None):
|
|
1059
|
+
if self.dev_popup_win is None or not self.dev_popup_win.winfo_exists():
|
|
1060
|
+
return
|
|
1061
|
+
try:
|
|
1062
|
+
foc = self.root.focus_get()
|
|
1063
|
+
if foc is not None and str(foc).startswith(str(self.dev_popup_win)):
|
|
1064
|
+
return
|
|
1065
|
+
except Exception:
|
|
1066
|
+
pass
|
|
1067
|
+
self._dev_popup_closed_ts = time.monotonic()
|
|
1068
|
+
self._close_dev_popup_window()
|
|
1069
|
+
|
|
1070
|
+
def _close_dev_popup_window(self):
|
|
1071
|
+
if self.dev_popup_win is not None:
|
|
1072
|
+
self._dev_popup_closed_ts = time.monotonic()
|
|
1073
|
+
try:
|
|
1074
|
+
self.dev_popup_win.destroy()
|
|
1075
|
+
except Exception:
|
|
1076
|
+
pass
|
|
1077
|
+
self.dev_popup_win = None
|
|
1078
|
+
|
|
1079
|
+
def _build_dev_copy_menu(self):
|
|
1080
|
+
"""منوی آبشاری کپی زمان و GUID برای منوی راستکلیک System Tray"""
|
|
1081
|
+
self.copy_menu = tk.Menu(
|
|
1082
|
+
self.root,
|
|
1083
|
+
tearoff=0,
|
|
1084
|
+
bg="#0F172A",
|
|
1085
|
+
fg="#F8FAFC",
|
|
1086
|
+
activebackground="#0284C7",
|
|
1087
|
+
activeforeground="#FFFFFF",
|
|
1088
|
+
font=("Consolas", 9),
|
|
1089
|
+
)
|
|
1090
|
+
|
|
1091
|
+
def _populate_dev_copy_menu(self):
|
|
1092
|
+
self.copy_menu.delete(0, "end")
|
|
1093
|
+
d = self._get_dev_copy_data()
|
|
1094
|
+
|
|
1095
|
+
self.copy_menu.add_command(label="─── 🔑 GUID / UUID v4 Generator ───", state="disabled")
|
|
1096
|
+
self.copy_menu.add_command(
|
|
1097
|
+
label=f"🎲 Render & Copy GUID (v4): {d['new_guid_lower']}",
|
|
1098
|
+
command=lambda: self._copy_to_clipboard(str(uuid.uuid4()), "✓ GUID کپی شد!"),
|
|
1099
|
+
)
|
|
1100
|
+
self.copy_menu.add_command(
|
|
1101
|
+
label=f"🎲 Render GUID (UPPERCASE): {d['new_guid_upper']}",
|
|
1102
|
+
command=lambda: self._copy_to_clipboard(str(uuid.uuid4()).upper(), "✓ GUID (Upper) کپی شد!"),
|
|
1103
|
+
)
|
|
1104
|
+
self.copy_menu.add_command(
|
|
1105
|
+
label=f"🎲 Render GUID (No Hyphens): {d['new_guid_no_dash']}",
|
|
1106
|
+
command=lambda: self._copy_to_clipboard(uuid.uuid4().hex, "✓ GUID (Hex) کپی شد!"),
|
|
1107
|
+
)
|
|
1108
|
+
self.copy_menu.add_command(
|
|
1109
|
+
label=f"🎲 Copy Empty GUID (0000...): {d['empty_guid']}",
|
|
1110
|
+
command=lambda: self._copy_to_clipboard(d["empty_guid"], "✓ Empty GUID کپی شد!"),
|
|
1111
|
+
)
|
|
1112
|
+
|
|
1113
|
+
self.copy_menu.add_separator()
|
|
1114
|
+
self.copy_menu.add_command(label="─── 📅+🕒 Date & Time (API / ISO / DB) ───", state="disabled")
|
|
1115
|
+
for lbl, val in [
|
|
1116
|
+
(f"1. ISO-8601 UTC (ms): {d['fmt_1_utc_iso']}", d["fmt_1_utc_iso"]),
|
|
1117
|
+
(f"2. ISO-8601 Tehran (ms): {d['fmt_2_teh_iso']}", d["fmt_2_teh_iso"]),
|
|
1118
|
+
(f"3. SQL / API DateTime: {d['fmt_3_sql_dt']}", d["fmt_3_sql_dt"]),
|
|
1119
|
+
(f"4. Jalali DateTime (ms): {d['fmt_4_jalali_dt']}", d["fmt_4_jalali_dt"]),
|
|
1120
|
+
]:
|
|
1121
|
+
self.copy_menu.add_command(label=lbl, command=lambda v=val: self._copy_to_clipboard(v))
|
|
1122
|
+
|
|
1123
|
+
self.copy_menu.add_separator()
|
|
1124
|
+
self.copy_menu.add_command(label="─── 📅 Date Only (فقط تاریخ جدا) ───", state="disabled")
|
|
1125
|
+
for lbl, val in [
|
|
1126
|
+
(f"5. Date Only (Gregorian): {d['fmt_5_greg_date']}", d["fmt_5_greg_date"]),
|
|
1127
|
+
(f"6. Date Only (Jalali): {d['fmt_6_jalali_date']}", d["fmt_6_jalali_date"]),
|
|
1128
|
+
]:
|
|
1129
|
+
self.copy_menu.add_command(label=lbl, command=lambda v=val: self._copy_to_clipboard(v))
|
|
1130
|
+
|
|
1131
|
+
self.copy_menu.add_separator()
|
|
1132
|
+
self.copy_menu.add_command(label="─── 🕒 Time Only & Unix Epoch ───", state="disabled")
|
|
1133
|
+
for lbl, val in [
|
|
1134
|
+
(f"7. Time Only + ms (Teh): {d['fmt_7_time_ms']}", d["fmt_7_time_ms"]),
|
|
1135
|
+
(f"8. Time Only (HH:MM:SS): {d['fmt_8_time_std']}", d["fmt_8_time_std"]),
|
|
1136
|
+
(f"9. Unix Timestamp (ms): {d['fmt_9_unix_ms']}", d["fmt_9_unix_ms"]),
|
|
1137
|
+
(f"10. Unix Timestamp (sec): {d['fmt_10_unix_sec']}", d["fmt_10_unix_sec"]),
|
|
1138
|
+
]:
|
|
1139
|
+
self.copy_menu.add_command(label=lbl, command=lambda v=val: self._copy_to_clipboard(v))
|
|
1140
|
+
|
|
1141
|
+
self.copy_menu.add_separator()
|
|
1142
|
+
self.copy_menu.add_command(
|
|
1143
|
+
label="📦 Copy All as JSON Object",
|
|
1144
|
+
command=lambda v=d["json_payload"]: self._copy_to_clipboard(v, "✓ JSON کپی شد!"),
|
|
1145
|
+
)
|
|
1146
|
+
|
|
1147
|
+
def _copy_to_clipboard(self, text_val: str, toast_msg: str = "✓ کپی شد!"):
|
|
1148
|
+
self.root.clipboard_clear()
|
|
1149
|
+
self.root.clipboard_append(text_val)
|
|
1150
|
+
self.root.update()
|
|
1151
|
+
self._show_toast(toast_msg)
|
|
1152
|
+
|
|
1153
|
+
def _show_toast(self, msg: str):
|
|
1154
|
+
self._toast_text = msg
|
|
1155
|
+
self._refresh_display()
|
|
1156
|
+
|
|
1157
|
+
def _clear():
|
|
1158
|
+
self._toast_text = ""
|
|
1159
|
+
self._refresh_display()
|
|
1160
|
+
|
|
1161
|
+
self.root.after(1600, _clear)
|
|
1162
|
+
|
|
1163
|
+
def _build_calendar_window(self):
|
|
1164
|
+
self.cal_win = tk.Toplevel(self.root)
|
|
1165
|
+
self.cal_win.overrideredirect(True)
|
|
1166
|
+
bg = self.config["bg_color"]
|
|
1167
|
+
self.cal_win.configure(bg=bg)
|
|
1168
|
+
|
|
1169
|
+
self.cal_frame = tk.Frame(
|
|
1170
|
+
self.cal_win,
|
|
1171
|
+
bg=bg,
|
|
1172
|
+
highlightbackground=self.config["border_color"],
|
|
1173
|
+
highlightthickness=1,
|
|
1174
|
+
padx=12,
|
|
1175
|
+
pady=10,
|
|
1176
|
+
)
|
|
1177
|
+
self.cal_frame.pack(fill="both", expand=True)
|
|
1178
|
+
|
|
1179
|
+
self.cal_header = tk.Frame(self.cal_frame, bg=bg)
|
|
1180
|
+
self.cal_header.pack(fill="x", pady=(0, 6))
|
|
1181
|
+
|
|
1182
|
+
self.next_month_btn = tk.Label(
|
|
1183
|
+
self.cal_header,
|
|
1184
|
+
text="‹",
|
|
1185
|
+
font=("Segoe UI", 13, "bold"),
|
|
1186
|
+
fg=self.config["clock_color"],
|
|
1187
|
+
bg="#1E2638",
|
|
1188
|
+
padx=8,
|
|
1189
|
+
pady=0,
|
|
1190
|
+
cursor="hand2",
|
|
1191
|
+
)
|
|
1192
|
+
self.next_month_btn.pack(side="left")
|
|
1193
|
+
self.next_month_btn.bind("<Button-1>", lambda e: self._change_month(1))
|
|
1194
|
+
self._attach_hover(
|
|
1195
|
+
self.next_month_btn,
|
|
1196
|
+
"ماه بعد",
|
|
1197
|
+
base_fg_fn=lambda: self.config.get("clock_color", "#F8FAFC"),
|
|
1198
|
+
hover_bg="#0284C7",
|
|
1199
|
+
hover_fg="#FFFFFF",
|
|
1200
|
+
base_bg_fn=lambda: "#1E2638",
|
|
1201
|
+
)
|
|
1202
|
+
|
|
1203
|
+
self.today_btn = tk.Label(
|
|
1204
|
+
self.cal_header,
|
|
1205
|
+
text="امروز",
|
|
1206
|
+
font=(self.config["font_family"], 8, "bold"),
|
|
1207
|
+
fg=self.config["accent_color"],
|
|
1208
|
+
bg="#1E2638",
|
|
1209
|
+
padx=6,
|
|
1210
|
+
pady=2,
|
|
1211
|
+
cursor="hand2",
|
|
1212
|
+
)
|
|
1213
|
+
self.today_btn.pack(side="left", padx=(4, 0))
|
|
1214
|
+
self.today_btn.bind("<Button-1>", lambda e: self._go_to_today())
|
|
1215
|
+
self._attach_hover(
|
|
1216
|
+
self.today_btn,
|
|
1217
|
+
"بازگشت به تاریخ امروز",
|
|
1218
|
+
base_fg_fn=lambda: self.config.get("accent_color", "#38BDF8"),
|
|
1219
|
+
hover_bg="#0284C7",
|
|
1220
|
+
hover_fg="#FFFFFF",
|
|
1221
|
+
base_bg_fn=lambda: "#1E2638",
|
|
1222
|
+
)
|
|
1223
|
+
|
|
1224
|
+
self.prev_month_btn = tk.Label(
|
|
1225
|
+
self.cal_header,
|
|
1226
|
+
text="›",
|
|
1227
|
+
font=("Segoe UI", 13, "bold"),
|
|
1228
|
+
fg=self.config["clock_color"],
|
|
1229
|
+
bg="#1E2638",
|
|
1230
|
+
padx=8,
|
|
1231
|
+
pady=0,
|
|
1232
|
+
cursor="hand2",
|
|
1233
|
+
)
|
|
1234
|
+
self.prev_month_btn.pack(side="right")
|
|
1235
|
+
self.prev_month_btn.bind("<Button-1>", lambda e: self._change_month(-1))
|
|
1236
|
+
self._attach_hover(
|
|
1237
|
+
self.prev_month_btn,
|
|
1238
|
+
"ماه قبل",
|
|
1239
|
+
base_fg_fn=lambda: self.config.get("clock_color", "#F8FAFC"),
|
|
1240
|
+
hover_bg="#0284C7",
|
|
1241
|
+
hover_fg="#FFFFFF",
|
|
1242
|
+
base_bg_fn=lambda: "#1E2638",
|
|
1243
|
+
)
|
|
1244
|
+
|
|
1245
|
+
self.cal_title_canvas = tk.Canvas(
|
|
1246
|
+
self.cal_header,
|
|
1247
|
+
height=26,
|
|
1248
|
+
bg=bg,
|
|
1249
|
+
highlightthickness=0,
|
|
1250
|
+
)
|
|
1251
|
+
self.cal_title_canvas.pack(side="right", fill="x", expand=True)
|
|
1252
|
+
|
|
1253
|
+
self.week_header = tk.Frame(self.cal_frame, bg=bg)
|
|
1254
|
+
self.week_header.pack(fill="x", pady=(0, 4))
|
|
1255
|
+
self.week_canvases: list[tk.Canvas] = []
|
|
1256
|
+
for col_idx in range(7):
|
|
1257
|
+
c_canvas = tk.Canvas(
|
|
1258
|
+
self.week_header,
|
|
1259
|
+
width=36,
|
|
1260
|
+
height=22,
|
|
1261
|
+
bg=bg,
|
|
1262
|
+
highlightthickness=0,
|
|
1263
|
+
)
|
|
1264
|
+
c_canvas.grid(row=0, column=col_idx, sticky="nsew", padx=1)
|
|
1265
|
+
self.week_header.grid_columnconfigure(col_idx, weight=1)
|
|
1266
|
+
self.week_canvases.append(c_canvas)
|
|
1267
|
+
|
|
1268
|
+
self.days_grid_frame = tk.Frame(self.cal_frame, bg=bg)
|
|
1269
|
+
self.days_grid_frame.pack(fill="x", pady=(0, 6))
|
|
1270
|
+
|
|
1271
|
+
self.day_cells: list[tk.Canvas] = []
|
|
1272
|
+
for r in range(6):
|
|
1273
|
+
self.days_grid_frame.grid_rowconfigure(r, weight=1)
|
|
1274
|
+
for c in range(7):
|
|
1275
|
+
self.days_grid_frame.grid_columnconfigure(c, weight=1)
|
|
1276
|
+
cell = tk.Canvas(
|
|
1277
|
+
self.days_grid_frame,
|
|
1278
|
+
width=36,
|
|
1279
|
+
height=28,
|
|
1280
|
+
bg=bg,
|
|
1281
|
+
highlightthickness=0,
|
|
1282
|
+
cursor="hand2",
|
|
1283
|
+
)
|
|
1284
|
+
cell.grid(row=r, column=c, sticky="nsew", padx=1, pady=1)
|
|
1285
|
+
self.day_cells.append(cell)
|
|
1286
|
+
|
|
1287
|
+
self.cal_sep = tk.Frame(self.cal_frame, bg=self.config["border_color"], height=1)
|
|
1288
|
+
self.cal_sep.pack(fill="x", pady=(2, 6))
|
|
1289
|
+
|
|
1290
|
+
self.detail_canvas = tk.Canvas(
|
|
1291
|
+
self.cal_frame,
|
|
1292
|
+
height=74,
|
|
1293
|
+
bg=bg,
|
|
1294
|
+
highlightthickness=0,
|
|
1295
|
+
)
|
|
1296
|
+
self.detail_canvas.pack(fill="both", expand=True)
|
|
1297
|
+
|
|
1298
|
+
self.cal_bottom_bar = tk.Frame(self.cal_frame, bg=bg)
|
|
1299
|
+
self.cal_bottom_bar.pack(fill="x", pady=(4, 0))
|
|
1300
|
+
|
|
1301
|
+
self.add_event_btn = tk.Label(
|
|
1302
|
+
self.cal_bottom_bar,
|
|
1303
|
+
text="⏰ + آلارم / رویداد",
|
|
1304
|
+
font=(self.config["font_family"], 8, "bold"),
|
|
1305
|
+
fg="#FBBF24",
|
|
1306
|
+
bg="#1E2638",
|
|
1307
|
+
padx=7,
|
|
1308
|
+
pady=3,
|
|
1309
|
+
cursor="hand2",
|
|
1310
|
+
)
|
|
1311
|
+
self.add_event_btn.pack(side="right")
|
|
1312
|
+
self.add_event_btn.bind("<Button-1>", lambda e: self.open_alarm_manager())
|
|
1313
|
+
self._attach_hover(
|
|
1314
|
+
self.add_event_btn,
|
|
1315
|
+
"افزودن یا ویرایش آلارم و رویداد با انتخاب چندروزه",
|
|
1316
|
+
base_fg_fn=lambda: "#FBBF24",
|
|
1317
|
+
hover_bg="#D97706",
|
|
1318
|
+
hover_fg="#FFFFFF",
|
|
1319
|
+
base_bg_fn=lambda: "#1E2638",
|
|
1320
|
+
)
|
|
1321
|
+
|
|
1322
|
+
self.add_occasion_btn = tk.Label(
|
|
1323
|
+
self.cal_bottom_bar,
|
|
1324
|
+
text="🎂 + مناسبت / تولد",
|
|
1325
|
+
font=(self.config["font_family"], 8, "bold"),
|
|
1326
|
+
fg="#F472B6",
|
|
1327
|
+
bg="#1E2638",
|
|
1328
|
+
padx=7,
|
|
1329
|
+
pady=3,
|
|
1330
|
+
cursor="hand2",
|
|
1331
|
+
)
|
|
1332
|
+
self.add_occasion_btn.pack(side="right", padx=(0, 6))
|
|
1333
|
+
self.add_occasion_btn.bind("<Button-1>", lambda e: self.open_custom_occasions_manager())
|
|
1334
|
+
self._attach_hover(
|
|
1335
|
+
self.add_occasion_btn,
|
|
1336
|
+
"ثبت تولد دوستان و مناسبتهای شخصی روی تقویم + آلارم روز قبل",
|
|
1337
|
+
base_fg_fn=lambda: "#F472B6",
|
|
1338
|
+
hover_bg="#DB2777",
|
|
1339
|
+
hover_fg="#FFFFFF",
|
|
1340
|
+
base_bg_fn=lambda: "#1E2638",
|
|
1341
|
+
)
|
|
1342
|
+
|
|
1343
|
+
self.cal_copyright_lbl = tk.Label(
|
|
1344
|
+
self.cal_bottom_bar,
|
|
1345
|
+
text="® Shayan Bamyar",
|
|
1346
|
+
font=("Segoe UI", 7, "bold"),
|
|
1347
|
+
fg="#64748B",
|
|
1348
|
+
bg=bg,
|
|
1349
|
+
)
|
|
1350
|
+
self.cal_copyright_lbl.pack(side="left")
|
|
1351
|
+
|
|
1352
|
+
self.cal_win.bind("<Button-3>", self._show_context_menu)
|
|
1353
|
+
|
|
1354
|
+
if not self.config.get("calendar_expanded", False):
|
|
1355
|
+
self.cal_win.withdraw()
|
|
1356
|
+
|
|
1357
|
+
def _render_calendar_month(self):
|
|
1358
|
+
month_data = get_month_calendar(self.view_jy, self.view_jm, CUSTOM_OCCASIONS_PATH)
|
|
1359
|
+
use_fa = self.config.get("persian_digits", False)
|
|
1360
|
+
bg = self.config["bg_color"]
|
|
1361
|
+
font_fa = self.config.get("font_family", "Vazir")
|
|
1362
|
+
|
|
1363
|
+
self.cal_title_canvas.configure(bg=bg)
|
|
1364
|
+
self.cal_title_canvas.delete("all")
|
|
1365
|
+
self.cal_title_canvas.update_idletasks()
|
|
1366
|
+
tw = max(140, self.cal_title_canvas.winfo_width())
|
|
1367
|
+
draw_outlined_canvas_text(
|
|
1368
|
+
self.cal_title_canvas,
|
|
1369
|
+
tw // 2,
|
|
1370
|
+
13,
|
|
1371
|
+
text=format_digits(month_data["header_title"], use_fa),
|
|
1372
|
+
font_spec=(font_fa, 10, "bold"),
|
|
1373
|
+
fill=self.config["clock_color"],
|
|
1374
|
+
outline="#000000",
|
|
1375
|
+
)
|
|
1376
|
+
|
|
1377
|
+
for col_idx, w_canvas in enumerate(self.week_canvases):
|
|
1378
|
+
p_idx = 6 - col_idx
|
|
1379
|
+
day_short = PERSIAN_WEEK_SHORT[p_idx]
|
|
1380
|
+
fg_color = self.config["holiday_color"] if p_idx == 6 else self.config["occasion_color"]
|
|
1381
|
+
w_canvas.configure(bg=bg)
|
|
1382
|
+
w_canvas.delete("all")
|
|
1383
|
+
draw_outlined_canvas_text(
|
|
1384
|
+
w_canvas,
|
|
1385
|
+
18,
|
|
1386
|
+
11,
|
|
1387
|
+
text=day_short,
|
|
1388
|
+
font_spec=(font_fa, 8, "bold"),
|
|
1389
|
+
fill=fg_color,
|
|
1390
|
+
outline="#000000",
|
|
1391
|
+
)
|
|
1392
|
+
|
|
1393
|
+
for cell in self.day_cells:
|
|
1394
|
+
cell.configure(bg=bg, cursor="arrow")
|
|
1395
|
+
cell.delete("all")
|
|
1396
|
+
cell.unbind("<Button-1>")
|
|
1397
|
+
cell.unbind("<Enter>")
|
|
1398
|
+
cell.unbind("<Leave>")
|
|
1399
|
+
|
|
1400
|
+
first_week_idx = month_data["first_day_week_idx"]
|
|
1401
|
+
|
|
1402
|
+
for day_info in month_data["days"]:
|
|
1403
|
+
d = day_info["jd"]
|
|
1404
|
+
offset = first_week_idx + (d - 1)
|
|
1405
|
+
row = offset // 7
|
|
1406
|
+
persian_col = offset % 7
|
|
1407
|
+
grid_col = 6 - persian_col
|
|
1408
|
+
cell_idx = row * 7 + grid_col
|
|
1409
|
+
if cell_idx >= len(self.day_cells):
|
|
1410
|
+
continue
|
|
1411
|
+
|
|
1412
|
+
cell = self.day_cells[cell_idx]
|
|
1413
|
+
day_str = format_digits(str(d), use_fa)
|
|
1414
|
+
|
|
1415
|
+
user_alarms = self.alarm_store.get_events_for_jalali_day(
|
|
1416
|
+
day_info["jy"], day_info["jm"], day_info["jd"], day_info["persian_week_idx"]
|
|
1417
|
+
)
|
|
1418
|
+
has_any_event = day_info["has_occasion"] or len(user_alarms) > 0
|
|
1419
|
+
|
|
1420
|
+
is_today = (
|
|
1421
|
+
day_info["jy"] == self.current_jy
|
|
1422
|
+
and day_info["jm"] == self.current_jm
|
|
1423
|
+
and day_info["jd"] == self.current_jd
|
|
1424
|
+
)
|
|
1425
|
+
is_selected = (
|
|
1426
|
+
self.selected_day_info
|
|
1427
|
+
and day_info["jy"] == self.selected_day_info["jy"]
|
|
1428
|
+
and day_info["jm"] == self.selected_day_info["jm"]
|
|
1429
|
+
and day_info["jd"] == self.selected_day_info["jd"]
|
|
1430
|
+
)
|
|
1431
|
+
|
|
1432
|
+
if is_today:
|
|
1433
|
+
cell_bg = "#0369A1"
|
|
1434
|
+
cell_fg = "#FFFFFF"
|
|
1435
|
+
elif is_selected:
|
|
1436
|
+
cell_bg = "#334155"
|
|
1437
|
+
cell_fg = self.config["holiday_color"] if day_info["is_holiday"] else "#FFFFFF"
|
|
1438
|
+
else:
|
|
1439
|
+
cell_bg = bg
|
|
1440
|
+
cell_fg = self.config["holiday_color"] if day_info["is_holiday"] else self.config["clock_color"]
|
|
1441
|
+
|
|
1442
|
+
cell.configure(bg=cell_bg, cursor="hand2")
|
|
1443
|
+
draw_outlined_canvas_text(
|
|
1444
|
+
cell,
|
|
1445
|
+
18,
|
|
1446
|
+
12,
|
|
1447
|
+
text=day_str,
|
|
1448
|
+
font_spec=(font_fa, 9, "bold" if (is_today or is_selected) else "normal"),
|
|
1449
|
+
fill=cell_fg,
|
|
1450
|
+
outline="#000000",
|
|
1451
|
+
)
|
|
1452
|
+
if has_any_event:
|
|
1453
|
+
dot_color = "#FBBF24" if user_alarms else self.config["accent_color"]
|
|
1454
|
+
cell.create_oval(16, 22, 20, 26, fill=dot_color, outline="#000000")
|
|
1455
|
+
|
|
1456
|
+
cell.bind("<Button-1>", lambda e, info=day_info: self._on_day_clicked(info))
|
|
1457
|
+
if not is_today and not is_selected:
|
|
1458
|
+
cell.bind("<Enter>", lambda e, c=cell: c.configure(bg="#1E293B"))
|
|
1459
|
+
cell.bind("<Leave>", lambda e, c=cell, b=cell_bg: c.configure(bg=b))
|
|
1460
|
+
|
|
1461
|
+
self._update_selected_day_panel()
|
|
1462
|
+
|
|
1463
|
+
def _on_day_clicked(self, day_info: dict):
|
|
1464
|
+
if self.config.get("locked", False):
|
|
1465
|
+
return
|
|
1466
|
+
self.selected_day_info = day_info
|
|
1467
|
+
self._render_calendar_month()
|
|
1468
|
+
|
|
1469
|
+
def _update_selected_day_panel(self):
|
|
1470
|
+
info = self.selected_day_info
|
|
1471
|
+
if not info:
|
|
1472
|
+
return
|
|
1473
|
+
use_fa = self.config.get("persian_digits", False)
|
|
1474
|
+
bg = self.config["bg_color"]
|
|
1475
|
+
font_fa = self.config.get("font_family", "Vazir")
|
|
1476
|
+
|
|
1477
|
+
self.detail_canvas.configure(bg=bg)
|
|
1478
|
+
self.detail_canvas.delete("all")
|
|
1479
|
+
cw = self.calendar_w - 28
|
|
1480
|
+
|
|
1481
|
+
status_badge = " (تعطیل)" if info["is_holiday"] else ""
|
|
1482
|
+
header_text = format_digits(f"{info['date_text']}{status_badge}", use_fa)
|
|
1483
|
+
|
|
1484
|
+
draw_outlined_canvas_text(
|
|
1485
|
+
self.detail_canvas,
|
|
1486
|
+
cw - 4,
|
|
1487
|
+
4,
|
|
1488
|
+
text=header_text,
|
|
1489
|
+
font_spec=(font_fa, 9, "bold"),
|
|
1490
|
+
fill=self.config["holiday_color"] if info["is_holiday"] else self.config["accent_color"],
|
|
1491
|
+
outline="#000000",
|
|
1492
|
+
anchor="ne",
|
|
1493
|
+
justify="right",
|
|
1494
|
+
)
|
|
1495
|
+
|
|
1496
|
+
lines = []
|
|
1497
|
+
for title, is_hol in info.get("events", []):
|
|
1498
|
+
prefix = "🔴 " if is_hol else "• "
|
|
1499
|
+
lines.append(f"{prefix}{title}")
|
|
1500
|
+
|
|
1501
|
+
user_alarms = self.alarm_store.get_events_for_jalali_day(
|
|
1502
|
+
info["jy"], info["jm"], info["jd"], info["persian_week_idx"]
|
|
1503
|
+
)
|
|
1504
|
+
for a in user_alarms:
|
|
1505
|
+
t_str = f"{int(a.get('hour', 0)):02d}:{int(a.get('minute', 0)):02d}"
|
|
1506
|
+
lines.append(f"⏰ {t_str} - {a.get('title', '')}")
|
|
1507
|
+
|
|
1508
|
+
if lines:
|
|
1509
|
+
occ_text = format_digits("\n".join(lines[:3]), use_fa)
|
|
1510
|
+
draw_outlined_canvas_text(
|
|
1511
|
+
self.detail_canvas,
|
|
1512
|
+
cw - 4,
|
|
1513
|
+
26,
|
|
1514
|
+
text=occ_text,
|
|
1515
|
+
font_spec=(font_fa, 8),
|
|
1516
|
+
fill=self.config["holiday_color"] if (info["is_holiday"] and info["has_occasion"]) else self.config["date_color"],
|
|
1517
|
+
outline="#000000",
|
|
1518
|
+
anchor="ne",
|
|
1519
|
+
justify="right",
|
|
1520
|
+
width=cw - 8,
|
|
1521
|
+
)
|
|
1522
|
+
|
|
1523
|
+
def _change_month(self, delta: int):
|
|
1524
|
+
if self.config.get("locked", False):
|
|
1525
|
+
return
|
|
1526
|
+
new_jm = self.view_jm + delta
|
|
1527
|
+
new_jy = self.view_jy
|
|
1528
|
+
if new_jm > 12:
|
|
1529
|
+
new_jm = 1
|
|
1530
|
+
new_jy += 1
|
|
1531
|
+
elif new_jm < 1:
|
|
1532
|
+
new_jm = 12
|
|
1533
|
+
new_jy -= 1
|
|
1534
|
+
self.view_jy = new_jy
|
|
1535
|
+
self.view_jm = new_jm
|
|
1536
|
+
month_data = get_month_calendar(self.view_jy, self.view_jm, CUSTOM_OCCASIONS_PATH)
|
|
1537
|
+
if self.view_jy == self.current_jy and self.view_jm == self.current_jm:
|
|
1538
|
+
for d_info in month_data["days"]:
|
|
1539
|
+
if d_info["jd"] == self.current_jd:
|
|
1540
|
+
self.selected_day_info = d_info
|
|
1541
|
+
break
|
|
1542
|
+
else:
|
|
1543
|
+
self.selected_day_info = month_data["days"][0]
|
|
1544
|
+
self._render_calendar_month()
|
|
1545
|
+
|
|
1546
|
+
def _go_to_today(self):
|
|
1547
|
+
if self.config.get("locked", False):
|
|
1548
|
+
return
|
|
1549
|
+
today = get_today_info(CUSTOM_OCCASIONS_PATH)
|
|
1550
|
+
self.view_jy = today["jy"]
|
|
1551
|
+
self.view_jm = today["jm"]
|
|
1552
|
+
self.selected_day_info = today
|
|
1553
|
+
self._render_calendar_month()
|
|
1554
|
+
|
|
1555
|
+
def _toggle_calendar(self, event=None):
|
|
1556
|
+
if event is not None and self.config.get("locked", False):
|
|
1557
|
+
return "break"
|
|
1558
|
+
expanded = not self.config.get("calendar_expanded", False)
|
|
1559
|
+
self.config["calendar_expanded"] = expanded
|
|
1560
|
+
self._save_config()
|
|
1561
|
+
self._sync_calendar_visibility()
|
|
1562
|
+
return "break"
|
|
1563
|
+
|
|
1564
|
+
def _sync_calendar_visibility(self):
|
|
1565
|
+
expanded = self.config.get("calendar_expanded", False)
|
|
1566
|
+
is_locked = bool(self.config.get("locked", False))
|
|
1567
|
+
z_mode = self.config.get("z_mode", "topmost")
|
|
1568
|
+
|
|
1569
|
+
if expanded:
|
|
1570
|
+
today = get_today_info(CUSTOM_OCCASIONS_PATH)
|
|
1571
|
+
self.view_jy = today["jy"]
|
|
1572
|
+
self.view_jm = today["jm"]
|
|
1573
|
+
if not self.selected_day_info:
|
|
1574
|
+
self.selected_day_info = today
|
|
1575
|
+
self._position_calendar_window()
|
|
1576
|
+
self.cal_win.deiconify()
|
|
1577
|
+
self._render_calendar_month()
|
|
1578
|
+
hide_from_taskbar(self.cal_win)
|
|
1579
|
+
apply_rounded_corners(self.cal_win, self.calendar_w, self.calendar_h, radius=16, enabled=True)
|
|
1580
|
+
cal_alpha = max(0.85, float(self.config.get("opacity", 0.90)))
|
|
1581
|
+
if is_locked and cal_alpha >= 1.0:
|
|
1582
|
+
cal_alpha = 0.99
|
|
1583
|
+
self.cal_win.attributes("-alpha", cal_alpha)
|
|
1584
|
+
apply_z_order_and_clickthrough(self.cal_win, z_mode, click_through=is_locked)
|
|
1585
|
+
self.extend_btn.configure(text="▸ بستن")
|
|
1586
|
+
else:
|
|
1587
|
+
if self.cal_win:
|
|
1588
|
+
self.cal_win.withdraw()
|
|
1589
|
+
self.extend_btn.configure(text="◂ تقویم")
|
|
1590
|
+
|
|
1591
|
+
def _position_calendar_window(self):
|
|
1592
|
+
if not self.cal_win or not self.widget_win:
|
|
1593
|
+
return
|
|
1594
|
+
screen_w = self.root.winfo_screenwidth()
|
|
1595
|
+
screen_h = self.root.winfo_screenheight()
|
|
1596
|
+
cw, ch = self.calendar_w, self.calendar_h
|
|
1597
|
+
|
|
1598
|
+
# در صورتی که ویجت دسکتاپ مخفی است (نمایش فقط در تسکبار)، تقویم دقیقاً بالای ساعت گوشه تسکبار باز شود
|
|
1599
|
+
hide_desktop = (
|
|
1600
|
+
bool(self.config.get("replace_windows_clock", False))
|
|
1601
|
+
and bool(self.config.get("replace_clock_hide_widget", False))
|
|
1602
|
+
)
|
|
1603
|
+
if hide_desktop or not self.widget_win.winfo_viewable():
|
|
1604
|
+
cx = screen_w - cw - 12
|
|
1605
|
+
cy = screen_h - ch - 54
|
|
1606
|
+
self.cal_win.geometry(f"{cw}x{ch}+{int(cx)}+{int(cy)}")
|
|
1607
|
+
return
|
|
1608
|
+
|
|
1609
|
+
wx = self.widget_win.winfo_x()
|
|
1610
|
+
wy = self.widget_win.winfo_y()
|
|
1611
|
+
ww, wh = self.compact_w, self.compact_h
|
|
1612
|
+
|
|
1613
|
+
mode = self.config.get("calendar_position", "beside")
|
|
1614
|
+
if mode == "below":
|
|
1615
|
+
cx = wx + ww - cw
|
|
1616
|
+
cy = wy + wh + self.GAP
|
|
1617
|
+
if cy + ch > screen_h - 40:
|
|
1618
|
+
cy = wy - ch - self.GAP
|
|
1619
|
+
else:
|
|
1620
|
+
if wx - cw - self.GAP >= 10:
|
|
1621
|
+
cx = wx - cw - self.GAP
|
|
1622
|
+
else:
|
|
1623
|
+
cx = wx + ww + self.GAP
|
|
1624
|
+
cy = wy
|
|
1625
|
+
if cy + ch > screen_h - 48:
|
|
1626
|
+
cy = max(10, screen_h - ch - 48)
|
|
1627
|
+
|
|
1628
|
+
cx = max(8, min(cx, screen_w - cw - 8))
|
|
1629
|
+
cy = max(8, min(cy, screen_h - ch - 8))
|
|
1630
|
+
self.cal_win.geometry(f"{cw}x{ch}+{int(cx)}+{int(cy)}")
|
|
1631
|
+
|
|
1632
|
+
def open_alarm_manager(self):
|
|
1633
|
+
if self.alarm_win and self.alarm_win.win and self.alarm_win.win.winfo_exists():
|
|
1634
|
+
self.alarm_win.win.deiconify()
|
|
1635
|
+
self.alarm_win.win.lift()
|
|
1636
|
+
self.alarm_win.win.focus_force()
|
|
1637
|
+
return
|
|
1638
|
+
|
|
1639
|
+
sel = self.selected_day_info or get_today_info(CUSTOM_OCCASIONS_PATH)
|
|
1640
|
+
init_jalali = (sel["jy"], sel["jm"], sel["jd"])
|
|
1641
|
+
self.alarm_win = AlarmManagerWindow(
|
|
1642
|
+
root=self.root,
|
|
1643
|
+
store=self.alarm_store,
|
|
1644
|
+
font_family=self.config.get("font_family", "Vazir"),
|
|
1645
|
+
initial_jalali=init_jalali,
|
|
1646
|
+
on_change_callback=self._on_alarms_updated,
|
|
1647
|
+
)
|
|
1648
|
+
|
|
1649
|
+
def _load_custom_occasions_dict(self) -> dict:
|
|
1650
|
+
if os.path.exists(CUSTOM_OCCASIONS_PATH):
|
|
1651
|
+
try:
|
|
1652
|
+
with open(CUSTOM_OCCASIONS_PATH, "r", encoding="utf-8") as f:
|
|
1653
|
+
data = json.load(f)
|
|
1654
|
+
if isinstance(data, dict):
|
|
1655
|
+
return data
|
|
1656
|
+
except Exception:
|
|
1657
|
+
pass
|
|
1658
|
+
return {}
|
|
1659
|
+
|
|
1660
|
+
def _save_custom_occasions_dict(self, data: dict):
|
|
1661
|
+
os.makedirs(os.path.dirname(CUSTOM_OCCASIONS_PATH), exist_ok=True)
|
|
1662
|
+
with open(CUSTOM_OCCASIONS_PATH, "w", encoding="utf-8") as f:
|
|
1663
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
1664
|
+
|
|
1665
|
+
def open_custom_occasions_manager(self):
|
|
1666
|
+
"""پنجره مدیریت مناسبتهای شخصی و تولد دوستان + ثبت خودکار آلارم یادآور ۱ روز قبل"""
|
|
1667
|
+
if self.custom_occ_win and self.custom_occ_win.winfo_exists():
|
|
1668
|
+
self.custom_occ_win.deiconify()
|
|
1669
|
+
self.custom_occ_win.lift()
|
|
1670
|
+
self.custom_occ_win.focus_force()
|
|
1671
|
+
return
|
|
1672
|
+
|
|
1673
|
+
font_fa = self.config.get("font_family", "Vazir")
|
|
1674
|
+
sel = self.selected_day_info or get_today_info(CUSTOM_OCCASIONS_PATH)
|
|
1675
|
+
init_m = int(sel.get("jm", 1))
|
|
1676
|
+
init_d = int(sel.get("jd", 1))
|
|
1677
|
+
init_y = int(sel.get("jy", 1405))
|
|
1678
|
+
|
|
1679
|
+
win = tk.Toplevel(self.root)
|
|
1680
|
+
self.custom_occ_win = win
|
|
1681
|
+
win.title("🎂 مدیریت مناسبتهای شخصی و تولدها")
|
|
1682
|
+
win.configure(bg="#0F172A")
|
|
1683
|
+
win.attributes("-topmost", True)
|
|
1684
|
+
w, h = 470, 520
|
|
1685
|
+
sx = (win.winfo_screenwidth() - w) // 2
|
|
1686
|
+
sy = (win.winfo_screenheight() - h) // 2
|
|
1687
|
+
win.geometry(f"{w}x{h}+{sx}+{sy}")
|
|
1688
|
+
win.resizable(False, False)
|
|
1689
|
+
|
|
1690
|
+
container = tk.Frame(win, bg="#0F172A", padx=14, pady=12)
|
|
1691
|
+
container.pack(fill="both", expand=True)
|
|
1692
|
+
|
|
1693
|
+
tk.Label(
|
|
1694
|
+
container,
|
|
1695
|
+
text="🎂 ثبت روز تولد دوستان و مناسبتهای شخصی در تقویم",
|
|
1696
|
+
font=(font_fa, 10, "bold"),
|
|
1697
|
+
fg="#F472B6",
|
|
1698
|
+
bg="#0F172A",
|
|
1699
|
+
).pack(anchor="e", pady=(0, 8))
|
|
1700
|
+
|
|
1701
|
+
form = tk.LabelFrame(
|
|
1702
|
+
container,
|
|
1703
|
+
text=" افزودن مناسبت جدید ",
|
|
1704
|
+
font=(font_fa, 9, "bold"),
|
|
1705
|
+
fg="#E2E8F0",
|
|
1706
|
+
bg="#1E293B",
|
|
1707
|
+
bd=1,
|
|
1708
|
+
padx=10,
|
|
1709
|
+
pady=8,
|
|
1710
|
+
)
|
|
1711
|
+
form.pack(fill="x", pady=(0, 10))
|
|
1712
|
+
|
|
1713
|
+
# عنوان مناسبت
|
|
1714
|
+
row1 = tk.Frame(form, bg="#1E293B")
|
|
1715
|
+
row1.pack(fill="x", pady=3)
|
|
1716
|
+
tk.Label(row1, text="عنوان:", font=(font_fa, 9), fg="#CBD5E1", bg="#1E293B").pack(side="right", padx=(6, 0))
|
|
1717
|
+
title_entry = tk.Entry(
|
|
1718
|
+
row1,
|
|
1719
|
+
font=(font_fa, 9),
|
|
1720
|
+
bg="#0F172A",
|
|
1721
|
+
fg="#F8FAFC",
|
|
1722
|
+
insertbackground="#F8FAFC",
|
|
1723
|
+
relief="flat",
|
|
1724
|
+
justify="right",
|
|
1725
|
+
)
|
|
1726
|
+
title_entry.pack(side="right", fill="x", expand=True, ipady=4, padx=(0, 6))
|
|
1727
|
+
title_entry.focus_set()
|
|
1728
|
+
|
|
1729
|
+
# نوع مناسبت
|
|
1730
|
+
row2 = tk.Frame(form, bg="#1E293B")
|
|
1731
|
+
row2.pack(fill="x", pady=4)
|
|
1732
|
+
kind_var = tk.StringVar(value="birthday")
|
|
1733
|
+
holiday_var = tk.BooleanVar(value=False)
|
|
1734
|
+
prev_alarm_var = tk.BooleanVar(value=True)
|
|
1735
|
+
|
|
1736
|
+
def on_kind_change():
|
|
1737
|
+
if kind_var.get() == "birthday":
|
|
1738
|
+
prev_alarm_var.set(True)
|
|
1739
|
+
holiday_var.set(False)
|
|
1740
|
+
elif kind_var.get() == "holiday":
|
|
1741
|
+
holiday_var.set(True)
|
|
1742
|
+
else:
|
|
1743
|
+
holiday_var.set(False)
|
|
1744
|
+
|
|
1745
|
+
for val, lbl_txt, fg_c in (
|
|
1746
|
+
("birthday", "🎂 روز تولد", "#F472B6"),
|
|
1747
|
+
("personal", "🎉 مناسبت شخصی", "#38BDF8"),
|
|
1748
|
+
("holiday", "🏖 تعطیل شخصی", "#F87171"),
|
|
1749
|
+
):
|
|
1750
|
+
tk.Radiobutton(
|
|
1751
|
+
row2,
|
|
1752
|
+
text=lbl_txt,
|
|
1753
|
+
value=val,
|
|
1754
|
+
variable=kind_var,
|
|
1755
|
+
command=on_kind_change,
|
|
1756
|
+
font=(font_fa, 8, "bold"),
|
|
1757
|
+
fg=fg_c,
|
|
1758
|
+
bg="#1E293B",
|
|
1759
|
+
selectcolor="#0F172A",
|
|
1760
|
+
activebackground="#1E293B",
|
|
1761
|
+
).pack(side="right", padx=(8, 0))
|
|
1762
|
+
|
|
1763
|
+
# انتخاب ماه و روز شمسی
|
|
1764
|
+
row3 = tk.Frame(form, bg="#1E293B")
|
|
1765
|
+
row3.pack(fill="x", pady=4)
|
|
1766
|
+
tk.Label(row3, text="تاریخ شمسی:", font=(font_fa, 9), fg="#CBD5E1", bg="#1E293B").pack(side="right", padx=(6, 0))
|
|
1767
|
+
|
|
1768
|
+
month_names = [f"{m} - {PERSIAN_MONTHS[m]}" for m in range(1, 13)]
|
|
1769
|
+
month_var = tk.StringVar(value=month_names[max(0, min(11, init_m - 1))])
|
|
1770
|
+
m_menu = tk.OptionMenu(row3, month_var, *month_names)
|
|
1771
|
+
m_menu.configure(
|
|
1772
|
+
font=(font_fa, 8, "bold"),
|
|
1773
|
+
bg="#0F172A",
|
|
1774
|
+
fg="#F8FAFC",
|
|
1775
|
+
activebackground="#334155",
|
|
1776
|
+
highlightthickness=0,
|
|
1777
|
+
bd=0,
|
|
1778
|
+
)
|
|
1779
|
+
m_menu["menu"].configure(bg="#0F172A", fg="#F8FAFC", font=(font_fa, 8))
|
|
1780
|
+
m_menu.pack(side="right", padx=(6, 0))
|
|
1781
|
+
|
|
1782
|
+
tk.Label(row3, text="روز:", font=(font_fa, 9), fg="#CBD5E1", bg="#1E293B").pack(side="right", padx=(8, 4))
|
|
1783
|
+
day_spin = tk.Spinbox(
|
|
1784
|
+
row3,
|
|
1785
|
+
from_=1,
|
|
1786
|
+
to=31,
|
|
1787
|
+
width=4,
|
|
1788
|
+
font=("Segoe UI", 9, "bold"),
|
|
1789
|
+
bg="#0F172A",
|
|
1790
|
+
fg="#F8FAFC",
|
|
1791
|
+
buttonbackground="#334155",
|
|
1792
|
+
)
|
|
1793
|
+
day_spin.delete(0, "end")
|
|
1794
|
+
day_spin.insert(0, str(init_d))
|
|
1795
|
+
day_spin.pack(side="right")
|
|
1796
|
+
|
|
1797
|
+
# تیک آلارم ۱ روز قبل (تکرار سالانه)
|
|
1798
|
+
row4 = tk.Frame(form, bg="#1E293B")
|
|
1799
|
+
row4.pack(fill="x", pady=(6, 2))
|
|
1800
|
+
tk.Checkbutton(
|
|
1801
|
+
row4,
|
|
1802
|
+
text="⏰ ثبت خودکار آلارم یادآور ۱ روز قبل (تکرار سالانه)",
|
|
1803
|
+
variable=prev_alarm_var,
|
|
1804
|
+
font=(font_fa, 8, "bold"),
|
|
1805
|
+
fg="#FBBF24",
|
|
1806
|
+
bg="#1E293B",
|
|
1807
|
+
selectcolor="#0F172A",
|
|
1808
|
+
activebackground="#1E293B",
|
|
1809
|
+
anchor="e",
|
|
1810
|
+
).pack(side="right")
|
|
1811
|
+
|
|
1812
|
+
row5 = tk.Frame(form, bg="#1E293B")
|
|
1813
|
+
row5.pack(fill="x", pady=2)
|
|
1814
|
+
tk.Label(row5, text="ساعت یادآوری روز قبل:", font=(font_fa, 8), fg="#CBD5E1", bg="#1E293B").pack(side="right", padx=(6, 0))
|
|
1815
|
+
sp_m = tk.Spinbox(row5, from_=0, to=59, width=3, format="%02.0f", font=("Segoe UI", 9, "bold"), bg="#0F172A", fg="#F8FAFC", buttonbackground="#334155")
|
|
1816
|
+
sp_m.delete(0, "end")
|
|
1817
|
+
sp_m.insert(0, "00")
|
|
1818
|
+
sp_m.pack(side="right", padx=2)
|
|
1819
|
+
tk.Label(row5, text=":", font=("Segoe UI", 9, "bold"), fg="#F8FAFC", bg="#1E293B").pack(side="right")
|
|
1820
|
+
sp_h = tk.Spinbox(row5, from_=0, to=23, width=3, format="%02.0f", font=("Segoe UI", 9, "bold"), bg="#0F172A", fg="#F8FAFC", buttonbackground="#334155")
|
|
1821
|
+
sp_h.delete(0, "end")
|
|
1822
|
+
sp_h.insert(0, "09")
|
|
1823
|
+
sp_h.pack(side="right", padx=2)
|
|
1824
|
+
|
|
1825
|
+
status_lbl = tk.Label(form, text="", font=(font_fa, 8, "bold"), fg="#10B981", bg="#1E293B")
|
|
1826
|
+
status_lbl.pack(anchor="e", pady=(2, 0))
|
|
1827
|
+
|
|
1828
|
+
# بخش لیست مناسبتهای ثبتشده
|
|
1829
|
+
list_frame = tk.LabelFrame(
|
|
1830
|
+
container,
|
|
1831
|
+
text=" مناسبتهای شخصی ثبتشده ",
|
|
1832
|
+
font=(font_fa, 9, "bold"),
|
|
1833
|
+
fg="#E2E8F0",
|
|
1834
|
+
bg="#1E293B",
|
|
1835
|
+
bd=1,
|
|
1836
|
+
padx=8,
|
|
1837
|
+
pady=6,
|
|
1838
|
+
)
|
|
1839
|
+
list_frame.pack(fill="both", expand=True)
|
|
1840
|
+
|
|
1841
|
+
list_canvas = tk.Canvas(list_frame, bg="#1E293B", highlightthickness=0)
|
|
1842
|
+
scrollbar = tk.Scrollbar(list_frame, orient="vertical", command=list_canvas.yview)
|
|
1843
|
+
items_inner = tk.Frame(list_canvas, bg="#1E293B")
|
|
1844
|
+
items_inner.bind("<Configure>", lambda e: list_canvas.configure(scrollregion=list_canvas.bbox("all")))
|
|
1845
|
+
list_canvas.create_window((0, 0), window=items_inner, anchor="nw")
|
|
1846
|
+
list_canvas.configure(yscrollcommand=scrollbar.set)
|
|
1847
|
+
scrollbar.pack(side="left", fill="y")
|
|
1848
|
+
list_canvas.pack(side="right", fill="both", expand=True)
|
|
1849
|
+
|
|
1850
|
+
def refresh_list():
|
|
1851
|
+
for ch in items_inner.winfo_children():
|
|
1852
|
+
ch.destroy()
|
|
1853
|
+
data = self._load_custom_occasions_dict()
|
|
1854
|
+
if not data:
|
|
1855
|
+
tk.Label(
|
|
1856
|
+
items_inner,
|
|
1857
|
+
text="هنوز مناسبتی ثبت نشده است.",
|
|
1858
|
+
font=(font_fa, 8),
|
|
1859
|
+
fg="#64748B",
|
|
1860
|
+
bg="#1E293B",
|
|
1861
|
+
).pack(pady=16, padx=10)
|
|
1862
|
+
return
|
|
1863
|
+
|
|
1864
|
+
for mmdd in sorted(data.keys()):
|
|
1865
|
+
items = data.get(mmdd, [])
|
|
1866
|
+
try:
|
|
1867
|
+
m_i, d_i = map(int, mmdd.split("-"))
|
|
1868
|
+
m_name = PERSIAN_MONTHS[m_i] if 1 <= m_i <= 12 else str(m_i)
|
|
1869
|
+
date_lbl = f"{d_i} {m_name}"
|
|
1870
|
+
except Exception:
|
|
1871
|
+
date_lbl = mmdd
|
|
1872
|
+
|
|
1873
|
+
for idx_i, item in enumerate(items):
|
|
1874
|
+
t = item.get("title", "") if isinstance(item, dict) else str(item)
|
|
1875
|
+
is_h = bool(item.get("holiday", False)) if isinstance(item, dict) else False
|
|
1876
|
+
row = tk.Frame(items_inner, bg="#0F172A", padx=8, pady=5)
|
|
1877
|
+
row.pack(fill="x", pady=2, padx=2)
|
|
1878
|
+
|
|
1879
|
+
def make_del(k=mmdd, ix=idx_i, occ_title=t):
|
|
1880
|
+
def _del():
|
|
1881
|
+
d_cur = self._load_custom_occasions_dict()
|
|
1882
|
+
lst = d_cur.get(k, [])
|
|
1883
|
+
if 0 <= ix < len(lst):
|
|
1884
|
+
lst.pop(ix)
|
|
1885
|
+
if lst:
|
|
1886
|
+
d_cur[k] = lst
|
|
1887
|
+
else:
|
|
1888
|
+
d_cur.pop(k, None)
|
|
1889
|
+
self._save_custom_occasions_dict(d_cur)
|
|
1890
|
+
# حذف آلارم یادآور سالانه مرتبط در صورت وجود
|
|
1891
|
+
clean_t = occ_title.replace("🎂 ", "").strip()
|
|
1892
|
+
expected_alarm = f"🎂 یادآور فردا: {clean_t}"
|
|
1893
|
+
before_n = len(self.alarm_store.alarms)
|
|
1894
|
+
self.alarm_store.alarms = [
|
|
1895
|
+
a for a in self.alarm_store.alarms
|
|
1896
|
+
if not (a.get("repeat_mode") == "yearly" and a.get("title") == expected_alarm)
|
|
1897
|
+
]
|
|
1898
|
+
if len(self.alarm_store.alarms) != before_n:
|
|
1899
|
+
self.alarm_store.save()
|
|
1900
|
+
self._on_alarms_updated()
|
|
1901
|
+
refresh_list()
|
|
1902
|
+
return _del
|
|
1903
|
+
|
|
1904
|
+
tk.Button(
|
|
1905
|
+
row,
|
|
1906
|
+
text="🗑 حذف",
|
|
1907
|
+
font=(font_fa, 8),
|
|
1908
|
+
bg="#7F1D1D",
|
|
1909
|
+
fg="#FCA5A5",
|
|
1910
|
+
relief="flat",
|
|
1911
|
+
cursor="hand2",
|
|
1912
|
+
command=make_del(),
|
|
1913
|
+
).pack(side="left")
|
|
1914
|
+
|
|
1915
|
+
tk.Label(
|
|
1916
|
+
row,
|
|
1917
|
+
text=f"📅 {date_lbl} — {t}",
|
|
1918
|
+
font=(font_fa, 8, "bold"),
|
|
1919
|
+
fg="#F87171" if is_h else "#F8FAFC",
|
|
1920
|
+
bg="#0F172A",
|
|
1921
|
+
anchor="e",
|
|
1922
|
+
).pack(side="right", fill="x", expand=True, padx=(8, 0))
|
|
1923
|
+
|
|
1924
|
+
def on_add_occasion():
|
|
1925
|
+
raw_title = title_entry.get().strip()
|
|
1926
|
+
if not raw_title:
|
|
1927
|
+
status_lbl.configure(text="لطفاً عنوان مناسبت یا نام را وارد کنید.", fg="#F87171")
|
|
1928
|
+
return
|
|
1929
|
+
try:
|
|
1930
|
+
jm = int(month_var.get().split("-")[0].strip())
|
|
1931
|
+
jd = int(day_spin.get())
|
|
1932
|
+
max_d = get_jalali_month_days(init_y, jm)
|
|
1933
|
+
jd = max(1, min(jd, max_d))
|
|
1934
|
+
except Exception:
|
|
1935
|
+
status_lbl.configure(text="تاریخ واردشده معتبر نیست.", fg="#F87171")
|
|
1936
|
+
return
|
|
1937
|
+
|
|
1938
|
+
kind = kind_var.get()
|
|
1939
|
+
final_title = raw_title
|
|
1940
|
+
if kind == "birthday" and "🎂" not in final_title:
|
|
1941
|
+
final_title = f"🎂 {final_title}"
|
|
1942
|
+
|
|
1943
|
+
key = f"{jm:02d}-{jd:02d}"
|
|
1944
|
+
data = self._load_custom_occasions_dict()
|
|
1945
|
+
data.setdefault(key, [])
|
|
1946
|
+
data[key].append({"title": final_title, "holiday": bool(holiday_var.get())})
|
|
1947
|
+
self._save_custom_occasions_dict(data)
|
|
1948
|
+
|
|
1949
|
+
alarm_msg = ""
|
|
1950
|
+
if prev_alarm_var.get():
|
|
1951
|
+
try:
|
|
1952
|
+
ah = max(0, min(23, int(sp_h.get())))
|
|
1953
|
+
am = max(0, min(59, int(sp_m.get())))
|
|
1954
|
+
except Exception:
|
|
1955
|
+
ah, am = 9, 0
|
|
1956
|
+
if jd > 1:
|
|
1957
|
+
pm, pd = jm, jd - 1
|
|
1958
|
+
else:
|
|
1959
|
+
pm = 12 if jm == 1 else jm - 1
|
|
1960
|
+
pd = get_jalali_month_days(init_y, pm)
|
|
1961
|
+
prev_key = f"{pm:02d}-{pd:02d}"
|
|
1962
|
+
clean_t = final_title.replace("🎂 ", "").strip()
|
|
1963
|
+
self.alarm_store.add_or_update({
|
|
1964
|
+
"title": f"🎂 یادآور فردا: {clean_t}",
|
|
1965
|
+
"hour": ah,
|
|
1966
|
+
"minute": am,
|
|
1967
|
+
"repeat_mode": "yearly",
|
|
1968
|
+
"yearly_days": [prev_key],
|
|
1969
|
+
"enabled": True,
|
|
1970
|
+
"play_sound": True,
|
|
1971
|
+
})
|
|
1972
|
+
alarm_msg = f" + آلارم روز قبل ({pd} {PERSIAN_MONTHS[pm]} ساعت {ah:02d}:{am:02d})"
|
|
1973
|
+
|
|
1974
|
+
title_entry.delete(0, "end")
|
|
1975
|
+
status_lbl.configure(text=f"✓ «{final_title}» ثبت شد{alarm_msg}", fg="#10B981")
|
|
1976
|
+
self._on_alarms_updated()
|
|
1977
|
+
refresh_list()
|
|
1978
|
+
|
|
1979
|
+
tk.Button(
|
|
1980
|
+
form,
|
|
1981
|
+
text="✓ افزودن مناسبت به تقویم",
|
|
1982
|
+
font=(font_fa, 9, "bold"),
|
|
1983
|
+
bg="#059669",
|
|
1984
|
+
fg="#FFFFFF",
|
|
1985
|
+
padx=12,
|
|
1986
|
+
pady=4,
|
|
1987
|
+
relief="flat",
|
|
1988
|
+
cursor="hand2",
|
|
1989
|
+
command=on_add_occasion,
|
|
1990
|
+
).pack(anchor="w", pady=(6, 0))
|
|
1991
|
+
|
|
1992
|
+
refresh_list()
|
|
1993
|
+
hide_from_taskbar(win)
|
|
1994
|
+
apply_rounded_corners(win, w, h, radius=14, enabled=True)
|
|
1995
|
+
|
|
1996
|
+
def _on_alarms_updated(self):
|
|
1997
|
+
self._refresh_display()
|
|
1998
|
+
if self.config.get("calendar_expanded"):
|
|
1999
|
+
self._render_calendar_month()
|
|
2000
|
+
|
|
2001
|
+
def _on_alarm_dismissed(self, alarm: dict):
|
|
2002
|
+
"""وقتی آلارم بدون Snooze بسته شد، اگر یکباره یا تاریخمحور بود از لیست حذف شود"""
|
|
2003
|
+
removed = self.alarm_store.on_alarm_dismissed(alarm)
|
|
2004
|
+
if removed:
|
|
2005
|
+
self._on_alarms_updated()
|
|
2006
|
+
if self.alarm_win and self.alarm_win.win and self.alarm_win.win.winfo_exists():
|
|
2007
|
+
self.alarm_win._refresh_list()
|
|
2008
|
+
self.alarm_win._render_picker_calendar()
|
|
2009
|
+
|
|
2010
|
+
def _init_position(self):
|
|
2011
|
+
screen_w = self.root.winfo_screenwidth()
|
|
2012
|
+
screen_h = self.root.winfo_screenheight()
|
|
2013
|
+
ww, wh = self.compact_w, self.compact_h
|
|
2014
|
+
|
|
2015
|
+
x = self.config.get("x")
|
|
2016
|
+
y = self.config.get("y")
|
|
2017
|
+
if self.config.get("locked"):
|
|
2018
|
+
snap_side = self.config.get("snap_side", "right")
|
|
2019
|
+
x = (screen_w - ww) if snap_side == "right" else 0
|
|
2020
|
+
elif x is None or y is None:
|
|
2021
|
+
x = screen_w - ww - 32
|
|
2022
|
+
y = 32
|
|
2023
|
+
self.config["x"] = x
|
|
2024
|
+
self.config["y"] = y
|
|
2025
|
+
self._save_config()
|
|
2026
|
+
|
|
2027
|
+
x = max(0, min(int(x), screen_w - ww))
|
|
2028
|
+
y = max(0, min(int(y), screen_h - wh))
|
|
2029
|
+
self.widget_win.geometry(f"{ww}x{wh}+{x}+{y}")
|
|
2030
|
+
self._position_lock_window()
|
|
2031
|
+
|
|
2032
|
+
def _apply_all_styles(self):
|
|
2033
|
+
if not self.widget_win:
|
|
2034
|
+
return
|
|
2035
|
+
|
|
2036
|
+
hide_from_taskbar(self.widget_win)
|
|
2037
|
+
if self.lock_win:
|
|
2038
|
+
hide_from_taskbar(self.lock_win)
|
|
2039
|
+
|
|
2040
|
+
is_locked = bool(self.config.get("locked", False))
|
|
2041
|
+
transparent_bg = bool(self.config.get("transparent_bg", False))
|
|
2042
|
+
bg_color = self.config.get("bg_color", "#141821")
|
|
2043
|
+
border_color = self.config.get("border_color", "#2A3244")
|
|
2044
|
+
bg = TRANSPARENT_KEY if transparent_bg else bg_color
|
|
2045
|
+
|
|
2046
|
+
self.widget_win.configure(bg=bg)
|
|
2047
|
+
self.main_frame.configure(
|
|
2048
|
+
bg=bg,
|
|
2049
|
+
highlightbackground=border_color if not transparent_bg else bg,
|
|
2050
|
+
highlightthickness=0 if transparent_bg else 1,
|
|
2051
|
+
)
|
|
2052
|
+
self.canvas.configure(bg=bg)
|
|
2053
|
+
self.btn_container.configure(bg=bg)
|
|
2054
|
+
|
|
2055
|
+
btn_bg = self._get_btn_base_bg()
|
|
2056
|
+
font_fa = self.config.get("font_family", "Vazir")
|
|
2057
|
+
|
|
2058
|
+
for tip in self._tooltips:
|
|
2059
|
+
tip.font_family = font_fa
|
|
2060
|
+
|
|
2061
|
+
self.extend_btn.configure(
|
|
2062
|
+
bg=btn_bg,
|
|
2063
|
+
fg=self.config.get("accent_color", "#38BDF8"),
|
|
2064
|
+
font=(font_fa, 8, "bold"),
|
|
2065
|
+
)
|
|
2066
|
+
self.copy_time_btn.configure(bg=btn_bg, fg="#A7F3D0")
|
|
2067
|
+
self.guid_btn.configure(bg=btn_bg, fg="#C4B5FD")
|
|
2068
|
+
self.alarm_btn.configure(bg=btn_bg, fg="#FBBF24")
|
|
2069
|
+
self.settings_btn.configure(
|
|
2070
|
+
bg=btn_bg,
|
|
2071
|
+
fg=self.config.get("date_color", "#CBD5E1"),
|
|
2072
|
+
)
|
|
2073
|
+
if hasattr(self, "min_btn"):
|
|
2074
|
+
self.min_btn.configure(
|
|
2075
|
+
bg=btn_bg,
|
|
2076
|
+
fg=self.config.get("date_color", "#CBD5E1"),
|
|
2077
|
+
)
|
|
2078
|
+
|
|
2079
|
+
self._refresh_display()
|
|
2080
|
+
|
|
2081
|
+
ww, wh = self.compact_w, self.compact_h
|
|
2082
|
+
screen_w = self.root.winfo_screenwidth()
|
|
2083
|
+
screen_h = self.root.winfo_screenheight()
|
|
2084
|
+
cur_x = self.widget_win.winfo_x()
|
|
2085
|
+
cur_y = self.widget_win.winfo_y()
|
|
2086
|
+
if is_locked:
|
|
2087
|
+
snap_side = self.config.get("snap_side", "right")
|
|
2088
|
+
pos_x = (screen_w - ww) if snap_side == "right" else 0
|
|
2089
|
+
pos_y = max(0, min(cur_y, screen_h - wh - 40))
|
|
2090
|
+
else:
|
|
2091
|
+
pos_x = max(0, min(cur_x, screen_w - ww))
|
|
2092
|
+
pos_y = max(0, min(cur_y, screen_h - wh - 40))
|
|
2093
|
+
self.widget_win.geometry(f"{ww}x{wh}+{int(pos_x)}+{int(pos_y)}")
|
|
2094
|
+
self._position_lock_window()
|
|
2095
|
+
|
|
2096
|
+
alpha_val = float(self.config.get("opacity", 0.90))
|
|
2097
|
+
if is_locked and alpha_val >= 1.0:
|
|
2098
|
+
alpha_val = 0.99
|
|
2099
|
+
|
|
2100
|
+
if transparent_bg:
|
|
2101
|
+
apply_rounded_corners(self.widget_win, ww, wh, enabled=False)
|
|
2102
|
+
self.widget_win.attributes("-transparentcolor", TRANSPARENT_KEY)
|
|
2103
|
+
self.widget_win.attributes("-alpha", alpha_val)
|
|
2104
|
+
else:
|
|
2105
|
+
self.widget_win.attributes("-transparentcolor", "")
|
|
2106
|
+
self.widget_win.attributes("-alpha", alpha_val)
|
|
2107
|
+
apply_rounded_corners(self.widget_win, ww, wh, radius=16, enabled=True)
|
|
2108
|
+
|
|
2109
|
+
z_mode = self.config.get("z_mode", "topmost")
|
|
2110
|
+
if self.lock_win:
|
|
2111
|
+
lock_bg = "#1E2638" if not transparent_bg else "#141821"
|
|
2112
|
+
lock_border = "#F59E0B" if is_locked else border_color
|
|
2113
|
+
self.lock_win.configure(bg=lock_bg)
|
|
2114
|
+
self.lock_frame.configure(bg=lock_bg, highlightbackground=lock_border, highlightthickness=1)
|
|
2115
|
+
self.lock_btn_label.configure(
|
|
2116
|
+
text="🔒" if is_locked else "🔓",
|
|
2117
|
+
fg="#FBBF24" if is_locked else self.config.get("accent_color", "#38BDF8"),
|
|
2118
|
+
bg=lock_bg,
|
|
2119
|
+
)
|
|
2120
|
+
apply_rounded_corners(self.lock_win, self.LOCK_BTN_SIZE, self.LOCK_BTN_SIZE, radius=6, enabled=True)
|
|
2121
|
+
self.lock_win.attributes("-alpha", max(0.85, alpha_val))
|
|
2122
|
+
apply_z_order_and_clickthrough(self.lock_win, z_mode, click_through=False)
|
|
2123
|
+
|
|
2124
|
+
if self.cal_win:
|
|
2125
|
+
self.cal_win.configure(bg=bg_color)
|
|
2126
|
+
self.cal_frame.configure(bg=bg_color, highlightbackground=border_color)
|
|
2127
|
+
self.cal_header.configure(bg=bg_color)
|
|
2128
|
+
self.week_header.configure(bg=bg_color)
|
|
2129
|
+
self.days_grid_frame.configure(bg=bg_color)
|
|
2130
|
+
self.cal_sep.configure(bg=border_color)
|
|
2131
|
+
self.cal_bottom_bar.configure(bg=bg_color)
|
|
2132
|
+
self.cal_copyright_lbl.configure(bg=bg_color)
|
|
2133
|
+
self.add_event_btn.configure(font=(font_fa, 8, "bold"))
|
|
2134
|
+
self.today_btn.configure(font=(font_fa, 8, "bold"))
|
|
2135
|
+
|
|
2136
|
+
apply_z_order_and_clickthrough(
|
|
2137
|
+
self.widget_win,
|
|
2138
|
+
z_mode,
|
|
2139
|
+
click_through=is_locked,
|
|
2140
|
+
)
|
|
2141
|
+
if self.lock_win:
|
|
2142
|
+
apply_z_order_and_clickthrough(self.lock_win, z_mode, click_through=False)
|
|
2143
|
+
|
|
2144
|
+
self._sync_calendar_visibility()
|
|
2145
|
+
|
|
2146
|
+
def _on_drag_start(self, event):
|
|
2147
|
+
if self.config.get("locked", False):
|
|
2148
|
+
return
|
|
2149
|
+
self._close_dev_popup_window()
|
|
2150
|
+
self._is_dragging = True
|
|
2151
|
+
self._drag_start_x = event.x_root - self.widget_win.winfo_x()
|
|
2152
|
+
self._drag_start_y = event.y_root - self.widget_win.winfo_y()
|
|
2153
|
+
|
|
2154
|
+
def _on_drag_motion(self, event):
|
|
2155
|
+
if not self._is_dragging or self.config.get("locked", False):
|
|
2156
|
+
return
|
|
2157
|
+
new_x = event.x_root - self._drag_start_x
|
|
2158
|
+
new_y = event.y_root - self._drag_start_y
|
|
2159
|
+
self.widget_win.geometry(f"{self.compact_w}x{self.compact_h}+{int(new_x)}+{int(new_y)}")
|
|
2160
|
+
self._position_lock_window()
|
|
2161
|
+
if self.config.get("calendar_expanded", False):
|
|
2162
|
+
self._position_calendar_window()
|
|
2163
|
+
|
|
2164
|
+
def _on_drag_end(self, event):
|
|
2165
|
+
if not self._is_dragging:
|
|
2166
|
+
return
|
|
2167
|
+
self._is_dragging = False
|
|
2168
|
+
self.config["x"] = self.widget_win.winfo_x()
|
|
2169
|
+
self.config["y"] = self.widget_win.winfo_y()
|
|
2170
|
+
self._unlocked_x = self.config["x"]
|
|
2171
|
+
self._unlocked_y = self.config["y"]
|
|
2172
|
+
self._save_config()
|
|
2173
|
+
z_mode = self.config.get("z_mode", "topmost")
|
|
2174
|
+
is_locked = bool(self.config.get("locked", False))
|
|
2175
|
+
apply_z_order_and_clickthrough(self.widget_win, z_mode, click_through=is_locked)
|
|
2176
|
+
if self.lock_win:
|
|
2177
|
+
apply_z_order_and_clickthrough(self.lock_win, z_mode, click_through=False)
|
|
2178
|
+
|
|
2179
|
+
def _build_context_menu(self):
|
|
2180
|
+
self.menu = tk.Menu(
|
|
2181
|
+
self.root,
|
|
2182
|
+
tearoff=0,
|
|
2183
|
+
bg="#1E293B",
|
|
2184
|
+
fg="#F8FAFC",
|
|
2185
|
+
activebackground="#0284C7",
|
|
2186
|
+
activeforeground="#FFFFFF",
|
|
2187
|
+
font=("Tahoma", 9),
|
|
2188
|
+
)
|
|
2189
|
+
|
|
2190
|
+
def _populate_context_menu(self):
|
|
2191
|
+
self.menu.delete(0, "end")
|
|
2192
|
+
|
|
2193
|
+
locked_label = (
|
|
2194
|
+
"🔓 باز کردن قفل ویجت (فعالسازی کلیک و جابهجایی)"
|
|
2195
|
+
if self.config.get("locked")
|
|
2196
|
+
else "🔒 قفل و فیکس کردن ویجت (کوچکترین حالت و چسبیدن به لبه صفحه)"
|
|
2197
|
+
)
|
|
2198
|
+
self.menu.add_command(label=locked_label, command=self._toggle_lock)
|
|
2199
|
+
self.menu.add_separator()
|
|
2200
|
+
|
|
2201
|
+
if getattr(self, "_is_minimized", False):
|
|
2202
|
+
self.menu.add_command(
|
|
2203
|
+
label="🗖 نمایش مجدد ویجت روی صفحه (Restore)",
|
|
2204
|
+
command=self.restore_widget,
|
|
2205
|
+
)
|
|
2206
|
+
else:
|
|
2207
|
+
self.menu.add_command(
|
|
2208
|
+
label="🗕 مینیمایز ویجت (نمایش فقط در تسکبار و Hidden Icons)",
|
|
2209
|
+
command=self.minimize_widget,
|
|
2210
|
+
)
|
|
2211
|
+
self.menu.add_separator()
|
|
2212
|
+
|
|
2213
|
+
self.menu.add_command(
|
|
2214
|
+
label="🎲 رندر و کپی فوری GUID جدید (UUID v4)",
|
|
2215
|
+
command=lambda: self._copy_to_clipboard(str(uuid.uuid4()), "✓ GUID کپی شد!"),
|
|
2216
|
+
)
|
|
2217
|
+
self.menu.add_command(
|
|
2218
|
+
label="📋 باز کردن پنل کپی زمان (۱۰ فرمت API) و GUID...",
|
|
2219
|
+
command=self._toggle_dev_popup_window,
|
|
2220
|
+
)
|
|
2221
|
+
|
|
2222
|
+
self._populate_dev_copy_menu()
|
|
2223
|
+
self.menu.add_cascade(
|
|
2224
|
+
label="🕒 لیست سریع فرمتهای زمان API و GUID",
|
|
2225
|
+
menu=self.copy_menu,
|
|
2226
|
+
)
|
|
2227
|
+
self.menu.add_separator()
|
|
2228
|
+
|
|
2229
|
+
self.menu.add_command(
|
|
2230
|
+
label="⏰ مدیریت رویدادها و آلارمها...",
|
|
2231
|
+
command=self.open_alarm_manager,
|
|
2232
|
+
)
|
|
2233
|
+
self.menu.add_command(
|
|
2234
|
+
label="🎂 مدیریت مناسبتهای شخصی و تولدها...",
|
|
2235
|
+
command=self.open_custom_occasions_manager,
|
|
2236
|
+
)
|
|
2237
|
+
|
|
2238
|
+
self.menu.add_command(
|
|
2239
|
+
label="⚙️ تنظیمات ظاهری، رنگ، سایز و فونت...",
|
|
2240
|
+
command=self.open_settings_window,
|
|
2241
|
+
)
|
|
2242
|
+
|
|
2243
|
+
cal_text = "▾ بستن تقویم ماهانه" if self.config.get("calendar_expanded") else "📅 نمایش تقویم ماهانه (Extend)"
|
|
2244
|
+
self.menu.add_command(label=cal_text, command=lambda: self._toggle_calendar(None))
|
|
2245
|
+
|
|
2246
|
+
z_menu = tk.Menu(
|
|
2247
|
+
self.menu,
|
|
2248
|
+
tearoff=0,
|
|
2249
|
+
bg="#1E293B",
|
|
2250
|
+
fg="#F8FAFC",
|
|
2251
|
+
activebackground="#0284C7",
|
|
2252
|
+
activeforeground="#FFFFFF",
|
|
2253
|
+
font=("Tahoma", 9),
|
|
2254
|
+
)
|
|
2255
|
+
cur_z = self.config.get("z_mode", "topmost")
|
|
2256
|
+
for mode_key, mode_title in [
|
|
2257
|
+
("topmost", "همیشه روی تمام برنامهها (Z-Index بالا - پیشنهادی)"),
|
|
2258
|
+
("desktop", "چسبیده به دسکتاپ (پشت پنجرههای باز)"),
|
|
2259
|
+
]:
|
|
2260
|
+
prefix = "✓ " if cur_z == mode_key else ""
|
|
2261
|
+
z_menu.add_command(
|
|
2262
|
+
label=f"{prefix}{mode_title}",
|
|
2263
|
+
command=lambda m=mode_key: self._set_z_mode(m),
|
|
2264
|
+
)
|
|
2265
|
+
self.menu.add_cascade(label="📌 لایه قرارگیری (Z-Index)", menu=z_menu)
|
|
2266
|
+
|
|
2267
|
+
bg_label = (
|
|
2268
|
+
"✓ 🖼 حالت: بدون پسزمینه (تغییر به کارت با پسزمینه)"
|
|
2269
|
+
if self.config.get("transparent_bg")
|
|
2270
|
+
else "🖼 حالت: با پسزمینه (تغییر به بدون پسزمینه)"
|
|
2271
|
+
)
|
|
2272
|
+
self.menu.add_command(label=bg_label, command=self._toggle_transparent_bg)
|
|
2273
|
+
|
|
2274
|
+
digits_label = (
|
|
2275
|
+
"🔢 نمایش اعداد: فارسی (تغییر به انگلیسی)"
|
|
2276
|
+
if self.config.get("persian_digits")
|
|
2277
|
+
else "🔢 نمایش اعداد: انگلیسی (تغییر به فارسی)"
|
|
2278
|
+
)
|
|
2279
|
+
self.menu.add_command(label=digits_label, command=self._toggle_digits)
|
|
2280
|
+
|
|
2281
|
+
self.menu.add_separator()
|
|
2282
|
+
|
|
2283
|
+
startup_on = is_startup_enabled()
|
|
2284
|
+
os_name_fa = "ویندوز" if sys.platform == "win32" else "لینوکس"
|
|
2285
|
+
startup_label = f"✓ 🚀 اجرا با شروع {os_name_fa} (فعال)" if startup_on else f"🚀 اجرا خودکار با شروع {os_name_fa} (Startup)"
|
|
2286
|
+
self.menu.add_command(label=startup_label, command=self._toggle_startup)
|
|
2287
|
+
|
|
2288
|
+
# جایگزینی کامل ساعت ویندوز در تسکبار
|
|
2289
|
+
replace_clk_label = (
|
|
2290
|
+
"✓ ⭐ جایگزینی کامل ساعت و تاریخ ویندوز در تسکبار (فعال)"
|
|
2291
|
+
if self.config.get("replace_windows_clock")
|
|
2292
|
+
else "⭐ جایگزینی کامل ساعت و تاریخ ویندوز در تسکبار"
|
|
2293
|
+
)
|
|
2294
|
+
self.menu.add_command(label=replace_clk_label, command=lambda: self._toggle_taskbar_setting("replace_windows_clock"))
|
|
2295
|
+
|
|
2296
|
+
if self.config.get("replace_windows_clock"):
|
|
2297
|
+
hide_desk_label = (
|
|
2298
|
+
"✓ 👁️ مخفی کردن ویجت دسکتاپ (فقط ساعت تسکبار)"
|
|
2299
|
+
if self.config.get("replace_clock_hide_widget")
|
|
2300
|
+
else "👁️ مخفی کردن ویجت دسکتاپ (فقط ساعت تسکبار)"
|
|
2301
|
+
)
|
|
2302
|
+
self.menu.add_command(label=hide_desk_label, command=lambda: self._toggle_taskbar_setting("replace_clock_hide_widget"))
|
|
2303
|
+
|
|
2304
|
+
tb_clock_label = (
|
|
2305
|
+
"✓ 🕒 نمایش ساعت و تاریخ شناور روی تسکبار (فعال)"
|
|
2306
|
+
if self.config.get("taskbar_clock")
|
|
2307
|
+
else "🕒 نمایش ساعت و تاریخ شناور روی تسکبار"
|
|
2308
|
+
)
|
|
2309
|
+
self.menu.add_command(label=tb_clock_label, command=lambda: self._toggle_taskbar_setting("taskbar_clock"))
|
|
2310
|
+
tb_btn_label = (
|
|
2311
|
+
"✓ 📌 نمایش آیکون برنامه در تسکبار (فعال)"
|
|
2312
|
+
if self.config.get("taskbar_button")
|
|
2313
|
+
else "📌 نمایش آیکون برنامه در تسکبار"
|
|
2314
|
+
)
|
|
2315
|
+
self.menu.add_command(label=tb_btn_label, command=lambda: self._toggle_taskbar_setting("taskbar_button"))
|
|
2316
|
+
|
|
2317
|
+
self.menu.add_command(label="🔄 بازنشانی مکان به گوشه مانیتور", command=self._reset_position)
|
|
2318
|
+
self.menu.add_separator()
|
|
2319
|
+
self.menu.add_command(label="® Shayan Bamyar", state="disabled")
|
|
2320
|
+
self.menu.add_command(label="❌ خروج کامل از ویجت", command=self.quit_app)
|
|
2321
|
+
|
|
2322
|
+
def _show_context_menu(self, event):
|
|
2323
|
+
if self.config.get("locked", False):
|
|
2324
|
+
return
|
|
2325
|
+
self._populate_context_menu()
|
|
2326
|
+
try:
|
|
2327
|
+
self.menu.tk_popup(event.x_root, event.y_root)
|
|
2328
|
+
finally:
|
|
2329
|
+
self.menu.grab_release()
|
|
2330
|
+
|
|
2331
|
+
def _set_z_mode(self, mode: str):
|
|
2332
|
+
self.config["z_mode"] = mode
|
|
2333
|
+
self._save_config()
|
|
2334
|
+
self._apply_all_styles()
|
|
2335
|
+
|
|
2336
|
+
def _toggle_lock(self):
|
|
2337
|
+
self._close_dev_popup_window()
|
|
2338
|
+
going_to_lock = not self.config.get("locked", False)
|
|
2339
|
+
self.config["locked"] = going_to_lock
|
|
2340
|
+
screen_w = self.root.winfo_screenwidth()
|
|
2341
|
+
if going_to_lock:
|
|
2342
|
+
# ذخیره مکان آزاد قبل از قفل و تعیین جهت چسبیدن به لبه صفحه (چپ یا راست)
|
|
2343
|
+
if self.widget_win:
|
|
2344
|
+
self._unlocked_x = self.widget_win.winfo_x()
|
|
2345
|
+
self._unlocked_y = self.widget_win.winfo_y()
|
|
2346
|
+
center_x = self._unlocked_x + (self.compact_w / 2)
|
|
2347
|
+
self.config["snap_side"] = "right" if center_x >= (screen_w / 2) else "left"
|
|
2348
|
+
if self.config.get("calendar_expanded", False):
|
|
2349
|
+
self.config["calendar_expanded"] = False
|
|
2350
|
+
else:
|
|
2351
|
+
# هنگام باز کردن قفل، مکان به شکلی بازگردد که ویجت باز شده از صفحه بیرون نزند
|
|
2352
|
+
if self.config.get("snap_side") == "right":
|
|
2353
|
+
self.config["x"] = max(10, screen_w - self.BASE_MIN_W - 20)
|
|
2354
|
+
elif getattr(self, "_unlocked_x", None) is not None:
|
|
2355
|
+
self.config["x"] = max(10, min(int(self._unlocked_x), screen_w - self.BASE_MIN_W - 10))
|
|
2356
|
+
self._save_config()
|
|
2357
|
+
self._apply_all_styles()
|
|
2358
|
+
if hasattr(self, "_settings_lock_var") and self._settings_lock_var is not None:
|
|
2359
|
+
try:
|
|
2360
|
+
self._settings_lock_var.set(bool(self.config["locked"]))
|
|
2361
|
+
except Exception:
|
|
2362
|
+
pass
|
|
2363
|
+
|
|
2364
|
+
def _toggle_transparent_bg(self):
|
|
2365
|
+
self.config["transparent_bg"] = not self.config.get("transparent_bg", False)
|
|
2366
|
+
self._save_config()
|
|
2367
|
+
self._apply_all_styles()
|
|
2368
|
+
|
|
2369
|
+
def _toggle_digits(self):
|
|
2370
|
+
self.config["persian_digits"] = not self.config.get("persian_digits", False)
|
|
2371
|
+
self._save_config()
|
|
2372
|
+
self._apply_all_styles()
|
|
2373
|
+
|
|
2374
|
+
def _toggle_startup(self):
|
|
2375
|
+
cur = is_startup_enabled()
|
|
2376
|
+
set_startup_enabled(not cur, os.path.abspath(sys.argv[0]))
|
|
2377
|
+
|
|
2378
|
+
def _reset_position(self):
|
|
2379
|
+
self.config["x"] = None
|
|
2380
|
+
self.config["y"] = None
|
|
2381
|
+
self._init_position()
|
|
2382
|
+
if self.config.get("calendar_expanded"):
|
|
2383
|
+
self._position_calendar_window()
|
|
2384
|
+
|
|
2385
|
+
def open_settings_window(self):
|
|
2386
|
+
if self.settings_win and self.settings_win.winfo_exists():
|
|
2387
|
+
self.settings_win.deiconify()
|
|
2388
|
+
self.settings_win.lift()
|
|
2389
|
+
self.settings_win.focus_force()
|
|
2390
|
+
return
|
|
2391
|
+
|
|
2392
|
+
win = tk.Toplevel(self.root)
|
|
2393
|
+
self.settings_win = win
|
|
2394
|
+
win.title("تنظیمات ویجت ساعت و تقویم شمسی")
|
|
2395
|
+
win.configure(bg="#0F172A")
|
|
2396
|
+
win.resizable(False, False)
|
|
2397
|
+
win.attributes("-topmost", True)
|
|
2398
|
+
|
|
2399
|
+
ico_path = get_resource_path(os.path.join("assets", "icons", "app_icon.ico"))
|
|
2400
|
+
if os.path.exists(ico_path):
|
|
2401
|
+
try:
|
|
2402
|
+
win.iconbitmap(ico_path)
|
|
2403
|
+
except Exception:
|
|
2404
|
+
pass
|
|
2405
|
+
|
|
2406
|
+
w_width, w_height = 500, 800
|
|
2407
|
+
sx = (win.winfo_screenwidth() - w_width) // 2
|
|
2408
|
+
sy = (win.winfo_screenheight() - w_height) // 2
|
|
2409
|
+
win.geometry(f"{w_width}x{w_height}+{sx}+{sy}")
|
|
2410
|
+
|
|
2411
|
+
font_fa = self.config.get("font_family", "Vazir")
|
|
2412
|
+
container = tk.Frame(win, bg="#0F172A", padx=18, pady=14)
|
|
2413
|
+
container.pack(fill="both", expand=True)
|
|
2414
|
+
|
|
2415
|
+
tk.Label(
|
|
2416
|
+
container,
|
|
2417
|
+
text="⚙️ تنظیمات ظاهری و رفتاری ویجت",
|
|
2418
|
+
font=(font_fa, 12, "bold"),
|
|
2419
|
+
fg="#38BDF8",
|
|
2420
|
+
bg="#0F172A",
|
|
2421
|
+
anchor="e",
|
|
2422
|
+
).pack(fill="x", pady=(0, 8))
|
|
2423
|
+
|
|
2424
|
+
sec1 = tk.LabelFrame(
|
|
2425
|
+
container,
|
|
2426
|
+
text=" ۱. لایه نمایش (Z-Index) و قفل روی صفحه ",
|
|
2427
|
+
font=(font_fa, 9, "bold"),
|
|
2428
|
+
fg="#E2E8F0",
|
|
2429
|
+
bg="#1E293B",
|
|
2430
|
+
bd=1,
|
|
2431
|
+
padx=10,
|
|
2432
|
+
pady=6,
|
|
2433
|
+
)
|
|
2434
|
+
sec1.pack(fill="x", pady=(0, 8))
|
|
2435
|
+
|
|
2436
|
+
z_var = tk.StringVar(value=self.config.get("z_mode", "topmost"))
|
|
2437
|
+
|
|
2438
|
+
def on_z_change():
|
|
2439
|
+
self.config["z_mode"] = z_var.get()
|
|
2440
|
+
self._save_config()
|
|
2441
|
+
self._apply_all_styles()
|
|
2442
|
+
|
|
2443
|
+
for val, txt in [
|
|
2444
|
+
("topmost", "همیشه روی تمام برنامهها (با باز شدن برنامهها، ویجت روی آنها میماند)"),
|
|
2445
|
+
("desktop", "چسبیده به والپیپر دسکتاپ (پشت پنجرههای باز)"),
|
|
2446
|
+
]:
|
|
2447
|
+
tk.Radiobutton(
|
|
2448
|
+
sec1,
|
|
2449
|
+
text=txt,
|
|
2450
|
+
variable=z_var,
|
|
2451
|
+
value=val,
|
|
2452
|
+
command=on_z_change,
|
|
2453
|
+
font=(font_fa, 8),
|
|
2454
|
+
fg="#F8FAFC",
|
|
2455
|
+
bg="#1E293B",
|
|
2456
|
+
selectcolor="#0F172A",
|
|
2457
|
+
activebackground="#1E293B",
|
|
2458
|
+
activeforeground="#38BDF8",
|
|
2459
|
+
anchor="w",
|
|
2460
|
+
).pack(fill="x", pady=1)
|
|
2461
|
+
|
|
2462
|
+
self._settings_lock_var = tk.BooleanVar(value=bool(self.config.get("locked", False)))
|
|
2463
|
+
|
|
2464
|
+
def on_lock_change():
|
|
2465
|
+
new_locked = self._settings_lock_var.get()
|
|
2466
|
+
self.config["locked"] = new_locked
|
|
2467
|
+
screen_w = self.root.winfo_screenwidth()
|
|
2468
|
+
if new_locked:
|
|
2469
|
+
if self.widget_win:
|
|
2470
|
+
self._unlocked_x = self.widget_win.winfo_x()
|
|
2471
|
+
self._unlocked_y = self.widget_win.winfo_y()
|
|
2472
|
+
center_x = self._unlocked_x + (self.compact_w / 2)
|
|
2473
|
+
self.config["snap_side"] = "right" if center_x >= (screen_w / 2) else "left"
|
|
2474
|
+
if self.config.get("calendar_expanded", False):
|
|
2475
|
+
self.config["calendar_expanded"] = False
|
|
2476
|
+
else:
|
|
2477
|
+
if self.config.get("snap_side") == "right":
|
|
2478
|
+
self.config["x"] = max(10, screen_w - self.BASE_MIN_W - 20)
|
|
2479
|
+
elif getattr(self, "_unlocked_x", None) is not None:
|
|
2480
|
+
self.config["x"] = max(10, min(int(self._unlocked_x), screen_w - self.BASE_MIN_W - 10))
|
|
2481
|
+
self._save_config()
|
|
2482
|
+
self._apply_all_styles()
|
|
2483
|
+
|
|
2484
|
+
tk.Checkbutton(
|
|
2485
|
+
sec1,
|
|
2486
|
+
text="قفل و فیکس کردن ویجت (🔒 عبوردهی کلیک به برنامه زیرین؛ فقط دکمه قفل قابل کلیک است)",
|
|
2487
|
+
variable=self._settings_lock_var,
|
|
2488
|
+
command=on_lock_change,
|
|
2489
|
+
font=(font_fa, 8, "bold"),
|
|
2490
|
+
fg="#FBBF24",
|
|
2491
|
+
bg="#1E293B",
|
|
2492
|
+
selectcolor="#0F172A",
|
|
2493
|
+
activebackground="#1E293B",
|
|
2494
|
+
).pack(anchor="w", pady=(4, 0))
|
|
2495
|
+
|
|
2496
|
+
startup_var = tk.BooleanVar(value=is_startup_enabled())
|
|
2497
|
+
os_name_fa = "ویندوز" if sys.platform == "win32" else "لینوکس"
|
|
2498
|
+
|
|
2499
|
+
def on_startup_change():
|
|
2500
|
+
want = bool(startup_var.get())
|
|
2501
|
+
script_target = os.path.abspath(sys.argv[0]) if sys.argv else os.path.abspath(__file__)
|
|
2502
|
+
ok = set_startup_enabled(want, script_target)
|
|
2503
|
+
if not ok:
|
|
2504
|
+
startup_var.set(is_startup_enabled())
|
|
2505
|
+
|
|
2506
|
+
tk.Checkbutton(
|
|
2507
|
+
sec1,
|
|
2508
|
+
text=f"🚀 اجرا خودکار ویجت همزمان با بالا آمدن {os_name_fa} (Startup)",
|
|
2509
|
+
variable=startup_var,
|
|
2510
|
+
command=on_startup_change,
|
|
2511
|
+
font=(font_fa, 8, "bold"),
|
|
2512
|
+
fg="#38BDF8",
|
|
2513
|
+
bg="#1E293B",
|
|
2514
|
+
selectcolor="#0F172A",
|
|
2515
|
+
activebackground="#1E293B",
|
|
2516
|
+
).pack(anchor="w", pady=(2, 0))
|
|
2517
|
+
|
|
2518
|
+
sec2 = tk.LabelFrame(
|
|
2519
|
+
container,
|
|
2520
|
+
text=" ۲. پسزمینه و میزان شفافیت ",
|
|
2521
|
+
font=(font_fa, 9, "bold"),
|
|
2522
|
+
fg="#E2E8F0",
|
|
2523
|
+
bg="#1E293B",
|
|
2524
|
+
bd=1,
|
|
2525
|
+
padx=10,
|
|
2526
|
+
pady=6,
|
|
2527
|
+
)
|
|
2528
|
+
sec2.pack(fill="x", pady=(0, 8))
|
|
2529
|
+
|
|
2530
|
+
trans_var = tk.BooleanVar(value=bool(self.config.get("transparent_bg", False)))
|
|
2531
|
+
|
|
2532
|
+
def on_trans_change():
|
|
2533
|
+
self.config["transparent_bg"] = trans_var.get()
|
|
2534
|
+
self._save_config()
|
|
2535
|
+
self._apply_all_styles()
|
|
2536
|
+
|
|
2537
|
+
tk.Checkbutton(
|
|
2538
|
+
sec2,
|
|
2539
|
+
text="حالت بدون پسزمینه (نمایش فقط متن با خط دور مشکی روی صفحه)",
|
|
2540
|
+
variable=trans_var,
|
|
2541
|
+
command=on_trans_change,
|
|
2542
|
+
font=(font_fa, 8, "bold"),
|
|
2543
|
+
fg="#F8FAFC",
|
|
2544
|
+
bg="#1E293B",
|
|
2545
|
+
selectcolor="#0F172A",
|
|
2546
|
+
activebackground="#1E293B",
|
|
2547
|
+
).pack(anchor="w")
|
|
2548
|
+
|
|
2549
|
+
op_row = tk.Frame(sec2, bg="#1E293B")
|
|
2550
|
+
op_row.pack(fill="x", pady=(4, 0))
|
|
2551
|
+
tk.Label(
|
|
2552
|
+
op_row,
|
|
2553
|
+
text="شفافیت کل ویجت (%):",
|
|
2554
|
+
font=(font_fa, 8),
|
|
2555
|
+
fg="#CBD5E1",
|
|
2556
|
+
bg="#1E293B",
|
|
2557
|
+
).pack(side="right")
|
|
2558
|
+
|
|
2559
|
+
op_scale = tk.Scale(
|
|
2560
|
+
op_row,
|
|
2561
|
+
from_=25,
|
|
2562
|
+
to=100,
|
|
2563
|
+
orient="horizontal",
|
|
2564
|
+
bg="#1E293B",
|
|
2565
|
+
fg="#F8FAFC",
|
|
2566
|
+
highlightthickness=0,
|
|
2567
|
+
troughcolor="#0F172A",
|
|
2568
|
+
length=220,
|
|
2569
|
+
command=lambda v: self._on_opacity_slider(int(v)),
|
|
2570
|
+
)
|
|
2571
|
+
op_scale.set(int(float(self.config.get("opacity", 0.90)) * 100))
|
|
2572
|
+
op_scale.pack(side="left")
|
|
2573
|
+
|
|
2574
|
+
sec3 = tk.LabelFrame(
|
|
2575
|
+
container,
|
|
2576
|
+
text=" ۳. انتخاب رنگ نوشتهها و پسزمینه ",
|
|
2577
|
+
font=(font_fa, 9, "bold"),
|
|
2578
|
+
fg="#E2E8F0",
|
|
2579
|
+
bg="#1E293B",
|
|
2580
|
+
bd=1,
|
|
2581
|
+
padx=10,
|
|
2582
|
+
pady=6,
|
|
2583
|
+
)
|
|
2584
|
+
sec3.pack(fill="x", pady=(0, 8))
|
|
2585
|
+
|
|
2586
|
+
color_items = [
|
|
2587
|
+
("clock_color", "رنگ ساعت:"),
|
|
2588
|
+
("date_color", "رنگ تاریخ شمسی:"),
|
|
2589
|
+
("occasion_color", "رنگ مناسبتها:"),
|
|
2590
|
+
("holiday_color", "رنگ روزهای تعطیل:"),
|
|
2591
|
+
("bg_color", "رنگ کارت پسزمینه:"),
|
|
2592
|
+
("accent_color", "رنگ دکمهها و تاکید:"),
|
|
2593
|
+
]
|
|
2594
|
+
|
|
2595
|
+
self._color_previews: dict[str, tk.Button] = {}
|
|
2596
|
+
grid_colors = tk.Frame(sec3, bg="#1E293B")
|
|
2597
|
+
grid_colors.pack(fill="x")
|
|
2598
|
+
|
|
2599
|
+
for idx, (cfg_key, label_txt) in enumerate(color_items):
|
|
2600
|
+
r = idx // 2
|
|
2601
|
+
c = (idx % 2) * 2
|
|
2602
|
+
tk.Label(
|
|
2603
|
+
grid_colors,
|
|
2604
|
+
text=label_txt,
|
|
2605
|
+
font=(font_fa, 8),
|
|
2606
|
+
fg="#CBD5E1",
|
|
2607
|
+
bg="#1E293B",
|
|
2608
|
+
anchor="e",
|
|
2609
|
+
).grid(row=r, column=c + 1, sticky="e", padx=(6, 4), pady=3)
|
|
2610
|
+
|
|
2611
|
+
btn = tk.Button(
|
|
2612
|
+
grid_colors,
|
|
2613
|
+
text="انتخاب رنگ",
|
|
2614
|
+
font=(font_fa, 8),
|
|
2615
|
+
bg=self.config.get(cfg_key, "#FFFFFF"),
|
|
2616
|
+
fg="#000000" if cfg_key != "bg_color" else "#FFFFFF",
|
|
2617
|
+
width=10,
|
|
2618
|
+
relief="flat",
|
|
2619
|
+
cursor="hand2",
|
|
2620
|
+
command=lambda k=cfg_key: self._pick_color(k),
|
|
2621
|
+
)
|
|
2622
|
+
btn.grid(row=r, column=c, sticky="w", padx=4, pady=3)
|
|
2623
|
+
self._color_previews[cfg_key] = btn
|
|
2624
|
+
|
|
2625
|
+
for ci in range(4):
|
|
2626
|
+
grid_colors.grid_columnconfigure(ci, weight=1)
|
|
2627
|
+
|
|
2628
|
+
sec4 = tk.LabelFrame(
|
|
2629
|
+
container,
|
|
2630
|
+
text=" ۴. اندازه ویجت و تنظیمات فونت (پیشفرض: Vazir) ",
|
|
2631
|
+
font=(font_fa, 9, "bold"),
|
|
2632
|
+
fg="#E2E8F0",
|
|
2633
|
+
bg="#1E293B",
|
|
2634
|
+
bd=1,
|
|
2635
|
+
padx=10,
|
|
2636
|
+
pady=6,
|
|
2637
|
+
)
|
|
2638
|
+
sec4.pack(fill="x", pady=(0, 8))
|
|
2639
|
+
|
|
2640
|
+
scale_row = tk.Frame(sec4, bg="#1E293B")
|
|
2641
|
+
scale_row.pack(fill="x", pady=2)
|
|
2642
|
+
tk.Label(
|
|
2643
|
+
scale_row,
|
|
2644
|
+
text="اندازه کلی ویجت (%):",
|
|
2645
|
+
font=(font_fa, 8),
|
|
2646
|
+
fg="#CBD5E1",
|
|
2647
|
+
bg="#1E293B",
|
|
2648
|
+
).pack(side="right")
|
|
2649
|
+
|
|
2650
|
+
widget_scale_slider = tk.Scale(
|
|
2651
|
+
scale_row,
|
|
2652
|
+
from_=70,
|
|
2653
|
+
to=170,
|
|
2654
|
+
orient="horizontal",
|
|
2655
|
+
bg="#1E293B",
|
|
2656
|
+
fg="#F8FAFC",
|
|
2657
|
+
highlightthickness=0,
|
|
2658
|
+
troughcolor="#0F172A",
|
|
2659
|
+
length=220,
|
|
2660
|
+
command=lambda v: self._on_scale_slider(int(v)),
|
|
2661
|
+
)
|
|
2662
|
+
widget_scale_slider.set(int(self.config.get("widget_scale", 100)))
|
|
2663
|
+
widget_scale_slider.pack(side="left")
|
|
2664
|
+
|
|
2665
|
+
bundled_and_std = [
|
|
2666
|
+
"Vazir",
|
|
2667
|
+
"B Yekan+",
|
|
2668
|
+
"A Iranian Sans",
|
|
2669
|
+
"Tahoma",
|
|
2670
|
+
"Segoe UI",
|
|
2671
|
+
"Calibri",
|
|
2672
|
+
"Arial",
|
|
2673
|
+
"Consolas",
|
|
2674
|
+
]
|
|
2675
|
+
try:
|
|
2676
|
+
sys_fonts = set(tkfont.families())
|
|
2677
|
+
for pf in ["Vazirmatn", "IRANSans", "IRANSansWeb", "B Nazanin", "B Yekan", "Samim", "Shabnam"]:
|
|
2678
|
+
if pf in sys_fonts and pf not in bundled_and_std:
|
|
2679
|
+
bundled_and_std.append(pf)
|
|
2680
|
+
except Exception:
|
|
2681
|
+
pass
|
|
2682
|
+
|
|
2683
|
+
font_row = tk.Frame(sec4, bg="#1E293B")
|
|
2684
|
+
font_row.pack(fill="x", pady=3)
|
|
2685
|
+
tk.Label(
|
|
2686
|
+
font_row,
|
|
2687
|
+
text="فونت تاریخ و متون:",
|
|
2688
|
+
font=(font_fa, 8),
|
|
2689
|
+
fg="#CBD5E1",
|
|
2690
|
+
bg="#1E293B",
|
|
2691
|
+
).pack(side="right")
|
|
2692
|
+
|
|
2693
|
+
font_cb = ttk.Combobox(font_row, values=bundled_and_std, state="readonly", width=20)
|
|
2694
|
+
font_cb.set(self.config.get("font_family", "Vazir"))
|
|
2695
|
+
font_cb.pack(side="left")
|
|
2696
|
+
|
|
2697
|
+
clock_font_row = tk.Frame(sec4, bg="#1E293B")
|
|
2698
|
+
clock_font_row.pack(fill="x", pady=3)
|
|
2699
|
+
tk.Label(
|
|
2700
|
+
clock_font_row,
|
|
2701
|
+
text="فونت اعداد ساعت:",
|
|
2702
|
+
font=(font_fa, 8),
|
|
2703
|
+
fg="#CBD5E1",
|
|
2704
|
+
bg="#1E293B",
|
|
2705
|
+
).pack(side="right")
|
|
2706
|
+
|
|
2707
|
+
clock_font_cb = ttk.Combobox(
|
|
2708
|
+
clock_font_row,
|
|
2709
|
+
values=["Vazir", "B Yekan+", "A Iranian Sans", "Segoe UI Semibold", "Tahoma", "Consolas"],
|
|
2710
|
+
state="readonly",
|
|
2711
|
+
width=20,
|
|
2712
|
+
)
|
|
2713
|
+
clock_font_cb.set(self.config.get("clock_font_family", "Vazir"))
|
|
2714
|
+
clock_font_cb.pack(side="left")
|
|
2715
|
+
|
|
2716
|
+
def on_font_selected(event=None):
|
|
2717
|
+
self.config["font_family"] = font_cb.get()
|
|
2718
|
+
self.config["clock_font_family"] = clock_font_cb.get()
|
|
2719
|
+
self._save_config()
|
|
2720
|
+
self._apply_all_styles()
|
|
2721
|
+
|
|
2722
|
+
font_cb.bind("<<ComboboxSelected>>", on_font_selected)
|
|
2723
|
+
clock_font_cb.bind("<<ComboboxSelected>>", on_font_selected)
|
|
2724
|
+
|
|
2725
|
+
sizes_row = tk.Frame(sec4, bg="#1E293B")
|
|
2726
|
+
sizes_row.pack(fill="x", pady=(6, 2))
|
|
2727
|
+
|
|
2728
|
+
def make_size_spin(parent, label_text, cfg_key, min_v, max_v):
|
|
2729
|
+
frame = tk.Frame(parent, bg="#1E293B")
|
|
2730
|
+
frame.pack(side="right", expand=True)
|
|
2731
|
+
tk.Label(
|
|
2732
|
+
frame,
|
|
2733
|
+
text=label_text,
|
|
2734
|
+
font=(font_fa, 8),
|
|
2735
|
+
fg="#CBD5E1",
|
|
2736
|
+
bg="#1E293B",
|
|
2737
|
+
).pack(side="right", padx=(2, 4))
|
|
2738
|
+
sp = tk.Spinbox(
|
|
2739
|
+
frame,
|
|
2740
|
+
from_=min_v,
|
|
2741
|
+
to=max_v,
|
|
2742
|
+
width=4,
|
|
2743
|
+
font=("Segoe UI", 9),
|
|
2744
|
+
bg="#0F172A",
|
|
2745
|
+
fg="#F8FAFC",
|
|
2746
|
+
buttonbackground="#334155",
|
|
2747
|
+
)
|
|
2748
|
+
sp.delete(0, "end")
|
|
2749
|
+
sp.insert(0, str(self.config.get(cfg_key, 10)))
|
|
2750
|
+
|
|
2751
|
+
def on_spin_change(*args):
|
|
2752
|
+
try:
|
|
2753
|
+
val = int(sp.get())
|
|
2754
|
+
if min_v <= val <= max_v:
|
|
2755
|
+
self.config[cfg_key] = val
|
|
2756
|
+
self._save_config()
|
|
2757
|
+
self._apply_all_styles()
|
|
2758
|
+
except ValueError:
|
|
2759
|
+
pass
|
|
2760
|
+
|
|
2761
|
+
sp.configure(command=on_spin_change)
|
|
2762
|
+
sp.bind("<Return>", on_spin_change)
|
|
2763
|
+
sp.bind("<FocusOut>", on_spin_change)
|
|
2764
|
+
sp.pack(side="left")
|
|
2765
|
+
|
|
2766
|
+
make_size_spin(sizes_row, "ساعت:", "clock_font_size", 12, 52)
|
|
2767
|
+
make_size_spin(sizes_row, "تاریخ:", "date_font_size", 6, 28)
|
|
2768
|
+
make_size_spin(sizes_row, "مناسبت:", "occasion_font_size", 6, 22)
|
|
2769
|
+
|
|
2770
|
+
sec5 = tk.LabelFrame(
|
|
2771
|
+
container,
|
|
2772
|
+
text=" ۵. نمایش و جایگزینی در تسکبار ویندوز ",
|
|
2773
|
+
font=(font_fa, 9, "bold"),
|
|
2774
|
+
fg="#E2E8F0",
|
|
2775
|
+
bg="#1E293B",
|
|
2776
|
+
bd=1,
|
|
2777
|
+
padx=10,
|
|
2778
|
+
pady=6,
|
|
2779
|
+
)
|
|
2780
|
+
sec5.pack(fill="x", pady=(0, 8))
|
|
2781
|
+
|
|
2782
|
+
self._settings_replace_clock_var = tk.BooleanVar(value=bool(self.config.get("replace_windows_clock", False)))
|
|
2783
|
+
self._settings_hide_widget_var = tk.BooleanVar(value=bool(self.config.get("replace_clock_hide_widget", False)))
|
|
2784
|
+
self._settings_tb_clock_var = tk.BooleanVar(value=bool(self.config.get("taskbar_clock", False)))
|
|
2785
|
+
self._settings_tb_btn_var = tk.BooleanVar(value=bool(self.config.get("taskbar_button", False)))
|
|
2786
|
+
tb_sec_var = tk.BooleanVar(value=bool(self.config.get("taskbar_clock_seconds", False)))
|
|
2787
|
+
|
|
2788
|
+
def on_tb_change():
|
|
2789
|
+
self.config["replace_windows_clock"] = bool(self._settings_replace_clock_var.get())
|
|
2790
|
+
self.config["replace_clock_hide_widget"] = bool(self._settings_hide_widget_var.get())
|
|
2791
|
+
self.config["taskbar_clock"] = bool(self._settings_tb_clock_var.get())
|
|
2792
|
+
self.config["taskbar_button"] = bool(self._settings_tb_btn_var.get())
|
|
2793
|
+
self.config["taskbar_clock_seconds"] = bool(tb_sec_var.get())
|
|
2794
|
+
self._save_config()
|
|
2795
|
+
self.sync_taskbar_features()
|
|
2796
|
+
|
|
2797
|
+
def on_reset_replacer_pos():
|
|
2798
|
+
self.config["replace_clock_offset"] = 8
|
|
2799
|
+
self.config["replace_clock_width"] = 118
|
|
2800
|
+
self._save_config()
|
|
2801
|
+
try:
|
|
2802
|
+
sp_rw.delete(0, "end")
|
|
2803
|
+
sp_rw.insert(0, "118")
|
|
2804
|
+
except Exception:
|
|
2805
|
+
pass
|
|
2806
|
+
if self.clock_replacer:
|
|
2807
|
+
self.clock_replacer.refresh(get_today_info(CUSTOM_OCCASIONS_PATH))
|
|
2808
|
+
|
|
2809
|
+
def on_tb_reset_pos():
|
|
2810
|
+
self.config["taskbar_clock_offset"] = None
|
|
2811
|
+
self._save_config()
|
|
2812
|
+
if self.taskbar_clock:
|
|
2813
|
+
self.taskbar_clock.refresh(get_today_info(CUSTOM_OCCASIONS_PATH))
|
|
2814
|
+
|
|
2815
|
+
def on_pick_replacer_bg():
|
|
2816
|
+
cur_c = self.config.get("replace_clock_bg", "auto")
|
|
2817
|
+
if cur_c == "auto":
|
|
2818
|
+
cur_c = "#1C1C1C"
|
|
2819
|
+
c = colorchooser.askcolor(initialcolor=cur_c, title="انتخاب رنگ پسزمینه ساعت تسکبار", parent=win)
|
|
2820
|
+
if c and c[1]:
|
|
2821
|
+
self.config["replace_clock_bg"] = c[1]
|
|
2822
|
+
self._save_config()
|
|
2823
|
+
if self.clock_replacer:
|
|
2824
|
+
self.clock_replacer.refresh(get_today_info(CUSTOM_OCCASIONS_PATH))
|
|
2825
|
+
|
|
2826
|
+
def on_auto_replacer_bg():
|
|
2827
|
+
self.config["replace_clock_bg"] = "auto"
|
|
2828
|
+
self._save_config()
|
|
2829
|
+
if self.clock_replacer:
|
|
2830
|
+
self.clock_replacer._last_color_probe_ts = 0.0
|
|
2831
|
+
self.clock_replacer.refresh(get_today_info(CUSTOM_OCCASIONS_PATH))
|
|
2832
|
+
|
|
2833
|
+
win11_hide_var = tk.BooleanVar(value=get_windows11_tray_clock_hidden())
|
|
2834
|
+
|
|
2835
|
+
def on_toggle_win11_clock():
|
|
2836
|
+
want_hide = bool(win11_hide_var.get())
|
|
2837
|
+
set_windows11_tray_clock_hidden(want_hide)
|
|
2838
|
+
try:
|
|
2839
|
+
os.startfile("ms-settings:dateandtime")
|
|
2840
|
+
except Exception:
|
|
2841
|
+
pass
|
|
2842
|
+
|
|
2843
|
+
# الف) جایگزینی کامل ساعت ویندوز
|
|
2844
|
+
tk.Checkbutton(
|
|
2845
|
+
sec5,
|
|
2846
|
+
text="⭐ جایگزینی کامل ساعت و تاریخ ویندوز در تسکبار (پوشاندن ساعت انگلیسی با ساعت و تاریخ شمسی)",
|
|
2847
|
+
variable=self._settings_replace_clock_var,
|
|
2848
|
+
command=on_tb_change,
|
|
2849
|
+
font=(font_fa, 8, "bold"),
|
|
2850
|
+
fg="#38BDF8",
|
|
2851
|
+
bg="#1E293B",
|
|
2852
|
+
selectcolor="#0F172A",
|
|
2853
|
+
activebackground="#1E293B",
|
|
2854
|
+
anchor="w",
|
|
2855
|
+
).pack(fill="x", pady=2)
|
|
2856
|
+
|
|
2857
|
+
tk.Checkbutton(
|
|
2858
|
+
sec5,
|
|
2859
|
+
text=" 👁️ مخفی کردن ویجت دسکتاپ هنگام فعال بودن جایگزینی (نمایش اختصاصی در تسکبار)",
|
|
2860
|
+
variable=self._settings_hide_widget_var,
|
|
2861
|
+
command=on_tb_change,
|
|
2862
|
+
font=(font_fa, 8),
|
|
2863
|
+
fg="#CBD5E1",
|
|
2864
|
+
bg="#1E293B",
|
|
2865
|
+
selectcolor="#0F172A",
|
|
2866
|
+
activebackground="#1E293B",
|
|
2867
|
+
anchor="w",
|
|
2868
|
+
).pack(fill="x", pady=1)
|
|
2869
|
+
|
|
2870
|
+
tk.Checkbutton(
|
|
2871
|
+
sec5,
|
|
2872
|
+
text=" 🗑 حذف کامل ساعت و تاریخ انگلیسی از تسکبار ویندوز ۱۱ (تنظیم رسمی ویندوز)",
|
|
2873
|
+
variable=win11_hide_var,
|
|
2874
|
+
command=on_toggle_win11_clock,
|
|
2875
|
+
font=(font_fa, 8),
|
|
2876
|
+
fg="#FBBF24",
|
|
2877
|
+
bg="#1E293B",
|
|
2878
|
+
selectcolor="#0F172A",
|
|
2879
|
+
activebackground="#1E293B",
|
|
2880
|
+
anchor="w",
|
|
2881
|
+
).pack(fill="x", pady=1)
|
|
2882
|
+
|
|
2883
|
+
# ب) گزینههای دیگر تسکبار
|
|
2884
|
+
for var, txt in (
|
|
2885
|
+
(self._settings_tb_clock_var, "🕒 نمایش ساعت و تاریخ شمسی شناور روی تسکبار (کنار ساعت ویندوز)"),
|
|
2886
|
+
(tb_sec_var, "⏱ نمایش ثانیه در ساعت تسکبار"),
|
|
2887
|
+
(self._settings_tb_btn_var, "📌 نمایش دکمه/آیکون برنامه در نوار وظیفه (تسکبار)"),
|
|
2888
|
+
):
|
|
2889
|
+
tk.Checkbutton(
|
|
2890
|
+
sec5,
|
|
2891
|
+
text=txt,
|
|
2892
|
+
variable=var,
|
|
2893
|
+
command=on_tb_change,
|
|
2894
|
+
font=(font_fa, 8),
|
|
2895
|
+
fg="#F8FAFC",
|
|
2896
|
+
bg="#1E293B",
|
|
2897
|
+
selectcolor="#0F172A",
|
|
2898
|
+
activebackground="#1E293B",
|
|
2899
|
+
anchor="w",
|
|
2900
|
+
).pack(fill="x", pady=1)
|
|
2901
|
+
|
|
2902
|
+
btn_row = tk.Frame(sec5, bg="#1E293B")
|
|
2903
|
+
btn_row.pack(fill="x", pady=(4, 0))
|
|
2904
|
+
|
|
2905
|
+
tk.Label(
|
|
2906
|
+
btn_row,
|
|
2907
|
+
text="عرض:",
|
|
2908
|
+
font=(font_fa, 8),
|
|
2909
|
+
fg="#CBD5E1",
|
|
2910
|
+
bg="#1E293B",
|
|
2911
|
+
).pack(side="left", padx=(0, 3))
|
|
2912
|
+
|
|
2913
|
+
sp_rw = tk.Spinbox(
|
|
2914
|
+
btn_row,
|
|
2915
|
+
from_=80,
|
|
2916
|
+
to=200,
|
|
2917
|
+
width=4,
|
|
2918
|
+
font=("Segoe UI", 8, "bold"),
|
|
2919
|
+
bg="#0F172A",
|
|
2920
|
+
fg="#F8FAFC",
|
|
2921
|
+
buttonbackground="#334155",
|
|
2922
|
+
)
|
|
2923
|
+
sp_rw.delete(0, "end")
|
|
2924
|
+
sp_rw.insert(0, str(self.config.get("replace_clock_width", 118)))
|
|
2925
|
+
|
|
2926
|
+
def on_rw_change(*args):
|
|
2927
|
+
try:
|
|
2928
|
+
val = int(sp_rw.get())
|
|
2929
|
+
if 80 <= val <= 200:
|
|
2930
|
+
self.config["replace_clock_width"] = val
|
|
2931
|
+
self._save_config()
|
|
2932
|
+
if self.clock_replacer:
|
|
2933
|
+
self.clock_replacer.refresh(get_today_info(CUSTOM_OCCASIONS_PATH))
|
|
2934
|
+
except ValueError:
|
|
2935
|
+
pass
|
|
2936
|
+
|
|
2937
|
+
sp_rw.configure(command=on_rw_change)
|
|
2938
|
+
sp_rw.bind("<Return>", on_rw_change)
|
|
2939
|
+
sp_rw.bind("<FocusOut>", on_rw_change)
|
|
2940
|
+
sp_rw.pack(side="left", padx=(0, 6))
|
|
2941
|
+
|
|
2942
|
+
tk.Button(
|
|
2943
|
+
btn_row,
|
|
2944
|
+
text="🔄 مکان پیشفرض",
|
|
2945
|
+
font=(font_fa, 8),
|
|
2946
|
+
bg="#334155",
|
|
2947
|
+
fg="#F8FAFC",
|
|
2948
|
+
relief="flat",
|
|
2949
|
+
cursor="hand2",
|
|
2950
|
+
command=on_reset_replacer_pos,
|
|
2951
|
+
).pack(side="left", padx=(0, 5))
|
|
2952
|
+
|
|
2953
|
+
tk.Button(
|
|
2954
|
+
btn_row,
|
|
2955
|
+
text="🎨 رنگ خودکار",
|
|
2956
|
+
font=(font_fa, 8),
|
|
2957
|
+
bg="#334155",
|
|
2958
|
+
fg="#38BDF8",
|
|
2959
|
+
relief="flat",
|
|
2960
|
+
cursor="hand2",
|
|
2961
|
+
command=on_auto_replacer_bg,
|
|
2962
|
+
).pack(side="left", padx=(0, 5))
|
|
2963
|
+
|
|
2964
|
+
tk.Button(
|
|
2965
|
+
btn_row,
|
|
2966
|
+
text="🎨 رنگ دلخواه",
|
|
2967
|
+
font=(font_fa, 8),
|
|
2968
|
+
bg="#334155",
|
|
2969
|
+
fg="#F8FAFC",
|
|
2970
|
+
relief="flat",
|
|
2971
|
+
cursor="hand2",
|
|
2972
|
+
command=on_pick_replacer_bg,
|
|
2973
|
+
).pack(side="left", padx=(0, 5))
|
|
2974
|
+
|
|
2975
|
+
tk.Button(
|
|
2976
|
+
btn_row,
|
|
2977
|
+
text="بازنشانی ساعت شناور",
|
|
2978
|
+
font=(font_fa, 8),
|
|
2979
|
+
bg="#334155",
|
|
2980
|
+
fg="#CBD5E1",
|
|
2981
|
+
relief="flat",
|
|
2982
|
+
cursor="hand2",
|
|
2983
|
+
command=on_tb_reset_pos,
|
|
2984
|
+
).pack(side="left")
|
|
2985
|
+
|
|
2986
|
+
bottom_bar = tk.Frame(container, bg="#0F172A")
|
|
2987
|
+
bottom_bar.pack(fill="x", pady=(8, 0))
|
|
2988
|
+
|
|
2989
|
+
tk.Button(
|
|
2990
|
+
bottom_bar,
|
|
2991
|
+
text="بستن و ذخیره",
|
|
2992
|
+
font=(font_fa, 9, "bold"),
|
|
2993
|
+
bg="#0284C7",
|
|
2994
|
+
fg="#FFFFFF",
|
|
2995
|
+
padx=16,
|
|
2996
|
+
pady=4,
|
|
2997
|
+
relief="flat",
|
|
2998
|
+
cursor="hand2",
|
|
2999
|
+
command=win.destroy,
|
|
3000
|
+
).pack(side="left")
|
|
3001
|
+
|
|
3002
|
+
tk.Label(
|
|
3003
|
+
bottom_bar,
|
|
3004
|
+
text="® Shayan Bamyar",
|
|
3005
|
+
font=("Segoe UI", 8, "bold"),
|
|
3006
|
+
fg="#64748B",
|
|
3007
|
+
bg="#0F172A",
|
|
3008
|
+
).pack(side="left", padx=14)
|
|
3009
|
+
|
|
3010
|
+
tk.Button(
|
|
3011
|
+
bottom_bar,
|
|
3012
|
+
text="بازنشانی پیشفرض",
|
|
3013
|
+
font=(font_fa, 8),
|
|
3014
|
+
bg="#334155",
|
|
3015
|
+
fg="#F8FAFC",
|
|
3016
|
+
padx=10,
|
|
3017
|
+
pady=4,
|
|
3018
|
+
relief="flat",
|
|
3019
|
+
cursor="hand2",
|
|
3020
|
+
command=lambda: self._reset_all_defaults(win),
|
|
3021
|
+
).pack(side="right")
|
|
3022
|
+
|
|
3023
|
+
def _on_opacity_slider(self, val: int):
|
|
3024
|
+
self.config["opacity"] = round(val / 100.0, 2)
|
|
3025
|
+
self._save_config()
|
|
3026
|
+
self._apply_all_styles()
|
|
3027
|
+
|
|
3028
|
+
def _on_scale_slider(self, val: int):
|
|
3029
|
+
self.config["widget_scale"] = int(val)
|
|
3030
|
+
self._save_config()
|
|
3031
|
+
self._apply_all_styles()
|
|
3032
|
+
|
|
3033
|
+
def _pick_color(self, cfg_key: str):
|
|
3034
|
+
initial = self.config.get(cfg_key, "#FFFFFF")
|
|
3035
|
+
color_code = colorchooser.askcolor(
|
|
3036
|
+
color=initial,
|
|
3037
|
+
title="انتخاب رنگ دلخواه",
|
|
3038
|
+
parent=self.settings_win if self.settings_win else self.root,
|
|
3039
|
+
)
|
|
3040
|
+
if color_code and color_code[1]:
|
|
3041
|
+
hex_c = color_code[1]
|
|
3042
|
+
self.config[cfg_key] = hex_c
|
|
3043
|
+
self._save_config()
|
|
3044
|
+
if cfg_key in self._color_previews and self._color_previews[cfg_key].winfo_exists():
|
|
3045
|
+
self._color_previews[cfg_key].configure(bg=hex_c)
|
|
3046
|
+
self._apply_all_styles()
|
|
3047
|
+
|
|
3048
|
+
def _reset_all_defaults(self, settings_win):
|
|
3049
|
+
x, y = self.config.get("x"), self.config.get("y")
|
|
3050
|
+
self.config = DEFAULT_CONFIG.copy()
|
|
3051
|
+
self.config["x"] = x
|
|
3052
|
+
self.config["y"] = y
|
|
3053
|
+
self._save_config()
|
|
3054
|
+
self._apply_all_styles()
|
|
3055
|
+
self.sync_taskbar_features()
|
|
3056
|
+
if settings_win and settings_win.winfo_exists():
|
|
3057
|
+
settings_win.destroy()
|
|
3058
|
+
self.open_settings_window()
|
|
3059
|
+
|
|
3060
|
+
def _on_tray_left_click(self):
|
|
3061
|
+
def _act():
|
|
3062
|
+
if self._is_minimized:
|
|
3063
|
+
self.restore_widget()
|
|
3064
|
+
return
|
|
3065
|
+
if self.widget_win:
|
|
3066
|
+
self.widget_win.deiconify()
|
|
3067
|
+
if self.lock_win:
|
|
3068
|
+
self.lock_win.deiconify()
|
|
3069
|
+
self._toggle_calendar(None)
|
|
3070
|
+
self.root.after(0, _act)
|
|
3071
|
+
|
|
3072
|
+
def _on_tray_right_click(self):
|
|
3073
|
+
def _popup():
|
|
3074
|
+
self._populate_context_menu()
|
|
3075
|
+
x = self.root.winfo_pointerx()
|
|
3076
|
+
y = self.root.winfo_pointery()
|
|
3077
|
+
self.root.focus_force()
|
|
3078
|
+
try:
|
|
3079
|
+
self.menu.tk_popup(x, y)
|
|
3080
|
+
finally:
|
|
3081
|
+
self.menu.grab_release()
|
|
3082
|
+
self.root.after(0, _popup)
|
|
3083
|
+
|
|
3084
|
+
def _refresh_display(self):
|
|
3085
|
+
info = get_today_info(CUSTOM_OCCASIONS_PATH)
|
|
3086
|
+
self.current_jy = info["jy"]
|
|
3087
|
+
self.current_jm = info["jm"]
|
|
3088
|
+
self.current_jd = info["jd"]
|
|
3089
|
+
|
|
3090
|
+
prev_count = len(self.alarm_store.alarms)
|
|
3091
|
+
due_alarms = self.alarm_store.check_due_alarms(info)
|
|
3092
|
+
if len(self.alarm_store.alarms) != prev_count:
|
|
3093
|
+
if self.alarm_win and self.alarm_win.win and self.alarm_win.win.winfo_exists():
|
|
3094
|
+
self.alarm_win._refresh_list()
|
|
3095
|
+
self.alarm_win._render_picker_calendar()
|
|
3096
|
+
if self.config.get("calendar_expanded"):
|
|
3097
|
+
self._render_calendar_month()
|
|
3098
|
+
|
|
3099
|
+
for alarm in due_alarms:
|
|
3100
|
+
if alarm.get("show_popup", True) or alarm.get("play_sound", True):
|
|
3101
|
+
AnimatedBellPopup(
|
|
3102
|
+
root=self.root,
|
|
3103
|
+
alarm=alarm,
|
|
3104
|
+
font_family=self.config.get("font_family", "Vazir"),
|
|
3105
|
+
on_snooze=lambda a: self.alarm_store.add_snooze(a, 5),
|
|
3106
|
+
on_dismiss=self._on_alarm_dismissed,
|
|
3107
|
+
)
|
|
3108
|
+
|
|
3109
|
+
use_fa = self.config.get("persian_digits", False)
|
|
3110
|
+
is_locked = bool(self.config.get("locked", False))
|
|
3111
|
+
scale = max(70, min(200, int(self.config.get("widget_scale", 100)))) / 100.0
|
|
3112
|
+
|
|
3113
|
+
c_size = max(10, int(self.config.get("clock_font_size", 26) * scale))
|
|
3114
|
+
d_size = max(6, int(self.config.get("date_font_size", 11) * scale))
|
|
3115
|
+
o_size = max(6, int(self.config.get("occasion_font_size", 9) * scale))
|
|
3116
|
+
|
|
3117
|
+
font_fa = self.config.get("font_family", "Vazir")
|
|
3118
|
+
font_clock = self.config.get("clock_font_family", "Vazir")
|
|
3119
|
+
|
|
3120
|
+
# فقط متون پویا را پاک میکنیم تا نوار ابزار (toolbar) و وضعیت Hover آیکونها هرگز ریست نشوند
|
|
3121
|
+
self.canvas.delete("drawn_text")
|
|
3122
|
+
|
|
3123
|
+
toolbar_x = self.LOCK_OFFSET_X + self.LOCK_BTN_SIZE + 8
|
|
3124
|
+
toolbar_y = self.LOCK_OFFSET_Y
|
|
3125
|
+
# فضای نوار ابزار همیشه رزرو میشود (چه قفل چه باز) تا ابعاد و مکان ویجت
|
|
3126
|
+
# بعد از قفل/باز کردن هیچ تغییری نکند و ویجت روی صفحه جابهجا نشود
|
|
3127
|
+
self.canvas.coords(self._toolbar_win_id, toolbar_x, toolbar_y)
|
|
3128
|
+
self.btn_container.update_idletasks()
|
|
3129
|
+
toolbar_right = toolbar_x + self.btn_container.winfo_reqwidth()
|
|
3130
|
+
if not is_locked:
|
|
3131
|
+
self.canvas.itemconfigure(self._toolbar_win_id, state="normal")
|
|
3132
|
+
else:
|
|
3133
|
+
self.canvas.itemconfigure(self._toolbar_win_id, state="hidden")
|
|
3134
|
+
|
|
3135
|
+
time_str = format_digits(info["time_str"], use_fa)
|
|
3136
|
+
date_str = format_digits(info["date_text"], use_fa)
|
|
3137
|
+
|
|
3138
|
+
today_alarms = self.alarm_store.get_events_for_jalali_day(
|
|
3139
|
+
info["jy"], info["jm"], info["jd"], info["persian_week_idx"]
|
|
3140
|
+
)
|
|
3141
|
+
occ_parts = []
|
|
3142
|
+
if info["has_occasion"] and info["occasion"]:
|
|
3143
|
+
occ_parts.append(info["occasion"])
|
|
3144
|
+
for a in today_alarms:
|
|
3145
|
+
occ_parts.append(f"⏰ {int(a['hour']):02d}:{int(a['minute']):02d} {a['title']}")
|
|
3146
|
+
occ_full = " | ".join(occ_parts)
|
|
3147
|
+
if len(occ_full) > 40:
|
|
3148
|
+
occ_full = occ_full[:39] + "…"
|
|
3149
|
+
occ_str = format_digits(occ_full, use_fa) if occ_full else ""
|
|
3150
|
+
|
|
3151
|
+
tmp_t = self.canvas.create_text(0, 0, text=time_str, font=(font_clock, c_size, "bold"), anchor="nw")
|
|
3152
|
+
tmp_d = self.canvas.create_text(0, 0, text=date_str, font=(font_fa, d_size, "bold"), anchor="nw")
|
|
3153
|
+
tb_t = self.canvas.bbox(tmp_t) or (0, 0, 90, 32)
|
|
3154
|
+
tb_d = self.canvas.bbox(tmp_d) or (0, 0, 140, 20)
|
|
3155
|
+
time_w, time_h = tb_t[2] - tb_t[0], tb_t[3] - tb_t[1]
|
|
3156
|
+
date_w, date_h = tb_d[2] - tb_d[0], tb_d[3] - tb_d[1]
|
|
3157
|
+
|
|
3158
|
+
occ_w, occ_h = 0, 0
|
|
3159
|
+
if occ_str:
|
|
3160
|
+
tmp_o = self.canvas.create_text(0, 0, text=occ_str, font=(font_fa, o_size), anchor="nw")
|
|
3161
|
+
tb_o = self.canvas.bbox(tmp_o) or (0, 0, 120, 16)
|
|
3162
|
+
occ_w, occ_h = tb_o[2] - tb_o[0], tb_o[3] - tb_o[1]
|
|
3163
|
+
|
|
3164
|
+
self.canvas.delete(tmp_t, tmp_d)
|
|
3165
|
+
if occ_str:
|
|
3166
|
+
self.canvas.delete(tmp_o)
|
|
3167
|
+
|
|
3168
|
+
if is_locked:
|
|
3169
|
+
top_needed_w = self.LOCK_OFFSET_X + self.LOCK_BTN_SIZE + 10 + time_w + 14
|
|
3170
|
+
body_needed_w = max(date_w, occ_w) + 24
|
|
3171
|
+
target_w = max(top_needed_w, body_needed_w, int(130 * scale))
|
|
3172
|
+
else:
|
|
3173
|
+
top_needed_w = toolbar_right + time_w + 38
|
|
3174
|
+
body_needed_w = max(date_w, occ_w) + 32
|
|
3175
|
+
target_w = max(int(self.BASE_MIN_W * scale), top_needed_w, body_needed_w)
|
|
3176
|
+
|
|
3177
|
+
right_x = target_w - 14
|
|
3178
|
+
cur_y = 6
|
|
3179
|
+
|
|
3180
|
+
draw_outlined_canvas_text(
|
|
3181
|
+
self.canvas,
|
|
3182
|
+
right_x,
|
|
3183
|
+
cur_y,
|
|
3184
|
+
text=time_str,
|
|
3185
|
+
font_spec=(font_clock, c_size, "bold"),
|
|
3186
|
+
fill=self.config.get("clock_color", "#F8FAFC"),
|
|
3187
|
+
outline="#000000",
|
|
3188
|
+
anchor="ne",
|
|
3189
|
+
justify="right",
|
|
3190
|
+
tags="drawn_text",
|
|
3191
|
+
)
|
|
3192
|
+
cur_y += max(time_h, self.LOCK_BTN_SIZE + 4) + 4
|
|
3193
|
+
|
|
3194
|
+
draw_outlined_canvas_text(
|
|
3195
|
+
self.canvas,
|
|
3196
|
+
right_x,
|
|
3197
|
+
cur_y,
|
|
3198
|
+
text=date_str,
|
|
3199
|
+
font_spec=(font_fa, d_size, "bold"),
|
|
3200
|
+
fill=self.config.get("holiday_color", "#F87171")
|
|
3201
|
+
if info["is_holiday"]
|
|
3202
|
+
else self.config.get("date_color", "#CBD5E1"),
|
|
3203
|
+
outline="#000000",
|
|
3204
|
+
anchor="ne",
|
|
3205
|
+
justify="right",
|
|
3206
|
+
tags="drawn_text",
|
|
3207
|
+
)
|
|
3208
|
+
cur_y += date_h + 5
|
|
3209
|
+
|
|
3210
|
+
if occ_str:
|
|
3211
|
+
draw_outlined_canvas_text(
|
|
3212
|
+
self.canvas,
|
|
3213
|
+
right_x,
|
|
3214
|
+
cur_y,
|
|
3215
|
+
text=occ_str,
|
|
3216
|
+
font_spec=(font_fa, o_size),
|
|
3217
|
+
fill=self.config.get("holiday_color", "#F87171")
|
|
3218
|
+
if (info["is_holiday"] and info["has_occasion"])
|
|
3219
|
+
else self.config.get("occasion_color", "#94A3B8"),
|
|
3220
|
+
outline="#000000",
|
|
3221
|
+
anchor="ne",
|
|
3222
|
+
justify="right",
|
|
3223
|
+
tags="drawn_text",
|
|
3224
|
+
)
|
|
3225
|
+
cur_y += occ_h + 4
|
|
3226
|
+
|
|
3227
|
+
footer_y = cur_y + 2
|
|
3228
|
+
if self._toast_text:
|
|
3229
|
+
draw_outlined_canvas_text(
|
|
3230
|
+
self.canvas,
|
|
3231
|
+
10,
|
|
3232
|
+
footer_y,
|
|
3233
|
+
text=self._toast_text,
|
|
3234
|
+
font_spec=(font_fa, 8, "bold"),
|
|
3235
|
+
fill="#10B981",
|
|
3236
|
+
outline="#000000",
|
|
3237
|
+
anchor="nw",
|
|
3238
|
+
tags="drawn_text",
|
|
3239
|
+
)
|
|
3240
|
+
target_h = footer_y + 18
|
|
3241
|
+
elif not is_locked:
|
|
3242
|
+
draw_outlined_canvas_text(
|
|
3243
|
+
self.canvas,
|
|
3244
|
+
10,
|
|
3245
|
+
footer_y,
|
|
3246
|
+
text="® Shayan Bamyar",
|
|
3247
|
+
font_spec=("Segoe UI", 7, "bold"),
|
|
3248
|
+
fill="#64748B",
|
|
3249
|
+
outline="#000000",
|
|
3250
|
+
anchor="nw",
|
|
3251
|
+
tags="drawn_text",
|
|
3252
|
+
)
|
|
3253
|
+
target_h = footer_y + 18
|
|
3254
|
+
else:
|
|
3255
|
+
# در حالت قفل، فوتر @shayan bamyar حذف میشود و ارتفاع به حداقل ممکن کاهش مییابد
|
|
3256
|
+
target_h = cur_y + 6
|
|
3257
|
+
|
|
3258
|
+
size_changed = (target_w != self._computed_w) or (target_h != self._computed_h)
|
|
3259
|
+
self._computed_w = target_w
|
|
3260
|
+
self._computed_h = target_h
|
|
3261
|
+
|
|
3262
|
+
if self.widget_win and not getattr(self, "_is_minimized", False):
|
|
3263
|
+
screen_w = self.root.winfo_screenwidth()
|
|
3264
|
+
screen_h = self.root.winfo_screenheight()
|
|
3265
|
+
cur_cx = self.widget_win.winfo_x()
|
|
3266
|
+
cur_cy = self.widget_win.winfo_y()
|
|
3267
|
+
if is_locked:
|
|
3268
|
+
snap_side = self.config.get("snap_side", "right")
|
|
3269
|
+
target_x = (screen_w - target_w) if snap_side == "right" else 0
|
|
3270
|
+
target_y = max(0, min(cur_cy, screen_h - target_h - 40))
|
|
3271
|
+
else:
|
|
3272
|
+
target_x = max(0, min(cur_cx, screen_w - target_w))
|
|
3273
|
+
target_y = max(0, min(cur_cy, screen_h - target_h - 40))
|
|
3274
|
+
|
|
3275
|
+
if size_changed or (cur_cx != target_x) or (cur_cy != target_y):
|
|
3276
|
+
self.widget_win.geometry(f"{target_w}x{target_h}+{int(target_x)}+{int(target_y)}")
|
|
3277
|
+
self._position_lock_window()
|
|
3278
|
+
if not self.config.get("transparent_bg", False):
|
|
3279
|
+
apply_rounded_corners(self.widget_win, target_w, target_h, radius=10 if is_locked else 16, enabled=True)
|
|
3280
|
+
if self.config.get("calendar_expanded"):
|
|
3281
|
+
self._position_calendar_window()
|
|
3282
|
+
|
|
3283
|
+
if hasattr(self, "tray") and self.tray:
|
|
3284
|
+
self.tray.update_tooltip(self._format_tray_tooltip(info))
|
|
3285
|
+
|
|
3286
|
+
if getattr(self, "clock_replacer", None) is not None:
|
|
3287
|
+
try:
|
|
3288
|
+
self.clock_replacer.refresh(info)
|
|
3289
|
+
except Exception:
|
|
3290
|
+
pass
|
|
3291
|
+
if getattr(self, "taskbar_clock", None) is not None:
|
|
3292
|
+
try:
|
|
3293
|
+
self.taskbar_clock.refresh(info)
|
|
3294
|
+
except Exception:
|
|
3295
|
+
pass
|
|
3296
|
+
if getattr(self, "taskbar_proxy", None) is not None:
|
|
3297
|
+
self.taskbar_proxy.update_title(
|
|
3298
|
+
format_digits(f"{info['time_str'][:5]} | {info['date_text']}", use_fa)
|
|
3299
|
+
)
|
|
3300
|
+
|
|
3301
|
+
def _update_clock_loop(self):
|
|
3302
|
+
self._refresh_display()
|
|
3303
|
+
self.root.after(1000, self._update_clock_loop)
|
|
3304
|
+
|
|
3305
|
+
def _keep_z_order_loop(self):
|
|
3306
|
+
if not self._is_dragging and not getattr(self, "_is_minimized", False) and self.widget_win and self.widget_win.winfo_viewable():
|
|
3307
|
+
z_mode = self.config.get("z_mode", "topmost")
|
|
3308
|
+
is_locked = bool(self.config.get("locked", False))
|
|
3309
|
+
apply_z_order_and_clickthrough(self.widget_win, z_mode, click_through=is_locked)
|
|
3310
|
+
if self.lock_win and self.lock_win.winfo_viewable():
|
|
3311
|
+
apply_z_order_and_clickthrough(self.lock_win, z_mode, click_through=False)
|
|
3312
|
+
if self.cal_win and self.config.get("calendar_expanded"):
|
|
3313
|
+
apply_z_order_and_clickthrough(self.cal_win, z_mode, click_through=is_locked)
|
|
3314
|
+
# حفظ پنجره کپی زمان/GUID و Tooltipها روی ویجت تا هرگز زیر ویجت نروند
|
|
3315
|
+
if self.dev_popup_win and self.dev_popup_win.winfo_exists():
|
|
3316
|
+
apply_z_order_and_clickthrough(self.dev_popup_win, "topmost", click_through=False)
|
|
3317
|
+
for tip in self._tooltips:
|
|
3318
|
+
if tip.tip_win and tip.tip_win.winfo_exists():
|
|
3319
|
+
apply_z_order_and_clickthrough(tip.tip_win, "topmost", click_through=True)
|
|
3320
|
+
if getattr(self, "clock_replacer", None) is not None:
|
|
3321
|
+
self.clock_replacer.keep_on_top()
|
|
3322
|
+
if getattr(self, "taskbar_clock", None) is not None:
|
|
3323
|
+
self.taskbar_clock.keep_on_top()
|
|
3324
|
+
self.root.after(800, self._keep_z_order_loop)
|
|
3325
|
+
|
|
3326
|
+
def quit_app(self):
|
|
3327
|
+
try:
|
|
3328
|
+
if getattr(self, "clock_replacer", None) is not None:
|
|
3329
|
+
self.clock_replacer.destroy()
|
|
3330
|
+
if getattr(self, "taskbar_clock", None) is not None:
|
|
3331
|
+
self.taskbar_clock.destroy()
|
|
3332
|
+
if getattr(self, "taskbar_proxy", None) is not None:
|
|
3333
|
+
self.taskbar_proxy.destroy()
|
|
3334
|
+
except Exception:
|
|
3335
|
+
pass
|
|
3336
|
+
try:
|
|
3337
|
+
if hasattr(self, "tray") and self.tray:
|
|
3338
|
+
self.tray.stop()
|
|
3339
|
+
except Exception:
|
|
3340
|
+
pass
|
|
3341
|
+
self.root.quit()
|
|
3342
|
+
self.root.destroy()
|
|
3343
|
+
sys.exit(0)
|
|
3344
|
+
|
|
3345
|
+
def run(self):
|
|
3346
|
+
self.root.mainloop()
|
|
3347
|
+
|
|
3348
|
+
|
|
3349
|
+
if __name__ == "__main__":
|
|
3350
|
+
app = PersianClockWidget()
|
|
3351
|
+
app.run()
|