persian-clock-widget 0.0.0-stage → 1.0.1

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