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.
- package/LICENSE +21 -0
- package/README-LINUX.md +128 -0
- package/README.md +263 -2
- package/assets/fonts/BYekan+ Bold.ttf +0 -0
- package/assets/fonts/Iranian Sans.ttf +0 -0
- package/assets/fonts/Vazir-Medium.ttf +0 -0
- package/assets/icons/app_icon.ico +0 -0
- package/assets/icons/app_icon.png +0 -0
- package/assets/sounds/alarm.mp3 +0 -0
- package/assets/sounds/axel.mp3 +0 -0
- package/assets/sounds/heeeeyyyyy.mp3 +0 -0
- package/assets/sounds/liilililililililii.mp3 +0 -0
- package/assets/sounds/morning_glory.mp3 +0 -0
- package/assets/sounds/peter_axel_f.mp3 +0 -0
- package/bin/persian-clock.js +60 -0
- package/main.py +26 -0
- package/package.json +39 -4
- package/release/windows/PersianClockWidget.exe +0 -0
- package/requirements.txt +3 -0
- package/src/__init__.py +8 -0
- package/src/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/__pycache__/alarm_manager.cpython-314.pyc +0 -0
- package/src/__pycache__/calendar_data.cpython-314.pyc +0 -0
- package/src/__pycache__/taskbar_clock.cpython-314.pyc +0 -0
- package/src/__pycache__/widget.cpython-314.pyc +0 -0
- package/src/__pycache__/win_utils.cpython-314.pyc +0 -0
- package/src/alarm_manager.py +1314 -0
- package/src/calendar_data.py +519 -0
- package/src/icon_generator.py +106 -0
- package/src/taskbar_clock.py +913 -0
- package/src/widget.py +3570 -0
- package/src/win_utils.py +618 -0
|
@@ -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
|