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/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()