persian-clock-widget 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README-LINUX.md +128 -0
- package/README.md +263 -2
- package/assets/fonts/BYekan+ Bold.ttf +0 -0
- package/assets/fonts/Iranian Sans.ttf +0 -0
- package/assets/fonts/Vazir-Medium.ttf +0 -0
- package/assets/icons/app_icon.ico +0 -0
- package/assets/icons/app_icon.png +0 -0
- package/assets/sounds/alarm.mp3 +0 -0
- package/assets/sounds/axel.mp3 +0 -0
- package/assets/sounds/heeeeyyyyy.mp3 +0 -0
- package/assets/sounds/liilililililililii.mp3 +0 -0
- package/assets/sounds/morning_glory.mp3 +0 -0
- package/assets/sounds/peter_axel_f.mp3 +0 -0
- package/bin/persian-clock.js +60 -0
- package/main.py +26 -0
- package/package.json +39 -4
- package/release/windows/PersianClockWidget.exe +0 -0
- package/requirements.txt +3 -0
- package/src/__init__.py +8 -0
- package/src/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/__pycache__/alarm_manager.cpython-314.pyc +0 -0
- package/src/__pycache__/calendar_data.cpython-314.pyc +0 -0
- package/src/__pycache__/taskbar_clock.cpython-314.pyc +0 -0
- package/src/__pycache__/widget.cpython-314.pyc +0 -0
- package/src/__pycache__/win_utils.cpython-314.pyc +0 -0
- package/src/alarm_manager.py +1314 -0
- package/src/calendar_data.py +519 -0
- package/src/icon_generator.py +106 -0
- package/src/taskbar_clock.py +834 -0
- package/src/widget.py +3351 -0
- package/src/win_utils.py +541 -0
package/src/win_utils.py
ADDED
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
ابزارهای سیستمی ویندوز:
|
|
4
|
+
- مدیریت مسیرهای پروژه ساختاریافته (assets/fonts, assets/icons, assets/sounds, data)
|
|
5
|
+
- پخش و توقف فایلهای صوتی MP3 آلارم با استفاده از WinMM (mciSendStringW) بدون نیاز به کتابخانه جانبی
|
|
6
|
+
- مخفی کردن کامل ویجت از نوار وظیفه (Taskbar) و نمایش فقط در Hidden Icons (System Tray)
|
|
7
|
+
- مدیریت Z-Index و عبوردهی کلیک (Click-Through) در حالت قفل
|
|
8
|
+
- گرد کردن گوشههای پنجره در ویندوز 10 و 11 و بارگذاری فونتهای فارسی باندلشده
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import ctypes
|
|
12
|
+
from ctypes import wintypes
|
|
13
|
+
import os
|
|
14
|
+
import shutil
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
import threading
|
|
18
|
+
|
|
19
|
+
IS_WIN = sys.platform == "win32"
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
import winreg
|
|
23
|
+
except ImportError:
|
|
24
|
+
winreg = None
|
|
25
|
+
|
|
26
|
+
if IS_WIN:
|
|
27
|
+
try:
|
|
28
|
+
import win32api
|
|
29
|
+
import win32con
|
|
30
|
+
import win32gui
|
|
31
|
+
HAS_WIN32 = True
|
|
32
|
+
except ImportError:
|
|
33
|
+
HAS_WIN32 = False
|
|
34
|
+
else:
|
|
35
|
+
HAS_WIN32 = False
|
|
36
|
+
|
|
37
|
+
_linux_audio_proc = None
|
|
38
|
+
|
|
39
|
+
# ثابتهای Win32 API
|
|
40
|
+
GWL_EXSTYLE = -20
|
|
41
|
+
WS_EX_TOOLWINDOW = 0x00000080
|
|
42
|
+
WS_EX_APPWINDOW = 0x00040000
|
|
43
|
+
WS_EX_NOACTIVATE = 0x08000000
|
|
44
|
+
WS_EX_LAYERED = 0x00080000
|
|
45
|
+
WS_EX_TRANSPARENT = 0x00000020
|
|
46
|
+
|
|
47
|
+
HWND_BOTTOM = 1
|
|
48
|
+
HWND_TOPMOST = -1
|
|
49
|
+
HWND_NOTOPMOST = -2
|
|
50
|
+
|
|
51
|
+
SWP_NOSIZE = 0x0001
|
|
52
|
+
SWP_NOMOVE = 0x0002
|
|
53
|
+
SWP_NOACTIVATE = 0x0010
|
|
54
|
+
SWP_SHOWWINDOW = 0x0040
|
|
55
|
+
SWP_FRAMECHANGED = 0x0020
|
|
56
|
+
|
|
57
|
+
DWMWA_WINDOW_CORNER_PREFERENCE = 33
|
|
58
|
+
DWMWCP_DEFAULT = 0
|
|
59
|
+
DWMWCP_DONOTROUND = 1
|
|
60
|
+
DWMWCP_ROUND = 2
|
|
61
|
+
|
|
62
|
+
APP_REG_NAME = "PersianClockCalendarWidget"
|
|
63
|
+
DEFAULT_ALARM_SOUND = "heeeeyyyyy.mp3"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_project_root() -> str:
|
|
67
|
+
"""
|
|
68
|
+
دریافت ریشه اصلی پروژه:
|
|
69
|
+
- در حالت فایل اجرایی (.exe): پوشه کنار فایل اجرایی (یا ریشه پروژه اگر داخل release/ باشد)
|
|
70
|
+
- در حالت سورس پایتون: پوشه مادرِ src/
|
|
71
|
+
"""
|
|
72
|
+
if getattr(sys, "frozen", False):
|
|
73
|
+
exe_dir = os.path.dirname(os.path.abspath(sys.executable))
|
|
74
|
+
parent_dir = os.path.dirname(exe_dir)
|
|
75
|
+
if os.path.basename(exe_dir).lower() == "release" and os.path.isdir(os.path.join(parent_dir, "data")):
|
|
76
|
+
return parent_dir
|
|
77
|
+
return exe_dir
|
|
78
|
+
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_data_dir() -> str:
|
|
82
|
+
"""دریافت مسیر پوشه data/ برای ذخیره تنظیمات، آلارمها و مناسبتهای شخصی"""
|
|
83
|
+
root = get_project_root()
|
|
84
|
+
data_dir = os.path.join(root, "data")
|
|
85
|
+
try:
|
|
86
|
+
os.makedirs(data_dir, exist_ok=True)
|
|
87
|
+
test_f = os.path.join(data_dir, ".write_test")
|
|
88
|
+
with open(test_f, "w", encoding="utf-8") as f:
|
|
89
|
+
f.write("ok")
|
|
90
|
+
os.remove(test_f)
|
|
91
|
+
return data_dir
|
|
92
|
+
except Exception:
|
|
93
|
+
fallback = os.path.join(os.path.expanduser("~"), ".local", "share", "PersianClockWidget", "data")
|
|
94
|
+
os.makedirs(fallback, exist_ok=True)
|
|
95
|
+
return fallback
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def get_resource_path(relative_path: str) -> str:
|
|
99
|
+
"""دریافت مسیر فایلهای باندل شده در PyInstaller (_MEIPASS) یا در پوشه پروژه"""
|
|
100
|
+
if hasattr(sys, "_MEIPASS"):
|
|
101
|
+
p = os.path.join(sys._MEIPASS, relative_path)
|
|
102
|
+
if os.path.exists(p):
|
|
103
|
+
return p
|
|
104
|
+
return os.path.join(get_project_root(), relative_path)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def get_available_alarm_sounds() -> list[str]:
|
|
108
|
+
"""
|
|
109
|
+
دریافت لیست فایلهای صوتی آلارم موجود در assets/sounds
|
|
110
|
+
با قرار دادن heeeeyyyyy.mp3 به عنوان گزینه اول و پیشفرض
|
|
111
|
+
"""
|
|
112
|
+
sounds_dir = get_resource_path(os.path.join("assets", "sounds"))
|
|
113
|
+
files = []
|
|
114
|
+
if os.path.isdir(sounds_dir):
|
|
115
|
+
for f in sorted(os.listdir(sounds_dir)):
|
|
116
|
+
if f.lower().endswith((".mp3", ".wav")):
|
|
117
|
+
files.append(f)
|
|
118
|
+
if DEFAULT_ALARM_SOUND in files:
|
|
119
|
+
files.remove(DEFAULT_ALARM_SOUND)
|
|
120
|
+
files.insert(0, DEFAULT_ALARM_SOUND)
|
|
121
|
+
elif not files:
|
|
122
|
+
files = [DEFAULT_ALARM_SOUND]
|
|
123
|
+
return files
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def play_mp3_sound(sound_filename: str, repeat: bool = True) -> bool:
|
|
127
|
+
"""پخش غیرمسدودکننده فایل MP3 (در ویندوز با WinMM و در لینوکس با mpv/ffplay/mpg123/cvlc)"""
|
|
128
|
+
global _linux_audio_proc
|
|
129
|
+
stop_mp3_sound()
|
|
130
|
+
sound_path = get_resource_path(os.path.join("assets", "sounds", sound_filename))
|
|
131
|
+
if not os.path.exists(sound_path):
|
|
132
|
+
# تلاش با صدای پیشفرض در صورت یافت نشدن فایل
|
|
133
|
+
sound_path = get_resource_path(os.path.join("assets", "sounds", DEFAULT_ALARM_SOUND))
|
|
134
|
+
if not os.path.exists(sound_path):
|
|
135
|
+
return False
|
|
136
|
+
|
|
137
|
+
if not IS_WIN:
|
|
138
|
+
candidates = [
|
|
139
|
+
["mpv", "--no-video", "--really-quiet"] + (["--loop-file=inf"] if repeat else []) + [sound_path],
|
|
140
|
+
["ffplay", "-nodisp", "-autoexit", "-loglevel", "quiet"] + (["-loop", "0"] if repeat else []) + [sound_path],
|
|
141
|
+
["mpg123", "-q"] + (["--loop", "-1"] if repeat else []) + [sound_path],
|
|
142
|
+
["cvlc", "--play-and-exit", "--quiet"] + (["--loop"] if repeat else []) + [sound_path],
|
|
143
|
+
]
|
|
144
|
+
for cmd in candidates:
|
|
145
|
+
if shutil.which(cmd[0]):
|
|
146
|
+
try:
|
|
147
|
+
_linux_audio_proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
148
|
+
return True
|
|
149
|
+
except Exception:
|
|
150
|
+
continue
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
mci = ctypes.windll.winmm.mciSendStringW
|
|
155
|
+
mci(f'open "{sound_path}" type mpegvideo alias widget_alarm_audio', None, 0, 0)
|
|
156
|
+
play_cmd = "play widget_alarm_audio repeat" if repeat else "play widget_alarm_audio"
|
|
157
|
+
res = mci(play_cmd, None, 0, 0)
|
|
158
|
+
return res == 0
|
|
159
|
+
except Exception:
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def stop_mp3_sound():
|
|
164
|
+
"""توقف کامل پخش صدای MP3 آلارم"""
|
|
165
|
+
global _linux_audio_proc
|
|
166
|
+
if not IS_WIN:
|
|
167
|
+
if _linux_audio_proc is not None:
|
|
168
|
+
try:
|
|
169
|
+
_linux_audio_proc.terminate()
|
|
170
|
+
except Exception:
|
|
171
|
+
pass
|
|
172
|
+
_linux_audio_proc = None
|
|
173
|
+
return
|
|
174
|
+
try:
|
|
175
|
+
mci = ctypes.windll.winmm.mciSendStringW
|
|
176
|
+
mci("stop widget_alarm_audio", None, 0, 0)
|
|
177
|
+
mci("close widget_alarm_audio", None, 0, 0)
|
|
178
|
+
except Exception:
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def enable_dpi_awareness():
|
|
183
|
+
"""فعالسازی پشتیبانی از نمایشگرهای High-DPI برای وضوح کامل فونتها"""
|
|
184
|
+
if not IS_WIN:
|
|
185
|
+
return
|
|
186
|
+
try:
|
|
187
|
+
ctypes.windll.shcore.SetProcessDpiAwareness(2)
|
|
188
|
+
except Exception:
|
|
189
|
+
try:
|
|
190
|
+
ctypes.windll.user32.SetProcessDPIAware()
|
|
191
|
+
except Exception:
|
|
192
|
+
pass
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def register_bundled_fonts() -> list[str]:
|
|
196
|
+
"""
|
|
197
|
+
بارگذاری فونتهای فارسی باندلشده در assets/fonts:
|
|
198
|
+
- در ویندوز: با AddFontResourceExW در حافظه GDI
|
|
199
|
+
- در لینوکس: کپی خودکار در ~/.local/share/fonts و بهروزرسانی fc-cache
|
|
200
|
+
"""
|
|
201
|
+
loaded = []
|
|
202
|
+
fonts_dir = get_resource_path(os.path.join("assets", "fonts"))
|
|
203
|
+
if not os.path.isdir(fonts_dir):
|
|
204
|
+
return loaded
|
|
205
|
+
|
|
206
|
+
if not IS_WIN:
|
|
207
|
+
user_fonts = os.path.expanduser("~/.local/share/fonts/PersianClockWidget")
|
|
208
|
+
os.makedirs(user_fonts, exist_ok=True)
|
|
209
|
+
copied_new = False
|
|
210
|
+
for fname in os.listdir(fonts_dir):
|
|
211
|
+
if fname.lower().endswith((".ttf", ".otf")):
|
|
212
|
+
src_p = os.path.join(fonts_dir, fname)
|
|
213
|
+
dst_p = os.path.join(user_fonts, fname)
|
|
214
|
+
try:
|
|
215
|
+
if not os.path.exists(dst_p):
|
|
216
|
+
shutil.copy2(src_p, dst_p)
|
|
217
|
+
copied_new = True
|
|
218
|
+
loaded.append(dst_p)
|
|
219
|
+
except Exception:
|
|
220
|
+
pass
|
|
221
|
+
if copied_new and shutil.which("fc-cache"):
|
|
222
|
+
try:
|
|
223
|
+
subprocess.Popen(["fc-cache", "-f", user_fonts], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
224
|
+
except Exception:
|
|
225
|
+
pass
|
|
226
|
+
return loaded
|
|
227
|
+
|
|
228
|
+
FR_PRIVATE = 0x10
|
|
229
|
+
for fname in os.listdir(fonts_dir):
|
|
230
|
+
if fname.lower().endswith((".ttf", ".otf")):
|
|
231
|
+
fpath = os.path.join(fonts_dir, fname)
|
|
232
|
+
try:
|
|
233
|
+
res = ctypes.windll.gdi32.AddFontResourceExW(
|
|
234
|
+
ctypes.c_wchar_p(fpath),
|
|
235
|
+
FR_PRIVATE,
|
|
236
|
+
0,
|
|
237
|
+
)
|
|
238
|
+
if res > 0:
|
|
239
|
+
loaded.append(fpath)
|
|
240
|
+
except Exception:
|
|
241
|
+
pass
|
|
242
|
+
return loaded
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def get_hwnd(tk_win) -> int:
|
|
246
|
+
"""دریافت هندل واقعی پنجره ویندوز (HWND) از پنجره Tkinter"""
|
|
247
|
+
tk_win.update_idletasks()
|
|
248
|
+
if not IS_WIN:
|
|
249
|
+
return tk_win.winfo_id()
|
|
250
|
+
hwnd = ctypes.windll.user32.GetParent(tk_win.winfo_id())
|
|
251
|
+
if not hwnd:
|
|
252
|
+
hwnd = tk_win.winfo_id()
|
|
253
|
+
return hwnd
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def hide_from_taskbar(tk_win):
|
|
257
|
+
"""مخفی کردن کامل پنجره از Taskbar و Alt+Tab"""
|
|
258
|
+
if not IS_WIN:
|
|
259
|
+
try:
|
|
260
|
+
tk_win.attributes("-type", "utility")
|
|
261
|
+
except Exception:
|
|
262
|
+
pass
|
|
263
|
+
return
|
|
264
|
+
try:
|
|
265
|
+
hwnd = get_hwnd(tk_win)
|
|
266
|
+
user32 = ctypes.windll.user32
|
|
267
|
+
ex_style = user32.GetWindowLongW(hwnd, GWL_EXSTYLE)
|
|
268
|
+
new_style = (ex_style | WS_EX_TOOLWINDOW) & ~WS_EX_APPWINDOW
|
|
269
|
+
if new_style != ex_style:
|
|
270
|
+
user32.SetWindowLongW(hwnd, GWL_EXSTYLE, new_style)
|
|
271
|
+
user32.SetWindowPos(
|
|
272
|
+
hwnd, 0, 0, 0, 0, 0,
|
|
273
|
+
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_FRAMECHANGED
|
|
274
|
+
)
|
|
275
|
+
except Exception:
|
|
276
|
+
pass
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def apply_z_order_and_clickthrough(tk_win, z_mode: str, click_through: bool = False):
|
|
280
|
+
"""اعمال حالت Z-Index و قفل عبوردهی کلیک (Click-Through)"""
|
|
281
|
+
if not IS_WIN:
|
|
282
|
+
try:
|
|
283
|
+
is_topmost = (z_mode != "desktop")
|
|
284
|
+
tk_win.attributes("-topmost", is_topmost)
|
|
285
|
+
if not is_topmost:
|
|
286
|
+
tk_win.lower()
|
|
287
|
+
except Exception:
|
|
288
|
+
pass
|
|
289
|
+
return
|
|
290
|
+
try:
|
|
291
|
+
hwnd = get_hwnd(tk_win)
|
|
292
|
+
user32 = ctypes.windll.user32
|
|
293
|
+
|
|
294
|
+
is_topmost = (z_mode != "desktop")
|
|
295
|
+
|
|
296
|
+
ex_style = user32.GetWindowLongW(hwnd, GWL_EXSTYLE)
|
|
297
|
+
target_style = (ex_style | WS_EX_TOOLWINDOW | WS_EX_LAYERED) & ~WS_EX_APPWINDOW
|
|
298
|
+
if click_through:
|
|
299
|
+
target_style |= WS_EX_TRANSPARENT
|
|
300
|
+
else:
|
|
301
|
+
target_style &= ~WS_EX_TRANSPARENT
|
|
302
|
+
|
|
303
|
+
if target_style != ex_style:
|
|
304
|
+
user32.SetWindowLongW(hwnd, GWL_EXSTYLE, target_style)
|
|
305
|
+
user32.SetWindowPos(
|
|
306
|
+
hwnd, 0, 0, 0, 0, 0,
|
|
307
|
+
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_FRAMECHANGED
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
if is_topmost:
|
|
311
|
+
tk_win.attributes("-topmost", True)
|
|
312
|
+
user32.SetWindowPos(
|
|
313
|
+
hwnd,
|
|
314
|
+
HWND_TOPMOST,
|
|
315
|
+
0, 0, 0, 0,
|
|
316
|
+
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW,
|
|
317
|
+
)
|
|
318
|
+
else:
|
|
319
|
+
tk_win.attributes("-topmost", False)
|
|
320
|
+
user32.SetWindowPos(
|
|
321
|
+
hwnd,
|
|
322
|
+
HWND_BOTTOM,
|
|
323
|
+
0, 0, 0, 0,
|
|
324
|
+
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE,
|
|
325
|
+
)
|
|
326
|
+
except Exception:
|
|
327
|
+
pass
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def apply_rounded_corners(tk_win, width: int, height: int, radius: int = 16, enabled: bool = True):
|
|
331
|
+
"""اعمال گوشههای گرد به پنجره ویجت در ویندوز 11 و 10"""
|
|
332
|
+
if not IS_WIN:
|
|
333
|
+
return
|
|
334
|
+
try:
|
|
335
|
+
hwnd = get_hwnd(tk_win)
|
|
336
|
+
pref = ctypes.c_int(DWMWCP_ROUND if enabled else DWMWCP_DONOTROUND)
|
|
337
|
+
res = ctypes.windll.dwmapi.DwmSetWindowAttribute(
|
|
338
|
+
wintypes.HWND(hwnd),
|
|
339
|
+
ctypes.c_uint(DWMWA_WINDOW_CORNER_PREFERENCE),
|
|
340
|
+
ctypes.byref(pref),
|
|
341
|
+
ctypes.sizeof(pref),
|
|
342
|
+
)
|
|
343
|
+
if res != 0:
|
|
344
|
+
if enabled:
|
|
345
|
+
rgn = ctypes.windll.gdi32.CreateRoundRectRgn(0, 0, width + 1, height + 1, radius, radius)
|
|
346
|
+
ctypes.windll.user32.SetWindowRgn(hwnd, rgn, True)
|
|
347
|
+
else:
|
|
348
|
+
ctypes.windll.user32.SetWindowRgn(hwnd, 0, True)
|
|
349
|
+
except Exception:
|
|
350
|
+
pass
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
# ---------------------------------------------------------------------------
|
|
354
|
+
# مدیریت اجرای خودکار با شروع ویندوز (Startup)
|
|
355
|
+
# ---------------------------------------------------------------------------
|
|
356
|
+
def get_startup_command(script_path: str) -> str:
|
|
357
|
+
if getattr(sys, "frozen", False):
|
|
358
|
+
return f'"{os.path.abspath(sys.executable)}"'
|
|
359
|
+
python_exe = sys.executable
|
|
360
|
+
pythonw_exe = os.path.join(os.path.dirname(python_exe), "pythonw.exe")
|
|
361
|
+
if os.path.exists(pythonw_exe):
|
|
362
|
+
python_exe = pythonw_exe
|
|
363
|
+
return f'"{python_exe}" "{os.path.abspath(script_path)}"'
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _linux_autostart_path() -> str:
|
|
367
|
+
return os.path.expanduser("~/.config/autostart/persian-clock-widget.desktop")
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def is_startup_enabled() -> bool:
|
|
371
|
+
if not IS_WIN:
|
|
372
|
+
return os.path.exists(_linux_autostart_path())
|
|
373
|
+
try:
|
|
374
|
+
key = winreg.OpenKey(
|
|
375
|
+
winreg.HKEY_CURRENT_USER,
|
|
376
|
+
r"Software\Microsoft\Windows\CurrentVersion\Run",
|
|
377
|
+
0,
|
|
378
|
+
winreg.KEY_READ,
|
|
379
|
+
)
|
|
380
|
+
val, _ = winreg.QueryValueEx(key, APP_REG_NAME)
|
|
381
|
+
winreg.CloseKey(key)
|
|
382
|
+
return bool(val)
|
|
383
|
+
except Exception:
|
|
384
|
+
return False
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def set_startup_enabled(enable: bool, script_path: str) -> bool:
|
|
388
|
+
if not IS_WIN:
|
|
389
|
+
desktop_path = _linux_autostart_path()
|
|
390
|
+
try:
|
|
391
|
+
if enable:
|
|
392
|
+
os.makedirs(os.path.dirname(desktop_path), exist_ok=True)
|
|
393
|
+
exec_cmd = get_startup_command(script_path)
|
|
394
|
+
icon_path = get_resource_path(os.path.join("assets", "icons", "app_icon.png"))
|
|
395
|
+
with open(desktop_path, "w", encoding="utf-8") as f:
|
|
396
|
+
f.write(
|
|
397
|
+
"[Desktop Entry]\n"
|
|
398
|
+
"Type=Application\n"
|
|
399
|
+
"Name=Persian Clock & Calendar\n"
|
|
400
|
+
f"Exec={exec_cmd}\n"
|
|
401
|
+
f"Icon={icon_path}\n"
|
|
402
|
+
"Terminal=false\n"
|
|
403
|
+
"X-GNOME-Autostart-enabled=true\n"
|
|
404
|
+
)
|
|
405
|
+
else:
|
|
406
|
+
if os.path.exists(desktop_path):
|
|
407
|
+
os.remove(desktop_path)
|
|
408
|
+
return True
|
|
409
|
+
except Exception:
|
|
410
|
+
return False
|
|
411
|
+
try:
|
|
412
|
+
key = winreg.OpenKey(
|
|
413
|
+
winreg.HKEY_CURRENT_USER,
|
|
414
|
+
r"Software\Microsoft\Windows\CurrentVersion\Run",
|
|
415
|
+
0,
|
|
416
|
+
winreg.KEY_SET_VALUE,
|
|
417
|
+
)
|
|
418
|
+
if enable:
|
|
419
|
+
cmd = get_startup_command(script_path)
|
|
420
|
+
winreg.SetValueEx(key, APP_REG_NAME, 0, winreg.REG_SZ, cmd)
|
|
421
|
+
else:
|
|
422
|
+
try:
|
|
423
|
+
winreg.DeleteValue(key, APP_REG_NAME)
|
|
424
|
+
except FileNotFoundError:
|
|
425
|
+
pass
|
|
426
|
+
winreg.CloseKey(key)
|
|
427
|
+
return True
|
|
428
|
+
except Exception:
|
|
429
|
+
return False
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
# ---------------------------------------------------------------------------
|
|
433
|
+
# مدیریت آیکون کنار ساعت ویندوز (Hidden Icons / System Tray)
|
|
434
|
+
# ---------------------------------------------------------------------------
|
|
435
|
+
class SystemTrayIcon:
|
|
436
|
+
WM_TRAYICON = win32con.WM_USER + 20 if HAS_WIN32 else 0x0414
|
|
437
|
+
|
|
438
|
+
def __init__(self, tooltip: str, on_left_click, on_right_click):
|
|
439
|
+
self.tooltip = tooltip
|
|
440
|
+
self.on_left_click = on_left_click
|
|
441
|
+
self.on_right_click = on_right_click
|
|
442
|
+
self.hwnd = None
|
|
443
|
+
self._thread = None
|
|
444
|
+
self._running = False
|
|
445
|
+
|
|
446
|
+
def start(self):
|
|
447
|
+
if not HAS_WIN32 or self._running:
|
|
448
|
+
return
|
|
449
|
+
self._running = True
|
|
450
|
+
self._thread = threading.Thread(target=self._run, daemon=True)
|
|
451
|
+
self._thread.start()
|
|
452
|
+
|
|
453
|
+
def update_tooltip(self, new_tooltip: str):
|
|
454
|
+
self.tooltip = new_tooltip[:120]
|
|
455
|
+
if HAS_WIN32 and self.hwnd:
|
|
456
|
+
try:
|
|
457
|
+
nid = (
|
|
458
|
+
self.hwnd,
|
|
459
|
+
0,
|
|
460
|
+
win32gui.NIF_TIP,
|
|
461
|
+
self.WM_TRAYICON,
|
|
462
|
+
0,
|
|
463
|
+
self.tooltip,
|
|
464
|
+
)
|
|
465
|
+
win32gui.Shell_NotifyIcon(win32gui.NIM_MODIFY, nid)
|
|
466
|
+
except Exception:
|
|
467
|
+
pass
|
|
468
|
+
|
|
469
|
+
def stop(self):
|
|
470
|
+
self._running = False
|
|
471
|
+
if HAS_WIN32 and self.hwnd:
|
|
472
|
+
try:
|
|
473
|
+
win32gui.Shell_NotifyIcon(win32gui.NIM_DELETE, (self.hwnd, 0))
|
|
474
|
+
win32gui.PostMessage(self.hwnd, win32con.WM_CLOSE, 0, 0)
|
|
475
|
+
except Exception:
|
|
476
|
+
pass
|
|
477
|
+
self.hwnd = None
|
|
478
|
+
|
|
479
|
+
def _run(self):
|
|
480
|
+
try:
|
|
481
|
+
message_map = {
|
|
482
|
+
win32con.WM_DESTROY: self._on_destroy,
|
|
483
|
+
self.WM_TRAYICON: self._on_notify,
|
|
484
|
+
}
|
|
485
|
+
wc = win32gui.WNDCLASS()
|
|
486
|
+
hinst = wc.hInstance = win32api.GetModuleHandle(None)
|
|
487
|
+
wc.lpszClassName = "PersianClockWidgetTrayClass"
|
|
488
|
+
wc.lpfnWndProc = message_map
|
|
489
|
+
try:
|
|
490
|
+
class_atom = win32gui.RegisterClass(wc)
|
|
491
|
+
except Exception:
|
|
492
|
+
class_atom = wc.lpszClassName
|
|
493
|
+
|
|
494
|
+
self.hwnd = win32gui.CreateWindow(
|
|
495
|
+
class_atom,
|
|
496
|
+
"PersianClockWidgetTray",
|
|
497
|
+
0,
|
|
498
|
+
0, 0, 0, 0,
|
|
499
|
+
0, 0, hinst, None,
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
hicon = None
|
|
503
|
+
ico_path = get_resource_path(os.path.join("assets", "icons", "app_icon.ico"))
|
|
504
|
+
if os.path.exists(ico_path):
|
|
505
|
+
try:
|
|
506
|
+
hicon = win32gui.LoadImage(
|
|
507
|
+
hinst,
|
|
508
|
+
ico_path,
|
|
509
|
+
win32con.IMAGE_ICON,
|
|
510
|
+
0, 0,
|
|
511
|
+
win32con.LR_LOADFROMFILE | win32con.LR_DEFAULTSIZE,
|
|
512
|
+
)
|
|
513
|
+
except Exception:
|
|
514
|
+
hicon = None
|
|
515
|
+
if not hicon:
|
|
516
|
+
hicon = win32gui.LoadIcon(0, win32con.IDI_APPLICATION)
|
|
517
|
+
|
|
518
|
+
flags = win32gui.NIF_ICON | win32gui.NIF_MESSAGE | win32gui.NIF_TIP
|
|
519
|
+
nid = (self.hwnd, 0, flags, self.WM_TRAYICON, hicon, self.tooltip[:120])
|
|
520
|
+
win32gui.Shell_NotifyIcon(win32gui.NIM_ADD, nid)
|
|
521
|
+
|
|
522
|
+
win32gui.PumpMessages()
|
|
523
|
+
except Exception:
|
|
524
|
+
pass
|
|
525
|
+
|
|
526
|
+
def _on_notify(self, hwnd, msg, wparam, lparam):
|
|
527
|
+
if lparam == win32con.WM_LBUTTONUP or lparam == win32con.WM_LBUTTONDBLCLK:
|
|
528
|
+
if self.on_left_click:
|
|
529
|
+
self.on_left_click()
|
|
530
|
+
elif lparam == win32con.WM_RBUTTONUP:
|
|
531
|
+
if self.on_right_click:
|
|
532
|
+
self.on_right_click()
|
|
533
|
+
return True
|
|
534
|
+
|
|
535
|
+
def _on_destroy(self, hwnd, msg, wparam, lparam):
|
|
536
|
+
try:
|
|
537
|
+
win32gui.Shell_NotifyIcon(win32gui.NIM_DELETE, (self.hwnd, 0))
|
|
538
|
+
except Exception:
|
|
539
|
+
pass
|
|
540
|
+
win32gui.PostQuitMessage(0)
|
|
541
|
+
return True
|