py-neuromodulation 0.0.7__py3-none-any.whl → 0.1.1__py3-none-any.whl
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.
- py_neuromodulation/ConnectivityDecoding/_get_grid_whole_brain.py +0 -1
- py_neuromodulation/ConnectivityDecoding/_helper_write_connectome.py +0 -2
- py_neuromodulation/__init__.py +12 -4
- py_neuromodulation/analysis/RMAP.py +3 -3
- py_neuromodulation/analysis/decode.py +55 -2
- py_neuromodulation/analysis/feature_reader.py +1 -0
- py_neuromodulation/analysis/stats.py +3 -3
- py_neuromodulation/default_settings.yaml +25 -20
- py_neuromodulation/features/bandpower.py +65 -23
- py_neuromodulation/features/bursts.py +9 -8
- py_neuromodulation/features/coherence.py +7 -4
- py_neuromodulation/features/feature_processor.py +4 -4
- py_neuromodulation/features/fooof.py +7 -6
- py_neuromodulation/features/mne_connectivity.py +60 -87
- py_neuromodulation/features/oscillatory.py +5 -4
- py_neuromodulation/features/sharpwaves.py +21 -0
- py_neuromodulation/filter/kalman_filter.py +17 -6
- py_neuromodulation/gui/__init__.py +3 -0
- py_neuromodulation/gui/backend/app_backend.py +419 -0
- py_neuromodulation/gui/backend/app_manager.py +345 -0
- py_neuromodulation/gui/backend/app_pynm.py +253 -0
- py_neuromodulation/gui/backend/app_socket.py +97 -0
- py_neuromodulation/gui/backend/app_utils.py +306 -0
- py_neuromodulation/gui/backend/app_window.py +202 -0
- py_neuromodulation/gui/frontend/assets/Figtree-VariableFont_wght-CkXbWBDP.ttf +0 -0
- py_neuromodulation/gui/frontend/assets/index-_6V8ZfAS.js +300137 -0
- py_neuromodulation/gui/frontend/assets/plotly-DTCwMlpS.js +23594 -0
- py_neuromodulation/gui/frontend/charite.svg +16 -0
- py_neuromodulation/gui/frontend/index.html +14 -0
- py_neuromodulation/gui/window_api.py +115 -0
- py_neuromodulation/lsl_api.cfg +3 -0
- py_neuromodulation/processing/data_preprocessor.py +9 -2
- py_neuromodulation/processing/filter_preprocessing.py +43 -27
- py_neuromodulation/processing/normalization.py +32 -17
- py_neuromodulation/processing/projection.py +2 -2
- py_neuromodulation/processing/resample.py +6 -2
- py_neuromodulation/run_gui.py +36 -0
- py_neuromodulation/stream/__init__.py +7 -1
- py_neuromodulation/stream/backend_interface.py +47 -0
- py_neuromodulation/stream/data_processor.py +24 -3
- py_neuromodulation/stream/mnelsl_player.py +121 -21
- py_neuromodulation/stream/mnelsl_stream.py +9 -17
- py_neuromodulation/stream/settings.py +80 -34
- py_neuromodulation/stream/stream.py +83 -62
- py_neuromodulation/utils/channels.py +1 -1
- py_neuromodulation/utils/file_writer.py +110 -0
- py_neuromodulation/utils/io.py +46 -5
- py_neuromodulation/utils/perf.py +156 -0
- py_neuromodulation/utils/pydantic_extensions.py +322 -0
- py_neuromodulation/utils/types.py +33 -107
- {py_neuromodulation-0.0.7.dist-info → py_neuromodulation-0.1.1.dist-info}/METADATA +23 -4
- {py_neuromodulation-0.0.7.dist-info → py_neuromodulation-0.1.1.dist-info}/RECORD +55 -35
- {py_neuromodulation-0.0.7.dist-info → py_neuromodulation-0.1.1.dist-info}/WHEEL +1 -1
- py_neuromodulation-0.1.1.dist-info/entry_points.txt +2 -0
- {py_neuromodulation-0.0.7.dist-info → py_neuromodulation-0.1.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import multiprocessing as mp
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Sequence
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from py_neuromodulation.utils.types import _PathLike
|
|
7
|
+
from functools import lru_cache
|
|
8
|
+
import platform
|
|
9
|
+
from py_neuromodulation import logger
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def force_terminate_process(
|
|
13
|
+
process: mp.Process, name: str, logger: logging.Logger | None = None
|
|
14
|
+
) -> None:
|
|
15
|
+
log = logger.debug if logger else print
|
|
16
|
+
|
|
17
|
+
import psutil
|
|
18
|
+
|
|
19
|
+
p = psutil.Process(process.pid)
|
|
20
|
+
try:
|
|
21
|
+
log(f"Terminating process {name}")
|
|
22
|
+
for child in p.children(recursive=True):
|
|
23
|
+
log(f"Terminating child process {child.pid}")
|
|
24
|
+
child.terminate()
|
|
25
|
+
p.terminate()
|
|
26
|
+
p.wait(timeout=3)
|
|
27
|
+
except psutil.NoSuchProcess:
|
|
28
|
+
log(f"Process {name} has already exited.")
|
|
29
|
+
except psutil.TimeoutExpired:
|
|
30
|
+
log(f"Forcefully killing {name}...")
|
|
31
|
+
p.kill()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def create_logger(name, color: str, level=logging.INFO):
|
|
35
|
+
"""Function to set up a logger with color coded output"""
|
|
36
|
+
color = ansi_color(color=color, bright=True, styles=["BOLD"])
|
|
37
|
+
logger = logging.getLogger(name)
|
|
38
|
+
log_format = f"{color}[%(name)s %(levelname)s (%(asctime)s)]:{ansi_color(styles=['RESET'])} %(message)s"
|
|
39
|
+
stream_handler = logging.StreamHandler()
|
|
40
|
+
stream_handler.setFormatter(logging.Formatter(log_format, "%H:%M:%S"))
|
|
41
|
+
stream_handler.setStream(sys.stderr)
|
|
42
|
+
logger.setLevel(level)
|
|
43
|
+
logger.addHandler(stream_handler)
|
|
44
|
+
|
|
45
|
+
return logger
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def ansi_color(
|
|
49
|
+
color: str = "DEFAULT",
|
|
50
|
+
bright: bool = True,
|
|
51
|
+
styles: Sequence[str] = [],
|
|
52
|
+
bg_color: str = "DEFAULT",
|
|
53
|
+
bg_bright: bool = True,
|
|
54
|
+
) -> str:
|
|
55
|
+
"""
|
|
56
|
+
Function to generate ANSI color codes for colored text in the terminal.
|
|
57
|
+
See https://en.wikipedia.org/wiki/ANSI_escape_code
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
str: ANSI color code
|
|
61
|
+
"""
|
|
62
|
+
ANSI_COLORS = {
|
|
63
|
+
# https://en.wikipedia.org/wiki/ANSI_escape_code
|
|
64
|
+
"BLACK": 30,
|
|
65
|
+
"RED": 31,
|
|
66
|
+
"GREEN": 32,
|
|
67
|
+
"YELLOW": 33,
|
|
68
|
+
"BLUE": 34,
|
|
69
|
+
"MAGENTA": 35,
|
|
70
|
+
"CYAN": 36,
|
|
71
|
+
"WHITE": 37,
|
|
72
|
+
"DEFAULT": 39,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
ANSI_STYLES = {
|
|
76
|
+
"RESET": 0,
|
|
77
|
+
"BOLD": 1,
|
|
78
|
+
"FAINT": 2,
|
|
79
|
+
"ITALIC": 3,
|
|
80
|
+
"UNDERLINE": 4,
|
|
81
|
+
"BLINK": 5,
|
|
82
|
+
"NEGATIVE": 7,
|
|
83
|
+
"CROSSED": 9,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
color = color.upper()
|
|
87
|
+
bg_color = bg_color.upper()
|
|
88
|
+
styles = [style.upper() for style in styles]
|
|
89
|
+
|
|
90
|
+
if color not in ANSI_COLORS.keys() or bg_color not in ANSI_COLORS.keys():
|
|
91
|
+
raise ValueError(f"Invalid color: {color}")
|
|
92
|
+
|
|
93
|
+
for style in styles:
|
|
94
|
+
if style not in ANSI_STYLES.keys():
|
|
95
|
+
raise ValueError(f"Invalid style: {style}")
|
|
96
|
+
|
|
97
|
+
color_code = str(ANSI_COLORS[color] + (60 if bright else 0))
|
|
98
|
+
bg_color_code = str(ANSI_COLORS[bg_color] + 10 + (60 if bg_bright else 0))
|
|
99
|
+
style_codes = ";".join((str(ANSI_STYLES[style]) for style in styles))
|
|
100
|
+
|
|
101
|
+
return f"\033[{style_codes};{color_code};{bg_color_code}m"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
ansi_reset = ansi_color(styles=["RESET"])
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def is_hidden(filepath: _PathLike) -> bool:
|
|
108
|
+
"""Check if a file or directory is hidden.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
filepath (str): Path to the file or directory.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
bool: True if the file or directory is hidden, False otherwise.
|
|
115
|
+
"""
|
|
116
|
+
from pathlib import Path
|
|
117
|
+
|
|
118
|
+
filepath = Path(filepath)
|
|
119
|
+
|
|
120
|
+
if sys.platform.startswith("win"):
|
|
121
|
+
import ctypes
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
attrs = ctypes.windll.kernel32.GetFileAttributesW(str(filepath))
|
|
125
|
+
assert attrs != -1
|
|
126
|
+
result = bool(attrs & 2) or filepath.name.startswith(".")
|
|
127
|
+
except (AttributeError, AssertionError):
|
|
128
|
+
result = filepath.name.startswith(".")
|
|
129
|
+
else:
|
|
130
|
+
result = filepath.name.startswith(".")
|
|
131
|
+
|
|
132
|
+
return result
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@lru_cache(maxsize=1)
|
|
136
|
+
def get_quick_access():
|
|
137
|
+
system = platform.system()
|
|
138
|
+
if system == "Windows":
|
|
139
|
+
return get_windows_quick_access()
|
|
140
|
+
elif system == "Darwin": # macOS
|
|
141
|
+
return get_macos_quick_access()
|
|
142
|
+
else: # Linux, Unix, etc.
|
|
143
|
+
return {"items": []}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def get_windows_quick_access():
|
|
147
|
+
quick_access_items = []
|
|
148
|
+
|
|
149
|
+
# Add available drives
|
|
150
|
+
available_drives = [
|
|
151
|
+
f"{d}:\\" for d in "ABCDEFGHIJKLMNOPQRSTUVWXYZ" if Path(f"{d}:").exists()
|
|
152
|
+
]
|
|
153
|
+
for drive in available_drives:
|
|
154
|
+
quick_access_items.append(
|
|
155
|
+
{"name": f"Drive ({drive})", "type": "drive", "path": drive}
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Get user's pinned folders
|
|
159
|
+
pinned_folders = get_pinned_folders_windows()
|
|
160
|
+
for folder in pinned_folders:
|
|
161
|
+
path = Path(folder["Path"])
|
|
162
|
+
if path.exists():
|
|
163
|
+
quick_access_items.append(
|
|
164
|
+
{"name": folder["Name"], "type": "folder", "path": str(path)}
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Get user's home directory
|
|
168
|
+
home_path = Path.home()
|
|
169
|
+
|
|
170
|
+
# Add common folders if they're not already in pinned folders
|
|
171
|
+
common_folders = [
|
|
172
|
+
("Desktop", "Desktop"),
|
|
173
|
+
("Documents", "Documents"),
|
|
174
|
+
("Downloads", "Downloads"),
|
|
175
|
+
("Pictures", "Pictures"),
|
|
176
|
+
("Music", "Music"),
|
|
177
|
+
("Videos", "Videos"),
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
for folder_name, folder_path in common_folders:
|
|
181
|
+
full_path = home_path / folder_path
|
|
182
|
+
if full_path.exists() and str(full_path) not in [
|
|
183
|
+
item["path"] for item in quick_access_items
|
|
184
|
+
]:
|
|
185
|
+
quick_access_items.append(
|
|
186
|
+
{"name": folder_name, "type": "folder", "path": str(full_path)}
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Add user's home directory if not already included
|
|
190
|
+
if str(home_path) not in [item["path"] for item in quick_access_items]:
|
|
191
|
+
quick_access_items.append(
|
|
192
|
+
{"name": "Home", "type": "folder", "path": str(home_path)}
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
return {"items": quick_access_items}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def get_pinned_folders_windows():
|
|
199
|
+
import subprocess
|
|
200
|
+
import json
|
|
201
|
+
|
|
202
|
+
powershell_command = """
|
|
203
|
+
$shell = New-Object -ComObject Shell.Application
|
|
204
|
+
$quickaccess = $shell.Namespace("shell:::{679f85cb-0220-4080-b29b-5540cc05aab6}").Items()
|
|
205
|
+
$pinned = $quickaccess | Where-Object { $_.IsFolder } | ForEach-Object {
|
|
206
|
+
[PSCustomObject]@{
|
|
207
|
+
Name = $_.Name
|
|
208
|
+
Path = $_.Path
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
$pinned | ConvertTo-Json
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
result = subprocess.run(
|
|
216
|
+
["powershell", "-Command", powershell_command],
|
|
217
|
+
capture_output=True,
|
|
218
|
+
text=True,
|
|
219
|
+
check=True,
|
|
220
|
+
)
|
|
221
|
+
return json.loads(result.stdout)
|
|
222
|
+
except subprocess.CalledProcessError as e:
|
|
223
|
+
logger.error(f"Error running PowerShell command: {e}")
|
|
224
|
+
return []
|
|
225
|
+
except json.JSONDecodeError as e:
|
|
226
|
+
logger.error(f"Error decoding JSON: {e}")
|
|
227
|
+
return []
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def get_macos_quick_access():
|
|
231
|
+
quick_access_folders = get_macos_favorites()
|
|
232
|
+
quick_access_items = []
|
|
233
|
+
|
|
234
|
+
quick_access_items.append({"name": "Computer", "type": "drive", "path": "/"})
|
|
235
|
+
|
|
236
|
+
# Add Volumes for macOS
|
|
237
|
+
volumes_path = Path("/Volumes")
|
|
238
|
+
if volumes_path.exists():
|
|
239
|
+
for volume in volumes_path.iterdir():
|
|
240
|
+
if volume.is_mount():
|
|
241
|
+
quick_access_items.append(
|
|
242
|
+
{"name": volume.name, "type": "drive", "path": str(volume)}
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# Add quick access folders
|
|
246
|
+
for folder in quick_access_folders:
|
|
247
|
+
path = Path(folder["Path"])
|
|
248
|
+
if path.exists():
|
|
249
|
+
quick_access_items.append(
|
|
250
|
+
{"name": folder["Name"], "type": "folder", "path": str(path)}
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Add user's home directory if not already included
|
|
254
|
+
home_path = str(Path.home())
|
|
255
|
+
if home_path not in [item["path"] for item in quick_access_items]:
|
|
256
|
+
quick_access_items.append({"name": "Home", "type": "folder", "path": home_path})
|
|
257
|
+
|
|
258
|
+
return {"items": quick_access_items}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def get_macos_favorites():
|
|
262
|
+
import subprocess
|
|
263
|
+
import json
|
|
264
|
+
|
|
265
|
+
favorites = []
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
# Common locations in macOS
|
|
269
|
+
common_locations = [
|
|
270
|
+
("Desktop", Path.home() / "Desktop"),
|
|
271
|
+
("Documents", Path.home() / "Documents"),
|
|
272
|
+
("Downloads", Path.home() / "Downloads"),
|
|
273
|
+
("Pictures", Path.home() / "Pictures"),
|
|
274
|
+
("Music", Path.home() / "Music"),
|
|
275
|
+
("Movies", Path.home() / "Movies"),
|
|
276
|
+
]
|
|
277
|
+
|
|
278
|
+
for name, path in common_locations:
|
|
279
|
+
if path.exists():
|
|
280
|
+
favorites.append({"Name": name, "Path": str(path)})
|
|
281
|
+
|
|
282
|
+
# Get user-defined favorites from sidebar plist
|
|
283
|
+
plist_path = (
|
|
284
|
+
Path.home()
|
|
285
|
+
/ "Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.FavoriteItems.sfl2"
|
|
286
|
+
)
|
|
287
|
+
if plist_path.exists():
|
|
288
|
+
try:
|
|
289
|
+
result = subprocess.run(
|
|
290
|
+
["plutil", "-convert", "json", "-o", "-", str(plist_path)],
|
|
291
|
+
capture_output=True,
|
|
292
|
+
text=True,
|
|
293
|
+
check=True,
|
|
294
|
+
)
|
|
295
|
+
plist_data = json.loads(result.stdout)
|
|
296
|
+
for item in plist_data.get("Bookmark", []):
|
|
297
|
+
if "Name" in item and "URL" in item:
|
|
298
|
+
path = item["URL"].replace("file://", "")
|
|
299
|
+
favorites.append({"Name": item["Name"], "Path": path})
|
|
300
|
+
except (subprocess.CalledProcessError, json.JSONDecodeError) as e:
|
|
301
|
+
logger.error(f"Error processing macOS favorites: {e}")
|
|
302
|
+
|
|
303
|
+
except Exception as e:
|
|
304
|
+
logger.error(f"Error getting macOS favorites: {e}")
|
|
305
|
+
|
|
306
|
+
return favorites
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import time
|
|
3
|
+
import logging
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
from .app_utils import ansi_color, ansi_reset
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
import webview
|
|
12
|
+
|
|
13
|
+
DEV = True
|
|
14
|
+
|
|
15
|
+
VITE_URL = "http://localhost:54321"
|
|
16
|
+
FASTAPI_URL = "http://localhost:50001"
|
|
17
|
+
APP_URL = VITE_URL if DEV else FASTAPI_URL
|
|
18
|
+
|
|
19
|
+
USER_AGENT = "PyNmWebView"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class WebViewWindow:
|
|
23
|
+
def __init__(self, debug: bool = False) -> None:
|
|
24
|
+
import webview
|
|
25
|
+
|
|
26
|
+
self.debug = debug
|
|
27
|
+
self.api = WebViewWindowApi()
|
|
28
|
+
|
|
29
|
+
self.window = webview.create_window(
|
|
30
|
+
title="PyNeuromodulation GUI",
|
|
31
|
+
url=APP_URL,
|
|
32
|
+
min_size=(1200, 800),
|
|
33
|
+
frameless=True,
|
|
34
|
+
resizable=True,
|
|
35
|
+
easy_drag=False,
|
|
36
|
+
js_api=self.api,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
self.api.register_window(self.window)
|
|
40
|
+
# Customize PyWebView logging format
|
|
41
|
+
color = ansi_color(color="CYAN", styles=["BOLD"])
|
|
42
|
+
logger = logging.getLogger("pywebview")
|
|
43
|
+
formatter = logging.Formatter(
|
|
44
|
+
f"{color}[PyWebView %(levelname)s (%(asctime)s)]:{ansi_reset} %(message)s",
|
|
45
|
+
datefmt="%H:%M:%S",
|
|
46
|
+
)
|
|
47
|
+
logger.handlers[0].setFormatter(formatter)
|
|
48
|
+
|
|
49
|
+
def start(self):
|
|
50
|
+
import webview
|
|
51
|
+
|
|
52
|
+
# Set timer to load SPA after a delay
|
|
53
|
+
if DEV:
|
|
54
|
+
self.wait_for_vite_server()
|
|
55
|
+
|
|
56
|
+
webview.start(debug=self.debug, user_agent=USER_AGENT)
|
|
57
|
+
|
|
58
|
+
def wait_for_vite_server(self):
|
|
59
|
+
while True:
|
|
60
|
+
if self.is_vite_server_running():
|
|
61
|
+
break
|
|
62
|
+
time.sleep(0.1) # Wait for 1 second before checking again
|
|
63
|
+
|
|
64
|
+
def is_vite_server_running(self):
|
|
65
|
+
try:
|
|
66
|
+
response = requests.get(VITE_URL, timeout=1)
|
|
67
|
+
return response.status_code == 200
|
|
68
|
+
except requests.RequestException:
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
# Register event handlers
|
|
72
|
+
def register_event_handler(self, event_type, handler):
|
|
73
|
+
# https://pywebview.flowrl.com/guide/api.html#window-events
|
|
74
|
+
match event_type:
|
|
75
|
+
case "closed":
|
|
76
|
+
self.window.events.closed += handler
|
|
77
|
+
case "closing":
|
|
78
|
+
self.window.events.closing += handler
|
|
79
|
+
case "loaded":
|
|
80
|
+
self.window.events.loaded += handler
|
|
81
|
+
case "minimized":
|
|
82
|
+
self.window.events.minimized += handler
|
|
83
|
+
case "maximized":
|
|
84
|
+
self.window.events.maximized += handler
|
|
85
|
+
case "resized":
|
|
86
|
+
self.window.events.resized += handler
|
|
87
|
+
case "restore":
|
|
88
|
+
self.window.events.restore += handler
|
|
89
|
+
case "shown":
|
|
90
|
+
self.window.events.shown += handler
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# API class implementing all the methods available in the PyWebView Window object
|
|
94
|
+
# API Reference: https://pywebview.flowrl.com/guide/api.html#webview-window
|
|
95
|
+
class WebViewWindowApi:
|
|
96
|
+
def __init__(self):
|
|
97
|
+
self._window: "webview.Window"
|
|
98
|
+
self.is_resizing = False
|
|
99
|
+
self.start_x = 0
|
|
100
|
+
self.start_y = 0
|
|
101
|
+
self.start_width = 0
|
|
102
|
+
self.start_height = 0
|
|
103
|
+
|
|
104
|
+
# Function to store the reference to the PyWevView window
|
|
105
|
+
def register_window(self, window: "webview.Window"):
|
|
106
|
+
self._window = window
|
|
107
|
+
|
|
108
|
+
# Functions to handle window resizing
|
|
109
|
+
def start_resize(self, start_x, start_y):
|
|
110
|
+
self.is_resizing = True
|
|
111
|
+
self.start_x = start_x
|
|
112
|
+
self.start_y = start_y
|
|
113
|
+
self.start_width, self.start_height = self.get_size()
|
|
114
|
+
threading.Thread(target=self._resize_loop).start()
|
|
115
|
+
|
|
116
|
+
def stop_resize(self):
|
|
117
|
+
self.is_resizing = False
|
|
118
|
+
|
|
119
|
+
def update_resize(self, current_x, current_y):
|
|
120
|
+
if self.is_resizing:
|
|
121
|
+
dx = current_x - self.start_x
|
|
122
|
+
dy = current_y - self.start_y
|
|
123
|
+
new_width = max(self.start_width + dx, 200) # Minimum width
|
|
124
|
+
new_height = max(self.start_height + dy, 200) # Minimum height
|
|
125
|
+
self.set_size(int(new_width), int(new_height))
|
|
126
|
+
|
|
127
|
+
def _resize_loop(self):
|
|
128
|
+
while self.is_resizing:
|
|
129
|
+
time.sleep(0.01) # Small delay to prevent excessive CPU usage
|
|
130
|
+
|
|
131
|
+
# All API methods from the PyWebView docs
|
|
132
|
+
def close_window(self):
|
|
133
|
+
self._window.destroy()
|
|
134
|
+
|
|
135
|
+
def maximize_window(self):
|
|
136
|
+
self._window.maximize()
|
|
137
|
+
|
|
138
|
+
def minimize_window(self):
|
|
139
|
+
self._window.minimize()
|
|
140
|
+
|
|
141
|
+
def restore_window(self):
|
|
142
|
+
self._window.restore()
|
|
143
|
+
|
|
144
|
+
def toggle_fullscreen(self):
|
|
145
|
+
self._window.toggle_fullscreen()
|
|
146
|
+
|
|
147
|
+
def set_title(self, title: str):
|
|
148
|
+
self._window.title = title
|
|
149
|
+
|
|
150
|
+
def get_position(self):
|
|
151
|
+
return (self._window.x, self._window.y)
|
|
152
|
+
|
|
153
|
+
def set_position(self, x: int, y: int):
|
|
154
|
+
self._window.move(x, y)
|
|
155
|
+
|
|
156
|
+
def get_size(self):
|
|
157
|
+
return (self._window.width, self._window.height)
|
|
158
|
+
|
|
159
|
+
def set_size(self, width: int, height: int):
|
|
160
|
+
self._window.resize(width, height)
|
|
161
|
+
|
|
162
|
+
def set_on_top(self, on_top: bool):
|
|
163
|
+
self._window.on_top = on_top
|
|
164
|
+
|
|
165
|
+
def show(self):
|
|
166
|
+
self._window.show()
|
|
167
|
+
|
|
168
|
+
def hide(self):
|
|
169
|
+
self._window.hide()
|
|
170
|
+
|
|
171
|
+
def create_file_dialog(
|
|
172
|
+
self,
|
|
173
|
+
dialog_type: int = 10, # webview.OPEN_DIALOG,
|
|
174
|
+
directory="",
|
|
175
|
+
allow_multiple=False,
|
|
176
|
+
save_filename="",
|
|
177
|
+
file_types=(),
|
|
178
|
+
):
|
|
179
|
+
return self._window.create_file_dialog(
|
|
180
|
+
dialog_type, directory, allow_multiple, save_filename, file_types
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def create_confirmation_dialog(self, title, message):
|
|
184
|
+
return self._window.create_confirmation_dialog(title, message)
|
|
185
|
+
|
|
186
|
+
def load_url(self, url):
|
|
187
|
+
self._window.load_url(url)
|
|
188
|
+
|
|
189
|
+
def load_html(self, content, base_uri: str):
|
|
190
|
+
self._window.load_html(content, base_uri)
|
|
191
|
+
|
|
192
|
+
def load_css(self, css):
|
|
193
|
+
self._window.load_css(css)
|
|
194
|
+
|
|
195
|
+
def evaluate_js(self, script, callback=None):
|
|
196
|
+
return self._window.evaluate_js(script, callback)
|
|
197
|
+
|
|
198
|
+
def get_current_url(self):
|
|
199
|
+
return self._window.get_current_url()
|
|
200
|
+
|
|
201
|
+
def get_elements(self, selector):
|
|
202
|
+
return self._window.get_elements(selector)
|