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.
@@ -0,0 +1,834 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ نمایش ساعت و تاریخ شمسی در تسک‌بار ویندوز:
4
+
5
+ ۱. WindowsClockReplacer: جایگزینی کامل ساعت و تاریخ پیش‌فرض انگلیسی ویندوز
6
+ - یک باکس سالید و مات که دقیقاً روی ساعت و تاریخ ویندوز در گوشه تسک‌بار می‌نشیند
7
+ - ساعت انگلیسی ویندوز را ۱۰۰٪ می‌پوشاند و مخفی می‌کند
8
+ - ساعت تهران و تاریخ شمسی را با فونت وزیر و خط دور مشکی نمایش می‌دهد
9
+ - کلیک چپ: باز/بسته کردن تقویم ماهانه
10
+ - کلیک راست: منوی تنظیمات
11
+ - قابلیت جابه‌جایی دستی با کشیدن ماوس (Drag)
12
+
13
+ ۲. TaskbarClockOverlay: ساعت و تاریخ شفاف شناور روی تسک‌بار (کنار ساعت ویندوز)
14
+
15
+ ۳. TaskbarButtonProxy: آیکون/دکمه برنامه در تسک‌بار ویندوز
16
+
17
+ طراحی و توسعه: شایان بامیار (® Shayan Bamyar)
18
+ """
19
+
20
+ import ctypes
21
+ from ctypes import wintypes
22
+ import os
23
+ import sys
24
+ import time
25
+ import tkinter as tk
26
+ from tkinter import font as tkfont
27
+
28
+ try:
29
+ import winreg
30
+ except ImportError:
31
+ winreg = None
32
+
33
+ IS_WIN = sys.platform == "win32"
34
+
35
+ try:
36
+ from src.alarm_manager import draw_outlined_canvas_text
37
+ from src.win_utils import (
38
+ apply_z_order_and_clickthrough,
39
+ get_resource_path,
40
+ hide_from_taskbar,
41
+ )
42
+ except ImportError:
43
+ from alarm_manager import draw_outlined_canvas_text
44
+ from win_utils import (
45
+ apply_z_order_and_clickthrough,
46
+ get_resource_path,
47
+ hide_from_taskbar,
48
+ )
49
+
50
+ TB_TRANSPARENT_KEY = "#010204"
51
+
52
+
53
+ def get_taskbar_geometry():
54
+ """
55
+ دریافت مختصات تسک‌بار اصلی ویندوز با ترکیب SystemParametersInfo و Win32 API
56
+ خروجی: (left, top, right, bottom, tray_left یا None)
57
+ """
58
+ if not IS_WIN:
59
+ return 0, 1032, 1920, 1080, None
60
+ try:
61
+ user32 = ctypes.windll.user32
62
+ sw = user32.GetSystemMetrics(0)
63
+ sh = user32.GetSystemMetrics(1)
64
+ r = wintypes.RECT()
65
+ # SPI_GETWORKAREA = 0x0030
66
+ user32.SystemParametersInfoW(0x0030, 0, ctypes.byref(r), 0)
67
+
68
+ if r.bottom < sh:
69
+ tb_l, tb_t, tb_r, tb_b = 0, r.bottom, sw, sh
70
+ elif r.top > 0:
71
+ tb_l, tb_t, tb_r, tb_b = 0, 0, sw, r.top
72
+ elif r.right < sw:
73
+ tb_l, tb_t, tb_r, tb_b = r.right, 0, sw, sh
74
+ elif r.left > 0:
75
+ tb_l, tb_t, tb_r, tb_b = 0, 0, r.left, sh
76
+ else:
77
+ tb_l, tb_t, tb_r, tb_b = 0, sh - 48, sw, sh
78
+
79
+ tray_left = None
80
+ hwnd = user32.FindWindowW("Shell_TrayWnd", None)
81
+ if hwnd:
82
+ tray_hwnd = user32.FindWindowExW(hwnd, 0, "TrayNotifyWnd", None)
83
+ if tray_hwnd:
84
+ t_rect = wintypes.RECT()
85
+ user32.GetWindowRect(tray_hwnd, ctypes.byref(t_rect))
86
+ if tb_l <= t_rect.left < tb_r and (t_rect.right - t_rect.left) > 20:
87
+ tray_left = t_rect.left
88
+
89
+ return tb_l, tb_t, tb_r, tb_b, tray_left
90
+ except Exception:
91
+ sw = 1920
92
+ sh = 1080
93
+ return 0, sh - 48, sw, sh, None
94
+
95
+
96
+ def is_taskbar_light_theme() -> bool:
97
+ """تشخیص تم روشن یا تاریک تسک‌بار ویندوز از رجیستری"""
98
+ if not IS_WIN or winreg is None:
99
+ return False
100
+ try:
101
+ with winreg.OpenKey(
102
+ winreg.HKEY_CURRENT_USER,
103
+ r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize",
104
+ ) as key:
105
+ val, _ = winreg.QueryValueEx(key, "SystemUsesLightTheme")
106
+ return int(val) == 1
107
+ except Exception:
108
+ return False
109
+
110
+
111
+ def detect_taskbar_colors(tb_l: int, tb_t: int, tb_r: int, tb_b: int) -> tuple[str, str, bool]:
112
+ """
113
+ تشخیص دقیق رنگ پس‌زمینه تسک‌بار با نمونه‌برداری پیکسل و بررسی تم ویندوز
114
+ خروجی: (base_bg_hex, hover_bg_hex, is_light)
115
+ """
116
+ is_light = is_taskbar_light_theme()
117
+ default_bg = "#EEEEEE" if is_light else "#1C1C1C"
118
+ default_hover = "#DFDFDF" if is_light else "#2B2B2B"
119
+ if not IS_WIN:
120
+ return default_bg, default_hover, is_light
121
+ try:
122
+ user32 = ctypes.windll.user32
123
+ gdi32 = ctypes.windll.gdi32
124
+ hdc = user32.GetDC(0)
125
+ if hdc:
126
+ sample_x = max(tb_l + 12, tb_r - 240)
127
+ sample_y = tb_t + max(4, (tb_b - tb_t) // 4)
128
+ color_ref = gdi32.GetPixel(hdc, int(sample_x), int(sample_y))
129
+ user32.ReleaseDC(0, hdc)
130
+ if color_ref != -1 and color_ref != 0xFFFFFFFF:
131
+ r = color_ref & 0xFF
132
+ g = (color_ref >> 8) & 0xFF
133
+ b = (color_ref >> 16) & 0xFF
134
+ lum = 0.299 * r + 0.587 * g + 0.114 * b
135
+ # اطمینان از همخوانی منطقی با تم روشن/تیره (تا رنگ تصادفی آیکون یا خطا نگیرد)
136
+ if (not is_light and lum < 110) or (is_light and lum > 150):
137
+ bg_hex = f"#{r:02X}{g:02X}{b:02X}"
138
+ if is_light:
139
+ hr, hg, hb = max(0, r - 18), max(0, g - 18), max(0, b - 18)
140
+ else:
141
+ hr, hg, hb = min(255, r + 18), min(255, g + 18), min(255, b + 18)
142
+ hover_hex = f"#{hr:02X}{hg:02X}{hb:02X}"
143
+ return bg_hex, hover_hex, (lum > 145)
144
+ except Exception:
145
+ pass
146
+ return default_bg, default_hover, is_light
147
+
148
+
149
+ def find_win10_tray_clock_hwnd() -> int | None:
150
+ """یافتن پنجره ساعت کلاسیک ویندوز ۱۰ (TrayClockWClass) در صورت وجود"""
151
+ if not IS_WIN:
152
+ return None
153
+ try:
154
+ user32 = ctypes.windll.user32
155
+ shell = user32.FindWindowW("Shell_TrayWnd", None)
156
+ if not shell:
157
+ return None
158
+ tray = user32.FindWindowExW(shell, 0, "TrayNotifyWnd", None)
159
+ if not tray:
160
+ return None
161
+ clk = user32.FindWindowExW(tray, 0, "TrayClockWClass", None)
162
+ return int(clk) if clk else None
163
+ except Exception:
164
+ return None
165
+
166
+
167
+ def get_windows11_tray_clock_hidden() -> bool:
168
+ """بررسی وضعیت مخفی بودن ساعت و تاریخ انگلیسی در تنظیمات تسک‌بار ویندوز ۱۱"""
169
+ if not IS_WIN or winreg is None:
170
+ return False
171
+ try:
172
+ with winreg.OpenKey(
173
+ winreg.HKEY_CURRENT_USER,
174
+ r"Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced",
175
+ ) as key:
176
+ val, _ = winreg.QueryValueEx(key, "HideSystrayDateTimeValueName")
177
+ return int(val) == 1
178
+ except Exception:
179
+ return False
180
+
181
+
182
+ def set_windows11_tray_clock_hidden(hide: bool) -> bool:
183
+ """
184
+ تنظیم کلید رسمی ویندوز ۱۱ برای نمایش یا حذف ساعت و تاریخ انگلیسی در تسک‌بار:
185
+ HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced\\HideSystrayDateTimeValueName
186
+ """
187
+ if not IS_WIN or winreg is None:
188
+ return False
189
+ try:
190
+ with winreg.CreateKey(
191
+ winreg.HKEY_CURRENT_USER,
192
+ r"Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced",
193
+ ) as key:
194
+ winreg.SetValueEx(key, "HideSystrayDateTimeValueName", 0, winreg.REG_DWORD, 1 if hide else 0)
195
+ # اطلاع‌رسانی به Explorer برای اعمال تنظیمات
196
+ ctypes.windll.user32.SendMessageTimeoutW(0xFFFF, 0x001A, 0, "TraySettings", 0x0002, 500, None)
197
+ return True
198
+ except Exception:
199
+ return False
200
+
201
+
202
+ class WindowsClockReplacer:
203
+ """
204
+ جایگزینی کامل ساعت و تاریخ ویندوز در تسک‌بار:
205
+ - اتصال مستقیم به داخل نوار تسک‌بار ویندوز (Shell_TrayWnd) با SetParent تا واقعاً جزئی از تسک‌بار شود
206
+ - مخفی کردن ساعت کلاسیک ویندوز ۱۰ یا پوشاندن یکپارچه ساعت ویندوز ۱۱ با تشخیص خودکار رنگ تسک‌بار
207
+ - نمایش ساعت تهران و تاریخ شمسی با فونت وزیر
208
+ - کلیک چپ: باز/بسته کردن تقویم ماهانه | کلیک راست: منوی تنظیمات | کشیدن (Drag): تنظیم مکان
209
+ """
210
+
211
+ def __init__(self, app):
212
+ self.app = app
213
+ self.root = app.root
214
+ self._press_x = 0
215
+ self._press_win_x = 0
216
+ self._dragged = False
217
+ self._last_w = 0
218
+ self._last_h = 0
219
+ self._last_rel_x = 0
220
+ self._last_rel_y = 0
221
+ self._is_hovered = False
222
+ self._last_info: dict | None = None
223
+ self._tip_win: tk.Toplevel | None = None
224
+ self._tip_timer: str | None = None
225
+ self._embedded_parent_hwnd: int | None = None
226
+ self._hidden_win10_clock_hwnd: int | None = None
227
+ self._last_color_probe_ts = 0.0
228
+ self._is_light_tb = is_taskbar_light_theme()
229
+
230
+ self.win = tk.Toplevel(self.root)
231
+ self.win.title("WindowsClockReplacer")
232
+ self.win.overrideredirect(True)
233
+ self.win.attributes("-topmost", True)
234
+
235
+ self.base_bg = "#EEEEEE" if self._is_light_tb else "#1C1C1C"
236
+ self.hover_bg = "#DFDFDF" if self._is_light_tb else "#2B2B2B"
237
+ self.win.configure(bg=self.base_bg)
238
+
239
+ self.canvas = tk.Canvas(
240
+ self.win,
241
+ bg=self.base_bg,
242
+ highlightthickness=0,
243
+ bd=0,
244
+ cursor="hand2",
245
+ )
246
+ self.canvas.pack(fill="both", expand=True)
247
+
248
+ # بایندینگ فقط روی canvas (نه self.win) تا رویداد کلیک دوبار اجرا نشود
249
+ self.canvas.bind("<ButtonPress-1>", self._on_press)
250
+ self.canvas.bind("<B1-Motion>", self._on_motion)
251
+ self.canvas.bind("<ButtonRelease-1>", self._on_release)
252
+ self.canvas.bind("<Button-3>", self._on_right_click)
253
+ self.canvas.bind("<Enter>", self._on_enter)
254
+ self.canvas.bind("<Leave>", self._on_leave)
255
+
256
+ self.win.after(50, self._attach_to_taskbar)
257
+
258
+ def _on_enter(self, event=None):
259
+ self._is_hovered = True
260
+ self.canvas.configure(bg=self.hover_bg)
261
+ self.win.configure(bg=self.hover_bg)
262
+ if self._tip_timer:
263
+ try:
264
+ self.win.after_cancel(self._tip_timer)
265
+ except Exception:
266
+ pass
267
+ self._tip_timer = self.win.after(120, self._show_tip)
268
+
269
+ def _on_leave(self, event=None):
270
+ self._is_hovered = False
271
+ if self._tip_timer:
272
+ try:
273
+ self.win.after_cancel(self._tip_timer)
274
+ except Exception:
275
+ pass
276
+ self._tip_timer = None
277
+ self.canvas.configure(bg=self.base_bg)
278
+ self.win.configure(bg=self.base_bg)
279
+ self._hide_tip()
280
+
281
+ def _show_tip(self):
282
+ if not self._is_hovered or self._tip_win is not None or not self._last_info:
283
+ return
284
+ info = self._last_info
285
+ day_n = info.get("day_name", "")
286
+ dt_txt = info.get("date_text", "")
287
+ occ = info.get("occasion_text")
288
+ tip_text = f"{day_n} {dt_txt}"
289
+ if occ:
290
+ tip_text += f"\n{occ}"
291
+ try:
292
+ win = tk.Toplevel(self.win)
293
+ self._tip_win = win
294
+ win.overrideredirect(True)
295
+ win.attributes("-topmost", True)
296
+ lbl = tk.Label(
297
+ win,
298
+ text=tip_text,
299
+ font=(self.app.config.get("font_family", "Vazir"), 8, "bold"),
300
+ bg="#0F172A",
301
+ fg="#F8FAFC",
302
+ bd=1,
303
+ relief="solid",
304
+ padx=8,
305
+ pady=4,
306
+ justify="center",
307
+ )
308
+ lbl.pack()
309
+ win.update_idletasks()
310
+ tw = win.winfo_reqwidth()
311
+ th = win.winfo_reqheight()
312
+ wx = self.win.winfo_rootx()
313
+ wy = self.win.winfo_rooty()
314
+ tx = max(8, wx + (self._last_w - tw) // 2)
315
+ ty = max(8, wy - th - 6)
316
+ win.geometry(f"+{int(tx)}+{int(ty)}")
317
+ apply_z_order_and_clickthrough(win, "topmost", click_through=True)
318
+ except Exception:
319
+ pass
320
+
321
+ def _hide_tip(self):
322
+ if self._tip_win is not None:
323
+ try:
324
+ self._tip_win.destroy()
325
+ except Exception:
326
+ pass
327
+ self._tip_win = None
328
+
329
+ def _get_hwnd(self) -> int:
330
+ self.win.update_idletasks()
331
+ wid = self.win.winfo_id()
332
+ try:
333
+ root_hwnd = ctypes.windll.user32.GetAncestor(wid, 2)
334
+ return int(root_hwnd) if root_hwnd else int(wid)
335
+ except Exception:
336
+ return int(wid)
337
+
338
+ def _is_foreground_fullscreen(self) -> bool:
339
+ if not IS_WIN:
340
+ return False
341
+ try:
342
+ user32 = ctypes.windll.user32
343
+ fg = user32.GetForegroundWindow()
344
+ if not fg:
345
+ return False
346
+ cls = ctypes.create_unicode_buffer(64)
347
+ user32.GetClassNameW(fg, cls, 64)
348
+ if cls.value in ("Shell_TrayWnd", "Progman", "WorkerW", "TkTopLevel"):
349
+ return False
350
+ r = wintypes.RECT()
351
+ user32.GetWindowRect(fg, ctypes.byref(r))
352
+ sw = user32.GetSystemMetrics(0)
353
+ sh = user32.GetSystemMetrics(1)
354
+ return r.left <= 0 and r.top <= 0 and r.right >= sw and r.bottom >= sh
355
+ except Exception:
356
+ return False
357
+
358
+ def _attach_to_taskbar(self):
359
+ """تنظیم سبک غیرفعال‌ساز (بدون قفل شدن صف ورودی) و مخفی‌سازی ساعت ویندوز ۱۰ در صورت وجود"""
360
+ if not IS_WIN or not self.win or not self.win.winfo_exists():
361
+ return
362
+ try:
363
+ user32 = ctypes.windll.user32
364
+ hwnd = self._get_hwnd()
365
+ GWL_EXSTYLE = -20
366
+ WS_EX_NOACTIVATE = 0x08000000
367
+ WS_EX_TOOLWINDOW = 0x00000080
368
+ WS_EX_APPWINDOW = 0x00040000
369
+ ex_style = user32.GetWindowLongW(hwnd, GWL_EXSTYLE)
370
+ new_ex = (ex_style | WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW) & ~WS_EX_APPWINDOW
371
+ if new_ex != ex_style:
372
+ user32.SetWindowLongW(hwnd, GWL_EXSTYLE, new_ex)
373
+
374
+ # در ویندوز ۱۰، ساعت کلاسیک TrayClockWClass را مستقیماً مخفی کن
375
+ clk_hwnd = find_win10_tray_clock_hwnd()
376
+ if clk_hwnd and clk_hwnd != self._hidden_win10_clock_hwnd:
377
+ user32.ShowWindow(clk_hwnd, 0) # SW_HIDE
378
+ self._hidden_win10_clock_hwnd = clk_hwnd
379
+ except Exception:
380
+ hide_from_taskbar(self.win)
381
+
382
+ def _position_in_taskbar(self, x: int, y: int, w: int, h: int, tb_l: int, tb_t: int):
383
+ """قرار دادن پنجره دقیقاً در مختصات تسک‌بار"""
384
+ if self._is_foreground_fullscreen():
385
+ if self.win.winfo_viewable():
386
+ self.win.withdraw()
387
+ return
388
+ if not self.win.winfo_viewable():
389
+ self.win.deiconify()
390
+ self.win.geometry(f"{int(w)}x{int(h)}+{int(x)}+{int(y)}")
391
+ self.keep_on_top()
392
+
393
+ def refresh(self, info: dict):
394
+ if not self.win or not self.win.winfo_exists():
395
+ return
396
+ self._last_info = info
397
+ cfg = self.app.config
398
+ use_fa = cfg.get("persian_digits", False)
399
+ font_fa = cfg.get("font_family", "Vazir")
400
+ font_clock = cfg.get("clock_font_family", "Vazir")
401
+
402
+ geo = get_taskbar_geometry()
403
+ if geo:
404
+ tb_l, tb_t, tb_r, tb_b, _ = geo
405
+ else:
406
+ sw = self.root.winfo_screenwidth()
407
+ sh = self.root.winfo_screenheight()
408
+ tb_l, tb_t, tb_r, tb_b = 0, sh - 48, sw, sh
409
+
410
+ if not self._embedded_parent_hwnd:
411
+ self._attach_to_taskbar()
412
+
413
+ # تشخیص خودکار رنگ تسک‌بار هر ۱۰ ثانیه (در حالت auto)
414
+ bg_cfg = cfg.get("replace_clock_bg", "auto")
415
+ if bg_cfg == "auto" or not bg_cfg:
416
+ now_ts = time.monotonic()
417
+ if now_ts - self._last_color_probe_ts > 10.0:
418
+ self._last_color_probe_ts = now_ts
419
+ self.base_bg, self.hover_bg, self._is_light_tb = detect_taskbar_colors(tb_l, tb_t, tb_r, tb_b)
420
+ else:
421
+ self.base_bg = bg_cfg
422
+ self._is_light_tb = False
423
+ self.hover_bg = "#2B2B2B"
424
+
425
+ tb_w = tb_r - tb_l
426
+ tb_h = tb_b - tb_t
427
+ horizontal = tb_w >= tb_h
428
+
429
+ # متن ساعت
430
+ time_txt = str(info.get("time_str", ""))[:5]
431
+ if cfg.get("taskbar_clock_seconds", False):
432
+ time_txt = str(info.get("time_str", ""))
433
+ time_txt = self.app.format_digits_public(time_txt, use_fa)
434
+
435
+ # متن تاریخ شمسی (کوتاه و استاندارد برای قرارگیری در تسک‌بار)
436
+ jy = info.get("jy")
437
+ jm = info.get("jm")
438
+ jd = info.get("jd")
439
+ date_txt = f"{jy:04d}/{jm:02d}/{jd:02d}"
440
+ date_txt = self.app.format_digits_public(date_txt, use_fa)
441
+
442
+ # عرض و ارتفاع باکس پوشش ساعت ویندوز
443
+ win_w = int(cfg.get("replace_clock_width", 118))
444
+ win_h = tb_h if horizontal else 56
445
+
446
+ # موقعیت: دقیقاً روی ساعت پیش‌فرض ویندوز در گوشه راست تسک‌بار
447
+ saved_off = cfg.get("replace_clock_offset", 8)
448
+ if horizontal:
449
+ x = tb_r - win_w - int(saved_off)
450
+ y = tb_t
451
+ else:
452
+ x = tb_l
453
+ y = tb_b - win_h - int(saved_off)
454
+
455
+ self.canvas.delete("all")
456
+ self.canvas.configure(width=win_w, height=win_h)
457
+ bg = self.hover_bg if self._is_hovered else self.base_bg
458
+ self.canvas.configure(bg=bg)
459
+ self.win.configure(bg=bg)
460
+
461
+ # حاشیه ملایم در حالت Hover
462
+ if self._is_hovered:
463
+ outline_c = "#CBD5E1" if self._is_light_tb else "#334155"
464
+ self.canvas.create_rectangle(1, 1, win_w - 2, win_h - 2, outline=outline_c, width=1)
465
+
466
+ is_hol = bool(info.get("is_holiday"))
467
+ if self._is_light_tb and (bg_cfg == "auto" or not bg_cfg):
468
+ clock_color = "#0F172A"
469
+ date_color = "#DC2626" if is_hol else "#334155"
470
+ else:
471
+ clock_color = cfg.get("clock_color", "#F8FAFC")
472
+ date_color = cfg.get("holiday_color", "#F87171") if is_hol else cfg.get("date_color", "#CBD5E1")
473
+
474
+ # موقعیت عمودی هماهنگ با تسک‌بار ویندوز ۱۰ و ۱۱
475
+ y_time = max(3, int(win_h * 0.10))
476
+ y_date = max(y_time + 17, int(win_h * 0.52))
477
+
478
+ if self._is_light_tb and (bg_cfg == "auto" or not bg_cfg):
479
+ self.canvas.create_text(
480
+ win_w // 2,
481
+ y_time,
482
+ text=time_txt,
483
+ font=(font_clock, 10, "bold"),
484
+ fill=clock_color,
485
+ anchor="n",
486
+ )
487
+ self.canvas.create_text(
488
+ win_w // 2,
489
+ y_date,
490
+ text=date_txt,
491
+ font=(font_fa, 8, "bold"),
492
+ fill=date_color,
493
+ anchor="n",
494
+ )
495
+ else:
496
+ # خط ۱: ساعت به وقت تهران
497
+ draw_outlined_canvas_text(
498
+ self.canvas,
499
+ win_w // 2,
500
+ y_time,
501
+ text=time_txt,
502
+ font_spec=(font_clock, 10, "bold"),
503
+ fill=clock_color,
504
+ outline="#000000",
505
+ anchor="n",
506
+ )
507
+
508
+ # خط ۲: تاریخ شمسی
509
+ draw_outlined_canvas_text(
510
+ self.canvas,
511
+ win_w // 2,
512
+ y_date,
513
+ text=date_txt,
514
+ font_spec=(font_fa, 8, "bold"),
515
+ fill=date_color,
516
+ outline="#000000",
517
+ anchor="n",
518
+ )
519
+
520
+ self._last_w, self._last_h = win_w, win_h
521
+ if not self._dragged:
522
+ self._position_in_taskbar(x, y, win_w, win_h, tb_l, tb_t)
523
+ else:
524
+ self.keep_on_top()
525
+
526
+ def keep_on_top(self):
527
+ if not self.win or not self.win.winfo_exists():
528
+ return
529
+ if self._is_foreground_fullscreen():
530
+ if self.win.winfo_viewable():
531
+ self.win.withdraw()
532
+ return
533
+ if not self.win.winfo_viewable():
534
+ self.win.deiconify()
535
+ apply_z_order_and_clickthrough(self.win, "topmost", click_through=False)
536
+
537
+ def _on_press(self, event):
538
+ self._hide_tip()
539
+ self._press_x = event.x_root
540
+ self._press_win_x = self.win.winfo_rootx()
541
+ self._dragged = False
542
+ return "break"
543
+
544
+ def _on_motion(self, event):
545
+ dx = event.x_root - self._press_x
546
+ if abs(dx) > 4:
547
+ self._dragged = True
548
+ if self._dragged:
549
+ geo = get_taskbar_geometry()
550
+ new_x = self._press_win_x + dx
551
+ tb_l, tb_t = 0, self.win.winfo_rooty()
552
+ if geo:
553
+ tb_l, tb_t, tb_r, _, _ = geo
554
+ new_x = max(tb_l, min(new_x, tb_r - self._last_w))
555
+ self._position_in_taskbar(int(new_x), int(tb_t), self._last_w, self._last_h, tb_l, tb_t)
556
+ return "break"
557
+
558
+ def _on_release(self, event):
559
+ if self._dragged:
560
+ geo = get_taskbar_geometry()
561
+ tb_r = geo[2] if geo else self.root.winfo_screenwidth()
562
+ cur_x = self.win.winfo_rootx()
563
+ offset = tb_r - (cur_x + self._last_w)
564
+ self.app.config["replace_clock_offset"] = max(-10, min(800, int(offset)))
565
+ self.app._save_config()
566
+ self._dragged = False
567
+ return "break"
568
+ self.app.on_taskbar_clicked()
569
+ return "break"
570
+
571
+ def _on_right_click(self, event):
572
+ self._hide_tip()
573
+ self.app.show_menu_at_pointer()
574
+ return "break"
575
+
576
+ def destroy(self):
577
+ if self._tip_timer:
578
+ try:
579
+ self.win.after_cancel(self._tip_timer)
580
+ except Exception:
581
+ pass
582
+ self._tip_timer = None
583
+ self._hide_tip()
584
+ try:
585
+ user32 = ctypes.windll.user32
586
+ if self._hidden_win10_clock_hwnd:
587
+ user32.ShowWindow(self._hidden_win10_clock_hwnd, 5) # SW_SHOW
588
+ self._hidden_win10_clock_hwnd = None
589
+ if self._embedded_parent_hwnd and self.win and self.win.winfo_exists():
590
+ user32.SetParent(self._get_hwnd(), 0)
591
+ self._embedded_parent_hwnd = None
592
+ except Exception:
593
+ pass
594
+ try:
595
+ if self.win and self.win.winfo_exists():
596
+ self.win.destroy()
597
+ except Exception:
598
+ pass
599
+ self.win = None
600
+
601
+
602
+ class TaskbarClockOverlay:
603
+ """ساعت و تاریخ شمسی شناور و شفاف روی نوار تسک‌بار (کنار ساعت ویندوز)"""
604
+
605
+ def __init__(self, app):
606
+ self.app = app
607
+ self.root = app.root
608
+ self._press_x = 0
609
+ self._press_win_x = 0
610
+ self._dragged = False
611
+ self._last_w = 0
612
+ self._last_h = 0
613
+
614
+ self.win = tk.Toplevel(self.root)
615
+ self.win.title("PersianTaskbarClock")
616
+ self.win.overrideredirect(True)
617
+ self.win.configure(bg=TB_TRANSPARENT_KEY)
618
+ self.win.attributes("-transparentcolor", TB_TRANSPARENT_KEY)
619
+
620
+ self.canvas = tk.Canvas(
621
+ self.win,
622
+ bg=TB_TRANSPARENT_KEY,
623
+ highlightthickness=0,
624
+ bd=0,
625
+ cursor="hand2",
626
+ )
627
+ self.canvas.pack(fill="both", expand=True)
628
+
629
+ # بایندینگ فقط روی canvas تا رویداد کلیک دوبار اجرا نشود
630
+ self.canvas.bind("<ButtonPress-1>", self._on_press)
631
+ self.canvas.bind("<B1-Motion>", self._on_motion)
632
+ self.canvas.bind("<ButtonRelease-1>", self._on_release)
633
+ self.canvas.bind("<Button-3>", self._on_right_click)
634
+
635
+ self.win.after(50, lambda: hide_from_taskbar(self.win))
636
+
637
+ def _measure(self, text: str, font_spec: tuple) -> tuple[int, int]:
638
+ f = tkfont.Font(family=font_spec[0], size=font_spec[1], weight="bold")
639
+ return f.measure(text), f.metrics("linespace")
640
+
641
+ def refresh(self, info: dict):
642
+ if not self.win or not self.win.winfo_exists():
643
+ return
644
+ cfg = self.app.config
645
+ use_fa = cfg.get("persian_digits", False)
646
+ font_fa = cfg.get("font_family", "Vazir")
647
+ font_clock = cfg.get("clock_font_family", "Vazir")
648
+
649
+ geo = get_taskbar_geometry()
650
+ if geo:
651
+ tb_l, tb_t, tb_r, tb_b, tray_left = geo
652
+ else:
653
+ sw = self.root.winfo_screenwidth()
654
+ sh = self.root.winfo_screenheight()
655
+ tb_l, tb_t, tb_r, tb_b, tray_left = 0, sh - 48, sw, sh, None
656
+
657
+ tb_w = tb_r - tb_l
658
+ tb_h = tb_b - tb_t
659
+ horizontal = tb_w >= tb_h
660
+
661
+ time_txt = str(info.get("time_str", ""))[:5]
662
+ if cfg.get("taskbar_clock_seconds", False):
663
+ time_txt = str(info.get("time_str", ""))
664
+ date_txt = f"{info.get('weekday_name', '')} {info.get('date_numeric', '')}"
665
+ if info.get("has_occasion") and info.get("is_holiday"):
666
+ date_txt += " • تعطیل"
667
+
668
+ time_txt = self.app.format_digits_public(time_txt, use_fa)
669
+ date_txt = self.app.format_digits_public(date_txt, use_fa)
670
+
671
+ bar_h = tb_h if horizontal else 56
672
+ t_size = 10 if bar_h >= 44 else 9
673
+ d_size = 8 if bar_h >= 44 else 7
674
+ t_spec = (font_clock, t_size, "bold")
675
+ d_spec = (font_fa, d_size, "bold")
676
+ tw, th = self._measure(time_txt, t_spec)
677
+ dw, dh = self._measure(date_txt, d_spec)
678
+
679
+ win_w = max(tw, dw) + 16
680
+ win_h = bar_h if horizontal else (th + dh + 10)
681
+ if not horizontal:
682
+ win_w = tb_w
683
+
684
+ if horizontal:
685
+ saved_off = cfg.get("taskbar_clock_offset")
686
+ if saved_off is None:
687
+ anchor_right = (tray_left if tray_left else tb_r - 240) - 6
688
+ x = anchor_right - win_w
689
+ else:
690
+ x = tb_r - int(saved_off) - win_w
691
+ x = max(tb_l, min(x, tb_r - win_w))
692
+ y = tb_t
693
+ else:
694
+ x = tb_l
695
+ y = tb_b - win_h - 110
696
+
697
+ self.canvas.delete("all")
698
+ self.canvas.configure(width=win_w, height=win_h)
699
+ total_text_h = th + dh
700
+ top = max(0, (win_h - total_text_h) // 2)
701
+ is_hol = bool(info.get("is_holiday"))
702
+ draw_outlined_canvas_text(
703
+ self.canvas,
704
+ win_w // 2,
705
+ top,
706
+ text=time_txt,
707
+ font_spec=t_spec,
708
+ fill=cfg.get("clock_color", "#F8FAFC"),
709
+ outline="#000000",
710
+ anchor="n",
711
+ )
712
+ draw_outlined_canvas_text(
713
+ self.canvas,
714
+ win_w // 2,
715
+ top + th,
716
+ text=date_txt,
717
+ font_spec=d_spec,
718
+ fill=cfg.get("holiday_color", "#F87171") if is_hol else cfg.get("date_color", "#CBD5E1"),
719
+ outline="#000000",
720
+ anchor="n",
721
+ )
722
+
723
+ self._last_w, self._last_h = win_w, win_h
724
+
725
+ if not self._dragged:
726
+ self.win.geometry(f"{int(win_w)}x{int(win_h)}+{int(x)}+{int(y)}")
727
+ self.keep_on_top()
728
+
729
+ def keep_on_top(self):
730
+ if self.win and self.win.winfo_exists():
731
+ apply_z_order_and_clickthrough(self.win, "topmost", click_through=False)
732
+
733
+ def _on_press(self, event):
734
+ self._press_x = event.x_root
735
+ self._press_win_x = self.win.winfo_x()
736
+ self._dragged = False
737
+
738
+ def _on_motion(self, event):
739
+ dx = event.x_root - self._press_x
740
+ if abs(dx) > 4:
741
+ self._dragged = True
742
+ if self._dragged:
743
+ geo = get_taskbar_geometry()
744
+ new_x = self._press_win_x + dx
745
+ if geo:
746
+ tb_l, _, tb_r, _, _ = geo
747
+ new_x = max(tb_l, min(new_x, tb_r - self._last_w))
748
+ self.win.geometry(f"+{int(new_x)}+{self.win.winfo_y()}")
749
+
750
+ def _on_release(self, event):
751
+ if self._dragged:
752
+ geo = get_taskbar_geometry()
753
+ tb_r = geo[2] if geo else self.root.winfo_screenwidth()
754
+ offset = tb_r - (self.win.winfo_x() + self._last_w)
755
+ self.app.config["taskbar_clock_offset"] = int(offset)
756
+ self.app._save_config()
757
+ self._dragged = False
758
+ return
759
+ self.app.on_taskbar_clicked()
760
+
761
+ def _on_right_click(self, event):
762
+ self.app.show_menu_at_pointer()
763
+
764
+ def destroy(self):
765
+ try:
766
+ if self.win and self.win.winfo_exists():
767
+ self.win.destroy()
768
+ except Exception:
769
+ pass
770
+ self.win = None
771
+
772
+
773
+ class TaskbarButtonProxy:
774
+ """دکمه/آیکون معمولی برنامه در تسک‌بار ویندوز"""
775
+
776
+ def __init__(self, app):
777
+ self.app = app
778
+ self.root = app.root
779
+ self._busy = False
780
+
781
+ self.win = tk.Toplevel(self.root)
782
+ self.win.title("ساعت و تقویم شمسی")
783
+ self.win.geometry("1x1+-32000+-32000")
784
+ self.win.attributes("-alpha", 0.0)
785
+ ico_path = get_resource_path(os.path.join("assets", "icons", "app_icon.ico"))
786
+ if os.path.exists(ico_path):
787
+ try:
788
+ self.win.iconbitmap(ico_path)
789
+ except Exception:
790
+ pass
791
+ self.win.protocol("WM_DELETE_WINDOW", self._on_close_request)
792
+ self.win.update_idletasks()
793
+ self.win.iconify()
794
+ self.win.bind("<Map>", self._on_map)
795
+
796
+ def _on_map(self, event=None):
797
+ if self._busy or event is None or event.widget is not self.win:
798
+ return
799
+ if self.win.state() != "normal":
800
+ return
801
+ self._busy = True
802
+
803
+ def _act():
804
+ try:
805
+ self.win.iconify()
806
+ except Exception:
807
+ pass
808
+ self.app.on_taskbar_clicked()
809
+ self.win.after(250, self._release)
810
+
811
+ self.win.after(10, _act)
812
+
813
+ def _release(self):
814
+ self._busy = False
815
+
816
+ def _on_close_request(self):
817
+ self.app.config["taskbar_button"] = False
818
+ self.app._save_config()
819
+ self.app.sync_taskbar_features()
820
+
821
+ def update_title(self, text: str):
822
+ try:
823
+ if self.win and self.win.winfo_exists():
824
+ self.win.title(text)
825
+ except Exception:
826
+ pass
827
+
828
+ def destroy(self):
829
+ try:
830
+ if self.win and self.win.winfo_exists():
831
+ self.win.destroy()
832
+ except Exception:
833
+ pass
834
+ self.win = None