stupidpythonbrowser 4.7.2__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.
- pybrowser/__init__.py +14 -0
- pybrowser/browser.py +3175 -0
- pybrowser/doh_server.py +423 -0
- pybrowser/incognito.py +36 -0
- pybrowser/main.py +66 -0
- pybrowser/settings.py +656 -0
- pybrowser/task_manager.py +366 -0
- pybrowser/terminal.py +451 -0
- stupidpythonbrowser-4.7.2.dist-info/METADATA +25 -0
- stupidpythonbrowser-4.7.2.dist-info/RECORD +14 -0
- stupidpythonbrowser-4.7.2.dist-info/WHEEL +5 -0
- stupidpythonbrowser-4.7.2.dist-info/entry_points.txt +2 -0
- stupidpythonbrowser-4.7.2.dist-info/licenses/LICENSE +674 -0
- stupidpythonbrowser-4.7.2.dist-info/top_level.txt +1 -0
pybrowser/browser.py
ADDED
|
@@ -0,0 +1,3175 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PyBrowser: Native macOS Chromium/Material 3 Web Browser (PyObjC & WebKit).
|
|
3
|
+
Main Browser Module - Part 1: Views, Delegates, Coordinators & Utilities.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import json
|
|
9
|
+
import time
|
|
10
|
+
import uuid
|
|
11
|
+
import re
|
|
12
|
+
import ast
|
|
13
|
+
import operator
|
|
14
|
+
import threading
|
|
15
|
+
import ctypes
|
|
16
|
+
import base64
|
|
17
|
+
import urllib.parse
|
|
18
|
+
import urllib.request
|
|
19
|
+
import sqlite3
|
|
20
|
+
import traceback
|
|
21
|
+
import tempfile
|
|
22
|
+
import struct
|
|
23
|
+
import zipfile
|
|
24
|
+
import io
|
|
25
|
+
import html
|
|
26
|
+
from dataclasses import dataclass, asdict
|
|
27
|
+
from ctypes import c_void_p, c_uint32, c_char_p, POINTER, byref, string_at
|
|
28
|
+
|
|
29
|
+
import Cocoa
|
|
30
|
+
import Foundation
|
|
31
|
+
import WebKit
|
|
32
|
+
import objc
|
|
33
|
+
|
|
34
|
+
from pybrowser.settings import (
|
|
35
|
+
SETTINGS, UA_PRESETS, DEFAULT_URL, DATA_DIR, SESSION_FILE, BOOKMARKS_FILE,
|
|
36
|
+
SETTINGS_FILE, HISTORY_FILE, ADBLOCK_CACHE_FILE, DNS_CACHE_FILE, USERSCRIPTS_DIR,
|
|
37
|
+
FILTER_LIST_URL, LOCAL_PROXY_PORT, BASE_ADBLOCK_DOMAINS, M3_COLORS, GROUP_COLORS,
|
|
38
|
+
validate_and_get_download_dir, get_settings_html, sanitize_suggested_filename
|
|
39
|
+
)
|
|
40
|
+
from pybrowser.doh_server import (
|
|
41
|
+
DNS_CACHE, DNS_LOCK, STEVENBLACK_DOMAINS, BASE_ADBLOCK_DOMAINS, ADBLOCK_LOCK,
|
|
42
|
+
WK_RULE_LIST, is_domain_blocked, search_blocked_domains, load_dns_cache,
|
|
43
|
+
start_proxy_server, create_nw_proxy_config, load_adblock_filters_from_disk,
|
|
44
|
+
compile_webkit_adblock_rules, update_adblock_filters_from_web
|
|
45
|
+
)
|
|
46
|
+
from pybrowser.incognito import create_incognito_data_store, apply_incognito_preferences
|
|
47
|
+
from pybrowser.terminal import TERMINAL_MANAGER, dispatch_to_main
|
|
48
|
+
from pybrowser.task_manager import ProcessManagerController
|
|
49
|
+
|
|
50
|
+
# ============================================================================
|
|
51
|
+
# UTILITIES, DRAWING & KEYCHAIN
|
|
52
|
+
# ============================================================================
|
|
53
|
+
def hex_to_nscolor(hex_str):
|
|
54
|
+
if not hex_str:
|
|
55
|
+
return Cocoa.NSColor.clearColor()
|
|
56
|
+
if hex_str.startswith("rgba"):
|
|
57
|
+
parts = hex_str[5:-1].split(",")
|
|
58
|
+
return Cocoa.NSColor.colorWithSRGBRed_green_blue_alpha_(
|
|
59
|
+
float(parts[0]) / 255.0, float(parts[1]) / 255.0, float(parts[2]) / 255.0, float(parts[3])
|
|
60
|
+
)
|
|
61
|
+
clean_hex = hex_str.lstrip('#')
|
|
62
|
+
if len(clean_hex) == 6:
|
|
63
|
+
r, g, b = tuple(int(clean_hex[i:i+2], 16) for i in (0, 2, 4))
|
|
64
|
+
return Cocoa.NSColor.colorWithSRGBRed_green_blue_alpha_(r / 255.0, g / 255.0, b / 255.0, 1.0)
|
|
65
|
+
elif len(clean_hex) == 8:
|
|
66
|
+
r, g, b, a = tuple(int(clean_hex[i:i+2], 16) for i in (0, 2, 4, 6))
|
|
67
|
+
return Cocoa.NSColor.colorWithSRGBRed_green_blue_alpha_(r / 255.0, g / 255.0, b / 255.0, a / 255.0)
|
|
68
|
+
return Cocoa.NSColor.labelColor()
|
|
69
|
+
|
|
70
|
+
def get_m3_color(token, view, is_incognito=False):
|
|
71
|
+
if is_incognito and token == "tabstrip-bg":
|
|
72
|
+
token = "tabstrip-incognito"
|
|
73
|
+
elif is_incognito and token == "toolbar-bg":
|
|
74
|
+
token = "toolbar-incognito"
|
|
75
|
+
theme = SETTINGS.settings.get("theme_mode", "system")
|
|
76
|
+
is_dark = is_incognito or (theme == "dark")
|
|
77
|
+
if not is_dark and theme == "system" and view and hasattr(view, "effectiveAppearance") and view.effectiveAppearance():
|
|
78
|
+
try:
|
|
79
|
+
is_dark = "Dark" in str(view.effectiveAppearance().name())
|
|
80
|
+
except Exception:
|
|
81
|
+
pass
|
|
82
|
+
colors = M3_COLORS.get(token, ("#FFFFFF", "#000000"))
|
|
83
|
+
return hex_to_nscolor(colors[1 if is_dark else 0])
|
|
84
|
+
|
|
85
|
+
def atomic_write_json(filepath, data):
|
|
86
|
+
try:
|
|
87
|
+
dir_name = os.path.dirname(filepath) or "."
|
|
88
|
+
fd, temp_path = tempfile.mkstemp(dir=dir_name)
|
|
89
|
+
with os.fdopen(fd, 'w', encoding='utf-8') as f:
|
|
90
|
+
json.dump(data, f, indent=2)
|
|
91
|
+
os.replace(temp_path, filepath)
|
|
92
|
+
except Exception as e:
|
|
93
|
+
print(f"[Atomic JSON Write Error] {e}")
|
|
94
|
+
|
|
95
|
+
def download_image_data(img_url, dest_path, webview=None, completion_callback=None):
|
|
96
|
+
if not img_url:
|
|
97
|
+
if completion_callback: completion_callback(False)
|
|
98
|
+
return False
|
|
99
|
+
if img_url.startswith("data:"):
|
|
100
|
+
try:
|
|
101
|
+
raw = base64.b64decode(img_url.split(",", 1)[1])
|
|
102
|
+
with open(dest_path, "wb") as f:
|
|
103
|
+
f.write(raw)
|
|
104
|
+
if completion_callback: completion_callback(True)
|
|
105
|
+
return True
|
|
106
|
+
except Exception:
|
|
107
|
+
if completion_callback: completion_callback(False)
|
|
108
|
+
return False
|
|
109
|
+
if img_url.startswith("blob:"):
|
|
110
|
+
if webview:
|
|
111
|
+
dest_json = json.dumps(dest_path)
|
|
112
|
+
js = f"""(async () => {{
|
|
113
|
+
try {{
|
|
114
|
+
const res = await fetch("{img_url}");
|
|
115
|
+
const blob = await res.blob();
|
|
116
|
+
const reader = new FileReader();
|
|
117
|
+
reader.onloadend = () => {{
|
|
118
|
+
window.webkit.messageHandlers.bridge.postMessage({{cmd: 'blob_image_data', data: reader.result, path: {dest_json}}});
|
|
119
|
+
}};
|
|
120
|
+
reader.readAsDataURL(blob);
|
|
121
|
+
}} catch(e) {{
|
|
122
|
+
window.webkit.messageHandlers.bridge.postMessage({{cmd: 'image_download_failed', path: {dest_json}}});
|
|
123
|
+
}}
|
|
124
|
+
}})();"""
|
|
125
|
+
dispatch_to_main(lambda: webview.evaluateJavaScript_completionHandler_(js, None))
|
|
126
|
+
return True
|
|
127
|
+
if completion_callback: completion_callback(False)
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
def worker():
|
|
131
|
+
success = False
|
|
132
|
+
try:
|
|
133
|
+
referer = webview.URL().absoluteString() if (webview and webview.URL()) else img_url
|
|
134
|
+
req = urllib.request.Request(img_url, headers={
|
|
135
|
+
"User-Agent": SETTINGS.get_user_agent(),
|
|
136
|
+
"Accept": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
|
|
137
|
+
"Referer": referer
|
|
138
|
+
})
|
|
139
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
140
|
+
with open(dest_path, "wb") as f:
|
|
141
|
+
f.write(resp.read())
|
|
142
|
+
success = True
|
|
143
|
+
except Exception:
|
|
144
|
+
if webview:
|
|
145
|
+
dest_json = json.dumps(dest_path)
|
|
146
|
+
js = f"""(async () => {{
|
|
147
|
+
try {{
|
|
148
|
+
const res = await fetch("{img_url}", {{ credentials: 'include' }});
|
|
149
|
+
const blob = await res.blob();
|
|
150
|
+
const reader = new FileReader();
|
|
151
|
+
reader.onloadend = () => {{
|
|
152
|
+
window.webkit.messageHandlers.bridge.postMessage({{cmd: 'blob_image_data', data: reader.result, path: {dest_json}}});
|
|
153
|
+
}};
|
|
154
|
+
reader.readAsDataURL(blob);
|
|
155
|
+
}} catch(err) {{
|
|
156
|
+
window.webkit.messageHandlers.bridge.postMessage({{cmd: 'image_download_failed', path: {dest_json}}});
|
|
157
|
+
}}
|
|
158
|
+
}})();"""
|
|
159
|
+
dispatch_to_main(lambda: webview.evaluateJavaScript_completionHandler_(js, None))
|
|
160
|
+
return
|
|
161
|
+
if completion_callback:
|
|
162
|
+
dispatch_to_main(lambda: completion_callback(success))
|
|
163
|
+
|
|
164
|
+
threading.Thread(target=worker, daemon=True).start()
|
|
165
|
+
return True
|
|
166
|
+
|
|
167
|
+
def safe_math_eval(expr):
|
|
168
|
+
expr = expr.strip()
|
|
169
|
+
if not expr or not re.search(r'[\+\-\*/%]', expr):
|
|
170
|
+
return None
|
|
171
|
+
expr = re.sub(r'([\d\.]+)\s*%\s*of\s*([\d\.]+)', r'(\1/100)*\2', expr)
|
|
172
|
+
if not re.match(r'^[\d\.\s\+\-\*/\(\)]+$', expr):
|
|
173
|
+
return None
|
|
174
|
+
try:
|
|
175
|
+
node = ast.parse(expr, mode='eval').body
|
|
176
|
+
def _eval(n):
|
|
177
|
+
if isinstance(n, (ast.Constant, ast.Num)):
|
|
178
|
+
return getattr(n, 'value', getattr(n, 'n', None))
|
|
179
|
+
elif isinstance(n, ast.BinOp):
|
|
180
|
+
op = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv}[type(n.op)]
|
|
181
|
+
return op(_eval(n.left), _eval(n.right))
|
|
182
|
+
elif isinstance(n, ast.UnaryOp):
|
|
183
|
+
op = {ast.USub: operator.neg, ast.UAdd: operator.pos}[type(n.op)]
|
|
184
|
+
return op(_eval(n.operand))
|
|
185
|
+
raise ValueError()
|
|
186
|
+
res = _eval(node)
|
|
187
|
+
return str(int(res)) if (isinstance(res, float) and res.is_integer()) else str(round(res, 4))
|
|
188
|
+
except Exception:
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
sec = ctypes.cdll.LoadLibrary("/System/Library/Frameworks/Security.framework/Security")
|
|
193
|
+
SecKeychainAddGenericPassword = sec.SecKeychainAddGenericPassword
|
|
194
|
+
SecKeychainAddGenericPassword.argtypes = [c_void_p, c_uint32, c_char_p, c_uint32, c_char_p, c_uint32, c_char_p, c_void_p]
|
|
195
|
+
SecKeychainFindGenericPassword = sec.SecKeychainFindGenericPassword
|
|
196
|
+
SecKeychainFindGenericPassword.argtypes = [c_void_p, c_uint32, c_char_p, c_uint32, c_char_p, POINTER(c_uint32), POINTER(c_void_p), POINTER(c_void_p)]
|
|
197
|
+
SecKeychainItemFreeContent = sec.SecKeychainItemFreeContent
|
|
198
|
+
SecKeychainItemDelete = sec.SecKeychainItemDelete
|
|
199
|
+
SecKeychainItemDelete.argtypes = [c_void_p]
|
|
200
|
+
except Exception:
|
|
201
|
+
pass
|
|
202
|
+
|
|
203
|
+
def save_to_keychain(host, username, password):
|
|
204
|
+
try:
|
|
205
|
+
srv, acc, pwd = host.encode('utf-8'), username.encode('utf-8'), password.encode('utf-8')
|
|
206
|
+
length, data_ptr, item_ref = c_uint32(), c_void_p(), c_void_p()
|
|
207
|
+
if SecKeychainFindGenericPassword(None, len(srv), srv, len(acc), acc, byref(length), byref(data_ptr), byref(item_ref)) == 0:
|
|
208
|
+
if data_ptr.value: SecKeychainItemFreeContent(None, data_ptr)
|
|
209
|
+
if item_ref.value: SecKeychainItemDelete(item_ref.value)
|
|
210
|
+
SecKeychainAddGenericPassword(None, len(srv), srv, len(acc), acc, len(pwd), pwd, None)
|
|
211
|
+
except Exception:
|
|
212
|
+
pass
|
|
213
|
+
|
|
214
|
+
def get_from_keychain(host, username):
|
|
215
|
+
try:
|
|
216
|
+
srv, acc = host.encode('utf-8'), username.encode('utf-8')
|
|
217
|
+
length, data_ptr, item_ref = c_uint32(), c_void_p(), c_void_p()
|
|
218
|
+
if SecKeychainFindGenericPassword(None, len(srv), srv, len(acc), acc, byref(length), byref(data_ptr), byref(item_ref)) == 0:
|
|
219
|
+
pwd = string_at(data_ptr.value, length.value).decode('utf-8')
|
|
220
|
+
SecKeychainItemFreeContent(None, data_ptr)
|
|
221
|
+
return pwd
|
|
222
|
+
except Exception:
|
|
223
|
+
pass
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
# ============================================================================
|
|
227
|
+
# USERSCRIPTS & EXTENSIONS (CRX)
|
|
228
|
+
# ============================================================================
|
|
229
|
+
def parse_userscript_meta(content):
|
|
230
|
+
meta = {"name": "Unnamed Script", "match": [], "run-at": "document-end", "enabled": True}
|
|
231
|
+
in_meta = False
|
|
232
|
+
for line in content.splitlines():
|
|
233
|
+
line = line.strip()
|
|
234
|
+
if line == "// ==UserScript==":
|
|
235
|
+
in_meta = True
|
|
236
|
+
elif line == "// ==/UserScript==":
|
|
237
|
+
break
|
|
238
|
+
elif in_meta and line.startswith("// @"):
|
|
239
|
+
parts = line.split(maxsplit=2)
|
|
240
|
+
if len(parts) >= 3:
|
|
241
|
+
key, val = parts[1][1:], parts[2]
|
|
242
|
+
if key in ("match", "include"):
|
|
243
|
+
meta["match"].append(val)
|
|
244
|
+
else:
|
|
245
|
+
meta[key] = val
|
|
246
|
+
return meta
|
|
247
|
+
|
|
248
|
+
def get_installed_userscripts():
|
|
249
|
+
scripts = []
|
|
250
|
+
if not os.path.exists(USERSCRIPTS_DIR):
|
|
251
|
+
return scripts
|
|
252
|
+
for fname in os.listdir(USERSCRIPTS_DIR):
|
|
253
|
+
if fname.endswith(".user.js") or fname.endswith(".user.js.disabled"):
|
|
254
|
+
real_name = fname[:-9] if fname.endswith(".disabled") else fname
|
|
255
|
+
path = os.path.join(USERSCRIPTS_DIR, fname)
|
|
256
|
+
try:
|
|
257
|
+
with open(path, 'r', encoding='utf-8') as f:
|
|
258
|
+
content = f.read()
|
|
259
|
+
meta = parse_userscript_meta(content)
|
|
260
|
+
meta["file"] = real_name
|
|
261
|
+
meta["actual_path"] = path
|
|
262
|
+
meta["code"] = content
|
|
263
|
+
meta["enabled"] = not fname.endswith(".disabled")
|
|
264
|
+
scripts.append(meta)
|
|
265
|
+
except Exception:
|
|
266
|
+
pass
|
|
267
|
+
return scripts
|
|
268
|
+
|
|
269
|
+
def install_crx(ext_id_or_url):
|
|
270
|
+
try:
|
|
271
|
+
m = re.search(r'([a-z]{32})', ext_id_or_url.lower())
|
|
272
|
+
ext_id = m.group(1) if m else ext_id_or_url.split("=")[-1].split("/")[-1].split("?")[0].strip()
|
|
273
|
+
url = f"https://clients2.google.com/service/update2/crx?response=redirect&prodversion=131.0&x=id%3D{ext_id}%26installsource%3Dondemand%26uc"
|
|
274
|
+
req = urllib.request.Request(url, headers={"User-Agent": UA_PRESETS["chrome_mac"]})
|
|
275
|
+
with urllib.request.urlopen(req, timeout=12) as resp:
|
|
276
|
+
data = resp.read()
|
|
277
|
+
if len(data) < 16 or data[:4] != b'Cr24':
|
|
278
|
+
return False
|
|
279
|
+
version = struct.unpack("<I", data[4:8])[0]
|
|
280
|
+
if version == 3:
|
|
281
|
+
zip_data = data[12 + struct.unpack("<I", data[8:12])[0]:]
|
|
282
|
+
elif version == 2:
|
|
283
|
+
zip_data = data[16 + struct.unpack("<I", data[8:12])[0] + struct.unpack("<I", data[12:16])[0]:]
|
|
284
|
+
else:
|
|
285
|
+
return False
|
|
286
|
+
|
|
287
|
+
with zipfile.ZipFile(io.BytesIO(zip_data)) as z:
|
|
288
|
+
manifest = json.loads(z.read("manifest.json").decode('utf-8', errors='ignore'))
|
|
289
|
+
name = manifest.get("name", ext_id)
|
|
290
|
+
content_scripts = manifest.get("content_scripts", [])
|
|
291
|
+
|
|
292
|
+
combined_js = f"// ==UserScript==\n// @name {name} (CRX)\n// @match <all_urls>\n// ==/UserScript==\n"
|
|
293
|
+
for cs in content_scripts:
|
|
294
|
+
matches, css_files, js_files = cs.get("matches", []), cs.get("css", []), cs.get("js", [])
|
|
295
|
+
match_regex = " || ".join([f"new RegExp('^' + '{m}'.replace(/\\*/g, '.*').replace(/\\//g, '\\\\/') + '$').test(location.href)" for m in matches]) or "true"
|
|
296
|
+
combined_js += f"if ({match_regex}) {{\n"
|
|
297
|
+
for css in css_files:
|
|
298
|
+
try:
|
|
299
|
+
css_content = z.read(css).decode('utf-8', errors='ignore').replace('`', '\\`').replace('$', '\\$')
|
|
300
|
+
combined_js += f"const st = document.createElement('style'); st.textContent = `{css_content}`; (document.head || document.documentElement).appendChild(st);\n"
|
|
301
|
+
except Exception:
|
|
302
|
+
pass
|
|
303
|
+
for jf in js_files:
|
|
304
|
+
try:
|
|
305
|
+
combined_js += z.read(jf).decode('utf-8', errors='ignore') + "\n"
|
|
306
|
+
except Exception:
|
|
307
|
+
pass
|
|
308
|
+
combined_js += "}\n"
|
|
309
|
+
|
|
310
|
+
with open(os.path.join(USERSCRIPTS_DIR, f"{ext_id}.user.js"), "w", encoding="utf-8") as f:
|
|
311
|
+
f.write(combined_js)
|
|
312
|
+
|
|
313
|
+
dispatch_to_main(reload_all_extensions_views)
|
|
314
|
+
return True
|
|
315
|
+
except Exception as e:
|
|
316
|
+
print(f"[CRX Install Error] {e}")
|
|
317
|
+
return False
|
|
318
|
+
|
|
319
|
+
def reload_all_extensions_views():
|
|
320
|
+
app_delegate = Cocoa.NSApp.delegate()
|
|
321
|
+
if not app_delegate:
|
|
322
|
+
return
|
|
323
|
+
for wc in getattr(app_delegate, "controllers", []):
|
|
324
|
+
for tab in wc.tabs:
|
|
325
|
+
wv = tab.get("webview")
|
|
326
|
+
if wv and wv.URL() and "browser://extensions" in wv.URL().absoluteString():
|
|
327
|
+
wv.reload()
|
|
328
|
+
|
|
329
|
+
# ============================================================================
|
|
330
|
+
# SQLITE HISTORY & FAVICONS
|
|
331
|
+
# ============================================================================
|
|
332
|
+
def init_history_db():
|
|
333
|
+
try:
|
|
334
|
+
with sqlite3.connect(HISTORY_FILE) as conn:
|
|
335
|
+
conn.execute("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;")
|
|
336
|
+
conn.execute("""
|
|
337
|
+
CREATE TABLE IF NOT EXISTS history (
|
|
338
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT UNIQUE, title TEXT,
|
|
339
|
+
visit_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, visit_count INTEGER DEFAULT 1
|
|
340
|
+
);
|
|
341
|
+
""")
|
|
342
|
+
conn.execute("CREATE INDEX IF NOT EXISTS idx_history_time ON history(visit_time DESC);")
|
|
343
|
+
except Exception as e:
|
|
344
|
+
print(f"[History DB Error] {e}")
|
|
345
|
+
|
|
346
|
+
def record_history(url, title):
|
|
347
|
+
if not url or url.startswith("browser://"):
|
|
348
|
+
return
|
|
349
|
+
try:
|
|
350
|
+
with sqlite3.connect(HISTORY_FILE) as conn:
|
|
351
|
+
conn.execute("""
|
|
352
|
+
INSERT INTO history (url, title, visit_time, visit_count) VALUES (?, ?, CURRENT_TIMESTAMP, 1)
|
|
353
|
+
ON CONFLICT(url) DO UPDATE SET title = excluded.title, visit_time = CURRENT_TIMESTAMP, visit_count = visit_count + 1;
|
|
354
|
+
""", (url, title or url))
|
|
355
|
+
except Exception:
|
|
356
|
+
pass
|
|
357
|
+
|
|
358
|
+
def query_history(keyword=None):
|
|
359
|
+
try:
|
|
360
|
+
with sqlite3.connect(HISTORY_FILE) as conn:
|
|
361
|
+
cursor = conn.cursor()
|
|
362
|
+
if keyword:
|
|
363
|
+
cursor.execute(
|
|
364
|
+
"SELECT id, url, title, datetime(visit_time, 'localtime'), visit_count FROM history WHERE url LIKE ? OR title LIKE ? ORDER BY visit_time DESC LIMIT 100",
|
|
365
|
+
(f"%{keyword}%", f"%{keyword}%")
|
|
366
|
+
)
|
|
367
|
+
else:
|
|
368
|
+
cursor.execute("SELECT id, url, title, datetime(visit_time, 'localtime'), visit_count FROM history ORDER BY visit_time DESC LIMIT 100")
|
|
369
|
+
return cursor.fetchall()
|
|
370
|
+
except Exception:
|
|
371
|
+
return []
|
|
372
|
+
|
|
373
|
+
def clear_history():
|
|
374
|
+
try:
|
|
375
|
+
with sqlite3.connect(HISTORY_FILE) as conn:
|
|
376
|
+
conn.execute("DELETE FROM history;")
|
|
377
|
+
except Exception:
|
|
378
|
+
pass
|
|
379
|
+
|
|
380
|
+
class FaviconCache:
|
|
381
|
+
_cache = {}
|
|
382
|
+
_lock = threading.Lock()
|
|
383
|
+
|
|
384
|
+
@classmethod
|
|
385
|
+
def clear(cls):
|
|
386
|
+
with cls._lock:
|
|
387
|
+
cls._cache.clear()
|
|
388
|
+
|
|
389
|
+
@classmethod
|
|
390
|
+
def get_favicon(cls, host, callback):
|
|
391
|
+
if not host or host in ("localhost", "127.0.0.1", "::1"):
|
|
392
|
+
return
|
|
393
|
+
with cls._lock:
|
|
394
|
+
if host in cls._cache:
|
|
395
|
+
callback(cls._cache[host])
|
|
396
|
+
return
|
|
397
|
+
def fetcher():
|
|
398
|
+
try:
|
|
399
|
+
url = f"https://www.google.com/s2/favicons?domain={urllib.parse.quote(host)}&sz=32"
|
|
400
|
+
req = urllib.request.Request(url, headers={"User-Agent": SETTINGS.get_user_agent()})
|
|
401
|
+
with urllib.request.urlopen(req, timeout=2.5) as resp:
|
|
402
|
+
raw = resp.read()
|
|
403
|
+
if raw:
|
|
404
|
+
img = Cocoa.NSImage.alloc().initWithData_(Foundation.NSData.dataWithBytes_length_(raw, len(raw)))
|
|
405
|
+
if img:
|
|
406
|
+
with cls._lock:
|
|
407
|
+
cls._cache[host] = img
|
|
408
|
+
dispatch_to_main(lambda: callback(img))
|
|
409
|
+
except Exception:
|
|
410
|
+
pass
|
|
411
|
+
threading.Thread(target=fetcher, daemon=True).start()
|
|
412
|
+
|
|
413
|
+
# ============================================================================
|
|
414
|
+
# INJECTED SCRIPTS & FULLSCREEN UTILITIES
|
|
415
|
+
# ============================================================================
|
|
416
|
+
AUDIO_MONITOR_JS = """
|
|
417
|
+
document.addEventListener('play', (e) => {
|
|
418
|
+
if (e.target && !e.target.muted) window.webkit.messageHandlers.audio.postMessage({cmd:'audio_play'});
|
|
419
|
+
}, true);
|
|
420
|
+
document.addEventListener('pause', () => window.webkit.messageHandlers.audio.postMessage({cmd:'audio_pause'}), true);
|
|
421
|
+
document.addEventListener('ended', () => window.webkit.messageHandlers.audio.postMessage({cmd:'audio_pause'}), true);
|
|
422
|
+
document.addEventListener('volumechange', (e) => {
|
|
423
|
+
if (e.target && e.target.muted) window.webkit.messageHandlers.audio.postMessage({cmd:'audio_pause'});
|
|
424
|
+
}, true);
|
|
425
|
+
"""
|
|
426
|
+
|
|
427
|
+
IMAGE_CONTEXT_MENU_JS = """
|
|
428
|
+
(function() {
|
|
429
|
+
document.addEventListener('contextmenu', function(e) {
|
|
430
|
+
let el = e.target, imgSrc = null;
|
|
431
|
+
let img = el.closest('img') || (el.tagName === 'IMG' ? el : null);
|
|
432
|
+
if (img) imgSrc = img.currentSrc || img.src;
|
|
433
|
+
else if (el.tagName === 'PICTURE') {
|
|
434
|
+
let i = el.querySelector('img'); if (i) imgSrc = i.currentSrc || i.src;
|
|
435
|
+
} else if (el.tagName === 'CANVAS') {
|
|
436
|
+
try { imgSrc = el.toDataURL('image/png'); } catch(_) {}
|
|
437
|
+
} else {
|
|
438
|
+
let bg = window.getComputedStyle(el).backgroundImage;
|
|
439
|
+
if (bg && bg.startsWith('url(')) imgSrc = bg.slice(4, -1).replace(/["']/g, "");
|
|
440
|
+
}
|
|
441
|
+
if (imgSrc && window.webkit?.messageHandlers?.bridge) {
|
|
442
|
+
window.webkit.messageHandlers.bridge.postMessage({cmd: 'context_target_image', src: imgSrc});
|
|
443
|
+
}
|
|
444
|
+
}, true);
|
|
445
|
+
})();
|
|
446
|
+
"""
|
|
447
|
+
|
|
448
|
+
KEYCHAIN_AUTOSAVE_JS = """
|
|
449
|
+
document.addEventListener('submit', (e) => {
|
|
450
|
+
if (!e.target || !e.target.querySelector) return;
|
|
451
|
+
let pw = e.target.querySelector('input[type="password"]');
|
|
452
|
+
let user = e.target.querySelector('input[type="text"], input[type="email"]');
|
|
453
|
+
if (pw && pw.value) {
|
|
454
|
+
window.webkit.messageHandlers.bridge.postMessage({
|
|
455
|
+
cmd: 'prompt_save_password', host: location.hostname,
|
|
456
|
+
username: user ? user.value : 'unknown', password: pw.value
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
}, true);
|
|
460
|
+
window.addEventListener('load', () => {
|
|
461
|
+
if (location.hostname && window.webkit?.messageHandlers?.bridge) {
|
|
462
|
+
window.webkit.messageHandlers.bridge.postMessage({cmd: 'check_autofill', host: location.hostname});
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
"""
|
|
466
|
+
|
|
467
|
+
COSMETIC_ADBLOCK_CSS = """
|
|
468
|
+
.ad, .ads, .advert, .advertising, .ad-banner, .ad-container,
|
|
469
|
+
.ad-slot, .ad-wrapper, .sponsored-post, [id^="google_ads_"],
|
|
470
|
+
[id^="div-gpt-ad"], ytd-ad-slot-renderer, .ytp-ad-module,
|
|
471
|
+
.ytp-ad-overlay-container, ytd-in-feed-ad-layout-renderer,
|
|
472
|
+
ytd-banner-promo-renderer, ytd-promoted-video-renderer,
|
|
473
|
+
ytd-ad-inline-playback-meta-block, #player-ads, #masthead-ad,
|
|
474
|
+
.taboola, .outbrain, .criteo-ad, [class*="sponsored"],
|
|
475
|
+
[data-ad-client], [data-ad-slot] {
|
|
476
|
+
display: none !important; visibility: hidden !important;
|
|
477
|
+
height: 0 !important; min-height: 0 !important; max-height: 0 !important;
|
|
478
|
+
pointer-events: none !important; opacity: 0 !important;
|
|
479
|
+
}
|
|
480
|
+
"""
|
|
481
|
+
|
|
482
|
+
YOUTUBE_AD_SHIELD_JS = """
|
|
483
|
+
(function() {
|
|
484
|
+
if (!location.hostname.includes('youtube.com')) return;
|
|
485
|
+
const skipAds = () => {
|
|
486
|
+
const skipBtn = document.querySelector('.ytp-skip-ad-button, .ytp-ad-skip-button, .ytp-ad-skip-button-modern');
|
|
487
|
+
if (skipBtn) skipBtn.click();
|
|
488
|
+
const adShowing = document.querySelector('.ad-showing, .ad-interrupting');
|
|
489
|
+
const video = document.querySelector('video');
|
|
490
|
+
if (adShowing && video && !isNaN(video.duration)) {
|
|
491
|
+
video.muted = true; video.playbackRate = 16.0; video.currentTime = video.duration - 0.1;
|
|
492
|
+
}
|
|
493
|
+
const overlay = document.querySelector('.ytp-ad-overlay-close-button');
|
|
494
|
+
if (overlay) overlay.click();
|
|
495
|
+
const dismissBtn = document.querySelector('ytd-enforcement-message-view-model #dismiss-button');
|
|
496
|
+
if (dismissBtn) dismissBtn.click();
|
|
497
|
+
};
|
|
498
|
+
setInterval(skipAds, 150);
|
|
499
|
+
})();
|
|
500
|
+
"""
|
|
501
|
+
|
|
502
|
+
IN_WINDOW_FULLSCREEN_JS = """
|
|
503
|
+
(function() {
|
|
504
|
+
let cur = null, origScrollY = 0;
|
|
505
|
+
function ensureStyle() {
|
|
506
|
+
if (!document.getElementById('__pybrowser_fs_style')) {
|
|
507
|
+
const st = document.createElement('style'); st.id = '__pybrowser_fs_style';
|
|
508
|
+
st.textContent = `.__in_window_fullscreen { position: fixed !important; top: 0 !important; left: 0 !important; width: 100vw !important; height: 100vh !important; max-width: 100vw !important; max-height: 100vh !important; margin: 0 !important; padding: 0 !important; border: 0 !important; z-index: 2147483647 !important; background: #000 !important; box-sizing: border-box !important; }`;
|
|
509
|
+
(document.head || document.documentElement).appendChild(st);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
Object.defineProperty(document, 'fullscreenElement', { get: () => cur, configurable: true });
|
|
513
|
+
Object.defineProperty(document, 'webkitFullscreenElement', { get: () => cur, configurable: true });
|
|
514
|
+
Object.defineProperty(document, 'fullscreenEnabled', { get: () => true, configurable: true });
|
|
515
|
+
Object.defineProperty(document, 'webkitFullscreenEnabled', { get: () => true, configurable: true });
|
|
516
|
+
|
|
517
|
+
Element.prototype.requestFullscreen = function() {
|
|
518
|
+
ensureStyle();
|
|
519
|
+
if (cur && cur !== this) cur.classList.remove('__in_window_fullscreen');
|
|
520
|
+
cur = this; origScrollY = window.scrollY || window.pageYOffset || 0;
|
|
521
|
+
this.classList.add('__in_window_fullscreen');
|
|
522
|
+
document.dispatchEvent(new Event('fullscreenchange', { bubbles: true }));
|
|
523
|
+
document.dispatchEvent(new Event('webkitfullscreenchange', { bubbles: true }));
|
|
524
|
+
window.dispatchEvent(new Event('resize'));
|
|
525
|
+
if (window.webkit?.messageHandlers?.bridge) window.webkit.messageHandlers.bridge.postMessage({cmd: 'html5_fullscreen_enter'});
|
|
526
|
+
return Promise.resolve();
|
|
527
|
+
};
|
|
528
|
+
Element.prototype.webkitRequestFullscreen = Element.prototype.requestFullscreen;
|
|
529
|
+
|
|
530
|
+
document.exitFullscreen = function() {
|
|
531
|
+
if (cur) {
|
|
532
|
+
const el = cur; cur = null;
|
|
533
|
+
el.classList.remove('__in_window_fullscreen');
|
|
534
|
+
document.dispatchEvent(new Event('fullscreenchange', { bubbles: true }));
|
|
535
|
+
document.dispatchEvent(new Event('webkitfullscreenchange', { bubbles: true }));
|
|
536
|
+
window.dispatchEvent(new Event('resize'));
|
|
537
|
+
window.scrollTo({ left: 0, top: origScrollY, behavior: 'instant' });
|
|
538
|
+
if (document.documentElement) document.documentElement.scrollLeft = 0;
|
|
539
|
+
if (document.body) document.body.scrollLeft = 0;
|
|
540
|
+
if (window.webkit?.messageHandlers?.bridge) window.webkit.messageHandlers.bridge.postMessage({cmd: 'html5_fullscreen_exit'});
|
|
541
|
+
}
|
|
542
|
+
return Promise.resolve();
|
|
543
|
+
};
|
|
544
|
+
document.webkitExitFullscreen = document.exitFullscreen;
|
|
545
|
+
document.addEventListener('keydown', function(e) {
|
|
546
|
+
if ((e.key === 'Escape' || e.keyCode === 27) && cur) { e.preventDefault(); e.stopPropagation(); document.exitFullscreen(); }
|
|
547
|
+
}, true);
|
|
548
|
+
})();
|
|
549
|
+
"""
|
|
550
|
+
|
|
551
|
+
REFLOW_RESTORE_JS = """
|
|
552
|
+
(function() {
|
|
553
|
+
if (document.documentElement) { document.documentElement.style.removeProperty('zoom'); document.documentElement.scrollLeft = 0; }
|
|
554
|
+
if (document.body) { document.body.style.removeProperty('zoom'); document.body.scrollLeft = 0; }
|
|
555
|
+
document.querySelectorAll('.__in_window_fullscreen').forEach(el => el.classList.remove('__in_window_fullscreen'));
|
|
556
|
+
if (window.scrollX !== 0) window.scrollTo(0, window.scrollY || 0);
|
|
557
|
+
window.dispatchEvent(new Event('resize'));
|
|
558
|
+
})();
|
|
559
|
+
"""
|
|
560
|
+
|
|
561
|
+
ESCAPE_CHECK_JS = """
|
|
562
|
+
(function() {
|
|
563
|
+
if (document.fullscreenElement) { if (document.exitFullscreen) document.exitFullscreen(); return true; }
|
|
564
|
+
const fs = document.querySelector('.__in_window_fullscreen');
|
|
565
|
+
if (fs) { fs.classList.remove('__in_window_fullscreen'); window.dispatchEvent(new Event('resize')); return true; }
|
|
566
|
+
return false;
|
|
567
|
+
})();
|
|
568
|
+
"""
|
|
569
|
+
|
|
570
|
+
# ============================================================================
|
|
571
|
+
# TAB GROUPS
|
|
572
|
+
# ============================================================================
|
|
573
|
+
@dataclass
|
|
574
|
+
class TabGroup:
|
|
575
|
+
id: str
|
|
576
|
+
title: str
|
|
577
|
+
color_name: str
|
|
578
|
+
collapsed: bool
|
|
579
|
+
|
|
580
|
+
class TabGroupHeaderView(Cocoa.NSView):
|
|
581
|
+
def initWithFrame_group_controller_(self, frame, group, controller):
|
|
582
|
+
self = objc.super(TabGroupHeaderView, self).initWithFrame_(frame)
|
|
583
|
+
if self:
|
|
584
|
+
self.group, self.controller = group, controller
|
|
585
|
+
return self
|
|
586
|
+
|
|
587
|
+
def drawRect_(self, dirtyRect):
|
|
588
|
+
bounds = self.bounds()
|
|
589
|
+
path = Cocoa.NSBezierPath.bezierPathWithRoundedRect_xRadius_yRadius_(Cocoa.NSMakeRect(2, 4, bounds.size.width - 4, 20), 4, 4)
|
|
590
|
+
hex_to_nscolor(GROUP_COLORS.get(self.group.color_name, "#5F6368")).setFill()
|
|
591
|
+
path.fill()
|
|
592
|
+
attrs = {Cocoa.NSFontAttributeName: Cocoa.NSFont.boldSystemFontOfSize_(11), Cocoa.NSForegroundColorAttributeName: Cocoa.NSColor.whiteColor()}
|
|
593
|
+
Cocoa.NSAttributedString.alloc().initWithString_attributes_(self.group.title, attrs).drawAtPoint_(Cocoa.NSMakePoint(6, 6))
|
|
594
|
+
|
|
595
|
+
def mouseDown_(self, event):
|
|
596
|
+
self.group.collapsed = not self.group.collapsed
|
|
597
|
+
self.controller.relayout_tabs()
|
|
598
|
+
|
|
599
|
+
# ============================================================================
|
|
600
|
+
# TITLE BAR VIEW (DEDICATED TOP BAR FOR DRAGGING, EXPANDING & ZEN TRAFFIC LIGHTS)
|
|
601
|
+
# ============================================================================
|
|
602
|
+
class TitleBarView(Cocoa.NSView):
|
|
603
|
+
"""
|
|
604
|
+
Dedicated compact title bar above tabs.
|
|
605
|
+
Hosts macOS traffic lights, provides window dragging, and supports double-click maximize.
|
|
606
|
+
Remains visible in Zen mode to keep traffic light buttons isolated from web content.
|
|
607
|
+
"""
|
|
608
|
+
def initWithFrame_controller_(self, frame, controller):
|
|
609
|
+
self = objc.super(TitleBarView, self).initWithFrame_(frame)
|
|
610
|
+
if self:
|
|
611
|
+
self.controller = controller
|
|
612
|
+
return self
|
|
613
|
+
|
|
614
|
+
def drawRect_(self, dirtyRect):
|
|
615
|
+
is_inc = self.controller.is_incognito if self.controller else False
|
|
616
|
+
get_m3_color("tabstrip-bg", self, is_inc).setFill()
|
|
617
|
+
Cocoa.NSRectFill(self.bounds())
|
|
618
|
+
|
|
619
|
+
def mouseDown_(self, event):
|
|
620
|
+
if event.clickCount() == 2 and self.controller:
|
|
621
|
+
action = SETTINGS.settings.get("tab_bar_double_click_action", "maximize")
|
|
622
|
+
win = self.controller.window()
|
|
623
|
+
if win:
|
|
624
|
+
if action == "fullscreen":
|
|
625
|
+
win.toggleFullScreen_(None)
|
|
626
|
+
else:
|
|
627
|
+
win.zoom_(None)
|
|
628
|
+
return
|
|
629
|
+
if self.window():
|
|
630
|
+
self.window().performWindowDragWithEvent_(event)
|
|
631
|
+
else:
|
|
632
|
+
objc.super(TitleBarView, self).mouseDown_(event)
|
|
633
|
+
|
|
634
|
+
# ============================================================================
|
|
635
|
+
# MATERIAL 3 CHROME VIEWS & DELEGATES
|
|
636
|
+
# ============================================================================
|
|
637
|
+
class ChromeTabStripView(Cocoa.NSView):
|
|
638
|
+
def initWithFrame_(self, frame):
|
|
639
|
+
self = objc.super(ChromeTabStripView, self).initWithFrame_(frame)
|
|
640
|
+
if self:
|
|
641
|
+
self.controller = None
|
|
642
|
+
return self
|
|
643
|
+
|
|
644
|
+
def drawRect_(self, dirtyRect):
|
|
645
|
+
is_inc = self.controller.is_incognito if self.controller else False
|
|
646
|
+
get_m3_color("tabstrip-bg", self, is_inc).setFill()
|
|
647
|
+
Cocoa.NSRectFill(self.bounds())
|
|
648
|
+
|
|
649
|
+
def mouseDown_(self, event):
|
|
650
|
+
if event.clickCount() == 2 and self.controller:
|
|
651
|
+
action = SETTINGS.settings.get("tab_bar_double_click_action", "maximize")
|
|
652
|
+
win = self.controller.window()
|
|
653
|
+
if win:
|
|
654
|
+
if action == "fullscreen":
|
|
655
|
+
win.toggleFullScreen_(None)
|
|
656
|
+
else:
|
|
657
|
+
win.zoom_(None)
|
|
658
|
+
return
|
|
659
|
+
if self.window():
|
|
660
|
+
self.window().performWindowDragWithEvent_(event)
|
|
661
|
+
else:
|
|
662
|
+
objc.super(ChromeTabStripView, self).mouseDown_(event)
|
|
663
|
+
|
|
664
|
+
class ChromeTabView(Cocoa.NSView):
|
|
665
|
+
def initWithFrame_controller_index_(self, frame, controller, index):
|
|
666
|
+
self = objc.super(ChromeTabView, self).initWithFrame_(frame)
|
|
667
|
+
if self:
|
|
668
|
+
self.controller = controller
|
|
669
|
+
self.index = index
|
|
670
|
+
self.is_hovered = False
|
|
671
|
+
self.tracking_area = None
|
|
672
|
+
self.close_btn_rect = Cocoa.NSZeroRect
|
|
673
|
+
self.audio_btn_rect = Cocoa.NSZeroRect
|
|
674
|
+
return self
|
|
675
|
+
|
|
676
|
+
def updateTrackingAreas(self):
|
|
677
|
+
if self.tracking_area:
|
|
678
|
+
self.removeTrackingArea_(self.tracking_area)
|
|
679
|
+
opts = Cocoa.NSTrackingMouseEnteredAndExited | Cocoa.NSTrackingActiveAlways | Cocoa.NSTrackingInVisibleRect
|
|
680
|
+
self.tracking_area = Cocoa.NSTrackingArea.alloc().initWithRect_options_owner_userInfo_(Cocoa.NSZeroRect, opts, self, None)
|
|
681
|
+
self.addTrackingArea_(self.tracking_area)
|
|
682
|
+
objc.super(ChromeTabView, self).updateTrackingAreas()
|
|
683
|
+
|
|
684
|
+
def mouseEntered_(self, event):
|
|
685
|
+
self.is_hovered = True
|
|
686
|
+
self.setNeedsDisplay_(True)
|
|
687
|
+
|
|
688
|
+
def mouseExited_(self, event):
|
|
689
|
+
self.is_hovered = False
|
|
690
|
+
self.setNeedsDisplay_(True)
|
|
691
|
+
|
|
692
|
+
def drawRect_(self, dirtyRect):
|
|
693
|
+
if not (0 <= self.index < len(self.controller.tabs)):
|
|
694
|
+
return
|
|
695
|
+
tab = self.controller.tabs[self.index]
|
|
696
|
+
is_active = (self.index == self.controller.active_tab_index)
|
|
697
|
+
is_inc = self.controller.is_incognito
|
|
698
|
+
bounds = self.bounds()
|
|
699
|
+
|
|
700
|
+
if is_active:
|
|
701
|
+
path = Cocoa.NSBezierPath.bezierPathWithRoundedRect_xRadius_yRadius_(bounds, 6, 6)
|
|
702
|
+
get_m3_color("active-tab-bg", self, is_inc).setFill()
|
|
703
|
+
path.fill()
|
|
704
|
+
elif self.is_hovered:
|
|
705
|
+
path = Cocoa.NSBezierPath.bezierPathWithRoundedRect_xRadius_yRadius_(bounds, 6, 6)
|
|
706
|
+
get_m3_color("inactive-tab-hover", self, is_inc).setFill()
|
|
707
|
+
path.fill()
|
|
708
|
+
|
|
709
|
+
# Separator for inactive tabs
|
|
710
|
+
if not is_active and not self.is_hovered and bounds.size.width > 50:
|
|
711
|
+
sep = Cocoa.NSBezierPath.bezierPathWithRect_(Cocoa.NSMakeRect(bounds.size.width - 1, 6, 1, bounds.size.height - 12))
|
|
712
|
+
get_m3_color("tab-divider", self, is_inc).setFill()
|
|
713
|
+
sep.fill()
|
|
714
|
+
|
|
715
|
+
# Favicon
|
|
716
|
+
icon_x = 8.0
|
|
717
|
+
icon_y = (bounds.size.height - 16) / 2.0
|
|
718
|
+
if tab.get("favicon"):
|
|
719
|
+
tab["favicon"].drawInRect_fromRect_operation_fraction_(
|
|
720
|
+
Cocoa.NSMakeRect(icon_x, icon_y, 16, 16), Cocoa.NSZeroRect, Cocoa.NSCompositeSourceOver, 1.0
|
|
721
|
+
)
|
|
722
|
+
else:
|
|
723
|
+
def_icon = "💤" if tab.get("is_sleeping") else "📄"
|
|
724
|
+
attrs = {Cocoa.NSFontAttributeName: Cocoa.NSFont.systemFontOfSize_(11)}
|
|
725
|
+
Cocoa.NSAttributedString.alloc().initWithString_attributes_(def_icon, attrs).drawAtPoint_(Cocoa.NSMakePoint(icon_x, icon_y - 1))
|
|
726
|
+
|
|
727
|
+
if tab.get("pinned", False):
|
|
728
|
+
return
|
|
729
|
+
|
|
730
|
+
# Audio indicator
|
|
731
|
+
has_audio = tab.get("has_audio", False)
|
|
732
|
+
is_muted = tab.get("is_muted", False)
|
|
733
|
+
right_offset = 24.0
|
|
734
|
+
|
|
735
|
+
if has_audio or is_muted:
|
|
736
|
+
audio_str = "🔇" if is_muted else "🔊"
|
|
737
|
+
self.audio_btn_rect = Cocoa.NSMakeRect(bounds.size.width - 40, (bounds.size.height - 16) / 2.0, 16, 16)
|
|
738
|
+
attrs = {Cocoa.NSFontAttributeName: Cocoa.NSFont.systemFontOfSize_(10)}
|
|
739
|
+
Cocoa.NSAttributedString.alloc().initWithString_attributes_(audio_str, attrs).drawAtPoint_(Cocoa.NSMakePoint(self.audio_btn_rect.origin.x, self.audio_btn_rect.origin.y - 1))
|
|
740
|
+
right_offset = 42.0
|
|
741
|
+
else:
|
|
742
|
+
self.audio_btn_rect = Cocoa.NSZeroRect
|
|
743
|
+
|
|
744
|
+
# Close button
|
|
745
|
+
self.close_btn_rect = Cocoa.NSMakeRect(bounds.size.width - 20, (bounds.size.height - 14) / 2.0, 14, 14)
|
|
746
|
+
if self.is_hovered or is_active:
|
|
747
|
+
close_attrs = {
|
|
748
|
+
Cocoa.NSFontAttributeName: Cocoa.NSFont.boldSystemFontOfSize_(11),
|
|
749
|
+
Cocoa.NSForegroundColorAttributeName: Cocoa.NSColor.secondaryLabelColor()
|
|
750
|
+
}
|
|
751
|
+
Cocoa.NSAttributedString.alloc().initWithString_attributes_("✕", close_attrs).drawAtPoint_(Cocoa.NSMakePoint(self.close_btn_rect.origin.x + 2, self.close_btn_rect.origin.y - 1))
|
|
752
|
+
|
|
753
|
+
# Tab title
|
|
754
|
+
title_x = icon_x + 20.0
|
|
755
|
+
max_title_w = max(10.0, bounds.size.width - title_x - right_offset)
|
|
756
|
+
title = tab.get("title", "New Tab")
|
|
757
|
+
text_color = Cocoa.NSColor.labelColor() if is_active else Cocoa.NSColor.secondaryLabelColor()
|
|
758
|
+
title_attrs = {
|
|
759
|
+
Cocoa.NSFontAttributeName: Cocoa.NSFont.systemFontOfSize_(12),
|
|
760
|
+
Cocoa.NSForegroundColorAttributeName: text_color
|
|
761
|
+
}
|
|
762
|
+
astr = Cocoa.NSAttributedString.alloc().initWithString_attributes_(title, title_attrs)
|
|
763
|
+
astr.drawInRect_(Cocoa.NSMakeRect(title_x, (bounds.size.height - 16) / 2.0, max_title_w, 16))
|
|
764
|
+
|
|
765
|
+
def mouseDown_(self, event):
|
|
766
|
+
pt = self.convertPoint_fromView_(event.locationInWindow(), None)
|
|
767
|
+
if Cocoa.NSPointInRect(pt, self.close_btn_rect):
|
|
768
|
+
self.controller.close_tab(self.index)
|
|
769
|
+
return
|
|
770
|
+
if self.audio_btn_rect.size.width > 0 and Cocoa.NSPointInRect(pt, self.audio_btn_rect):
|
|
771
|
+
self.controller.toggle_mute(self.index)
|
|
772
|
+
return
|
|
773
|
+
self.controller.switch_to_tab(self.index)
|
|
774
|
+
|
|
775
|
+
def menuForEvent_(self, event):
|
|
776
|
+
menu = Cocoa.NSMenu.alloc().init()
|
|
777
|
+
item_dupe = menu.addItemWithTitle_action_keyEquivalent_("Duplicate Tab", "onContextDuplicate:", "")
|
|
778
|
+
item_dupe.setTarget_(self)
|
|
779
|
+
item_pin = menu.addItemWithTitle_action_keyEquivalent_("Unpin Tab" if self.controller.tabs[self.index].get("pinned") else "Pin Tab", "onContextPin:", "")
|
|
780
|
+
item_pin.setTarget_(self)
|
|
781
|
+
item_mute = menu.addItemWithTitle_action_keyEquivalent_("Unmute Tab" if self.controller.tabs[self.index].get("is_muted") else "Mute Tab", "onContextMute:", "")
|
|
782
|
+
item_mute.setTarget_(self)
|
|
783
|
+
item_grp = menu.addItemWithTitle_action_keyEquivalent_("Add Tab to New Group", "onContextGroup:", "")
|
|
784
|
+
item_grp.setTarget_(self)
|
|
785
|
+
menu.addItem_(Cocoa.NSMenuItem.separatorItem())
|
|
786
|
+
item_close = menu.addItemWithTitle_action_keyEquivalent_("Close Tab", "onContextClose:", "")
|
|
787
|
+
item_close.setTarget_(self)
|
|
788
|
+
return menu
|
|
789
|
+
|
|
790
|
+
def onContextDuplicate_(self, sender): self.controller.duplicate_tab(self.index)
|
|
791
|
+
def onContextPin_(self, sender): self.controller.toggle_pin_tab(self.index)
|
|
792
|
+
def onContextMute_(self, sender): self.controller.toggle_mute(self.index)
|
|
793
|
+
def onContextGroup_(self, sender): self.controller.create_group_for_tab(self.index)
|
|
794
|
+
def onContextClose_(self, sender): self.controller.close_tab(self.index)
|
|
795
|
+
|
|
796
|
+
class ChromeToolbarView(Cocoa.NSView):
|
|
797
|
+
def initWithFrame_(self, frame):
|
|
798
|
+
self = objc.super(ChromeToolbarView, self).initWithFrame_(frame)
|
|
799
|
+
if self:
|
|
800
|
+
self.controller = None
|
|
801
|
+
return self
|
|
802
|
+
|
|
803
|
+
def drawRect_(self, dirtyRect):
|
|
804
|
+
is_inc = self.controller.is_incognito if self.controller else False
|
|
805
|
+
get_m3_color("toolbar-bg", self, is_inc).setFill()
|
|
806
|
+
Cocoa.NSRectFill(self.bounds())
|
|
807
|
+
sep = Cocoa.NSBezierPath.bezierPathWithRect_(Cocoa.NSMakeRect(0, 0, self.bounds().size.width, 1))
|
|
808
|
+
get_m3_color("toolbar-separator", self, is_inc).setFill()
|
|
809
|
+
sep.fill()
|
|
810
|
+
|
|
811
|
+
class BookmarksBarView(Cocoa.NSView):
|
|
812
|
+
def initWithFrame_controller_(self, frame, controller):
|
|
813
|
+
self = objc.super(BookmarksBarView, self).initWithFrame_(frame)
|
|
814
|
+
if self:
|
|
815
|
+
self.controller = controller
|
|
816
|
+
self.reload_bookmarks()
|
|
817
|
+
return self
|
|
818
|
+
|
|
819
|
+
def drawRect_(self, dirtyRect):
|
|
820
|
+
is_inc = self.controller.is_incognito if self.controller else False
|
|
821
|
+
get_m3_color("toolbar-bg", self, is_inc).setFill()
|
|
822
|
+
Cocoa.NSRectFill(self.bounds())
|
|
823
|
+
sep = Cocoa.NSBezierPath.bezierPathWithRect_(Cocoa.NSMakeRect(0, 0, self.bounds().size.width, 0.5))
|
|
824
|
+
get_m3_color("toolbar-separator", self, is_inc).setFill()
|
|
825
|
+
sep.fill()
|
|
826
|
+
|
|
827
|
+
def reload_bookmarks(self):
|
|
828
|
+
for sub in list(self.subviews()):
|
|
829
|
+
sub.removeFromSuperview()
|
|
830
|
+
bms = []
|
|
831
|
+
if os.path.exists(BOOKMARKS_FILE):
|
|
832
|
+
try:
|
|
833
|
+
with open(BOOKMARKS_FILE, "r", encoding="utf-8") as f:
|
|
834
|
+
bms = json.load(f)
|
|
835
|
+
except Exception:
|
|
836
|
+
bms = []
|
|
837
|
+
|
|
838
|
+
x = 8.0
|
|
839
|
+
h = self.bounds().size.height
|
|
840
|
+
for b in bms:
|
|
841
|
+
if not isinstance(b, dict) or not b.get("url"):
|
|
842
|
+
continue
|
|
843
|
+
title = b.get("title", b["url"])[:24]
|
|
844
|
+
# Fixed: Removed hardcoded default 🔖 favicon prefix from bookmarks
|
|
845
|
+
btn_w = max(40, min(140, len(title) * 7.5 + 14))
|
|
846
|
+
btn = Cocoa.NSButton.alloc().initWithFrame_(Cocoa.NSMakeRect(x, 2, btn_w, h - 4))
|
|
847
|
+
btn.setTitle_(title)
|
|
848
|
+
btn.setBezelStyle_(Cocoa.NSBezelStyleInline)
|
|
849
|
+
btn.setFont_(Cocoa.NSFont.systemFontOfSize_(11))
|
|
850
|
+
btn.setTarget_(self)
|
|
851
|
+
btn.setAction_("onBookmarkClick:")
|
|
852
|
+
btn.setToolTip_(b.get("url", ""))
|
|
853
|
+
self.addSubview_(btn)
|
|
854
|
+
x += btn_w + 4.0
|
|
855
|
+
self.setNeedsDisplay_(True)
|
|
856
|
+
|
|
857
|
+
def onBookmarkClick_(self, sender):
|
|
858
|
+
url = sender.toolTip()
|
|
859
|
+
if url and self.controller:
|
|
860
|
+
wv = self.controller.get_active_webview()
|
|
861
|
+
if wv:
|
|
862
|
+
wv.loadRequest_(Foundation.NSURLRequest.requestWithURL_(Foundation.NSURL.URLWithString_(url)))
|
|
863
|
+
|
|
864
|
+
class DownloadDelegate(Cocoa.NSObject):
|
|
865
|
+
def initWithController_(self, controller):
|
|
866
|
+
self = objc.super(DownloadDelegate, self).init()
|
|
867
|
+
if self:
|
|
868
|
+
self.controller = controller
|
|
869
|
+
self.dest_paths = {}
|
|
870
|
+
return self
|
|
871
|
+
|
|
872
|
+
def download_decideDestinationUsingResponse_suggestedFilename_completionHandler_(self, download, response, filename, handler):
|
|
873
|
+
dest_dir = validate_and_get_download_dir()
|
|
874
|
+
base_name = filename or "download"
|
|
875
|
+
dest_path = os.path.join(dest_dir, base_name)
|
|
876
|
+
base, ext = os.path.splitext(dest_path)
|
|
877
|
+
counter = 1
|
|
878
|
+
while os.path.exists(dest_path):
|
|
879
|
+
dest_path = f"{base} ({counter}){ext}"
|
|
880
|
+
counter += 1
|
|
881
|
+
|
|
882
|
+
self.dest_paths[download] = dest_path
|
|
883
|
+
self.controller.register_download(os.path.basename(dest_path), dest_path, download)
|
|
884
|
+
|
|
885
|
+
if hasattr(download, "progress") and download.progress():
|
|
886
|
+
try:
|
|
887
|
+
download.progress().addObserver_forKeyPath_options_context_(self.controller, "fractionCompleted", Cocoa.NSKeyValueObservingOptionNew, None)
|
|
888
|
+
except Exception:
|
|
889
|
+
pass
|
|
890
|
+
|
|
891
|
+
handler(Foundation.NSURL.fileURLWithPath_(dest_path))
|
|
892
|
+
|
|
893
|
+
def downloadDidFinish_(self, download):
|
|
894
|
+
if hasattr(download, "progress") and download.progress():
|
|
895
|
+
try:
|
|
896
|
+
download.progress().removeObserver_forKeyPath_(self.controller, "fractionCompleted")
|
|
897
|
+
except Exception:
|
|
898
|
+
pass
|
|
899
|
+
path = self.dest_paths.pop(download, None)
|
|
900
|
+
self.controller.finish_download(download, "Completed", dl_path=path)
|
|
901
|
+
|
|
902
|
+
def download_didFailWithError_resumeData_(self, download, error, resumeData):
|
|
903
|
+
if hasattr(download, "progress") and download.progress():
|
|
904
|
+
try:
|
|
905
|
+
download.progress().removeObserver_forKeyPath_(self.controller, "fractionCompleted")
|
|
906
|
+
except Exception:
|
|
907
|
+
pass
|
|
908
|
+
path = self.dest_paths.pop(download, None)
|
|
909
|
+
self.controller.finish_download(download, "Failed", dl_path=path)
|
|
910
|
+
|
|
911
|
+
# ============================================================================
|
|
912
|
+
# SCHEME HANDLER
|
|
913
|
+
# ============================================================================
|
|
914
|
+
class InternalSchemeHandler(Cocoa.NSObject):
|
|
915
|
+
def init(self):
|
|
916
|
+
self = objc.super(InternalSchemeHandler, self).init()
|
|
917
|
+
if self:
|
|
918
|
+
self.active_tasks, self.lock = set(), threading.Lock()
|
|
919
|
+
return self
|
|
920
|
+
|
|
921
|
+
def webView_startURLSchemeTask_(self, webView, task):
|
|
922
|
+
with self.lock:
|
|
923
|
+
self.active_tasks.add(task)
|
|
924
|
+
try:
|
|
925
|
+
url = task.request().URL().absoluteString()
|
|
926
|
+
if "settings" in url:
|
|
927
|
+
with ADBLOCK_LOCK:
|
|
928
|
+
sb = len(STEVENBLACK_DOMAINS)
|
|
929
|
+
base = len(BASE_ADBLOCK_DOMAINS)
|
|
930
|
+
cust = len(SETTINGS.get_custom_adblock_domains())
|
|
931
|
+
act = sb + base + cust
|
|
932
|
+
html_str = get_settings_html(sb, base, cust, act)
|
|
933
|
+
elif "bookmarks" in url:
|
|
934
|
+
html_str = self.get_bookmarks_html()
|
|
935
|
+
elif "downloads" in url:
|
|
936
|
+
html_str = self.get_downloads_html()
|
|
937
|
+
elif "extensions" in url:
|
|
938
|
+
html_str = self.get_extensions_html()
|
|
939
|
+
elif "terminal" in url:
|
|
940
|
+
html_str = self.get_terminal_html()
|
|
941
|
+
elif "processes" in url:
|
|
942
|
+
dispatch_to_main(lambda: Cocoa.NSApp.delegate().process_manager.show())
|
|
943
|
+
html_str = "<!DOCTYPE html><html><body style='font-family:-apple-system,sans-serif;padding:40px;background:#FAF9F6;color:#222;'><h2>Task Manager launched in a separate window.</h2></body></html>"
|
|
944
|
+
else:
|
|
945
|
+
html_str = self.get_history_html()
|
|
946
|
+
|
|
947
|
+
data = html_str.encode('utf-8')
|
|
948
|
+
resp = Foundation.NSURLResponse.alloc().initWithURL_MIMEType_expectedContentLength_textEncodingName_(
|
|
949
|
+
task.request().URL(), "text/html", len(data), "utf-8"
|
|
950
|
+
)
|
|
951
|
+
with self.lock:
|
|
952
|
+
if task not in self.active_tasks:
|
|
953
|
+
return
|
|
954
|
+
task.didReceiveResponse_(resp)
|
|
955
|
+
task.didReceiveData_(Foundation.NSData.dataWithBytes_length_(data, len(data)))
|
|
956
|
+
task.didFinish()
|
|
957
|
+
except Exception:
|
|
958
|
+
try:
|
|
959
|
+
task.didFailWithError_(Foundation.NSError.errorWithDomain_code_userInfo_("PyBrowserError", -1, None))
|
|
960
|
+
except Exception:
|
|
961
|
+
pass
|
|
962
|
+
finally:
|
|
963
|
+
with self.lock:
|
|
964
|
+
self.active_tasks.discard(task)
|
|
965
|
+
|
|
966
|
+
def webView_stopURLSchemeTask_(self, webView, task):
|
|
967
|
+
with self.lock:
|
|
968
|
+
self.active_tasks.discard(task)
|
|
969
|
+
|
|
970
|
+
def get_terminal_html(self):
|
|
971
|
+
# Fullscreen responsive xterm with ResizeObserver to ensure vim/less utilize entire viewport
|
|
972
|
+
bg = "#1e1e2e"
|
|
973
|
+
fg = "#c792ea"
|
|
974
|
+
return f"""<!DOCTYPE html><html><head><meta charset='utf-8'>
|
|
975
|
+
<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css' />
|
|
976
|
+
<script src='https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js'></script>
|
|
977
|
+
<script src='https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js'></script>
|
|
978
|
+
<style>
|
|
979
|
+
*{{box-sizing:border-box;margin:0;padding:0}}
|
|
980
|
+
html,body{{width:100vw;height:100vh;background:{bg};overflow:hidden;padding:0;margin:0}}
|
|
981
|
+
#t{{width:100%;height:100%;background:{bg};overflow:hidden}}
|
|
982
|
+
.xterm-viewport{{overflow-y:auto !important}}
|
|
983
|
+
</style></head><body><div id='t'></div><script>
|
|
984
|
+
const term = new Terminal({{
|
|
985
|
+
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
|
|
986
|
+
fontSize: 13,
|
|
987
|
+
lineHeight: 1.15,
|
|
988
|
+
cursorBlink: true,
|
|
989
|
+
theme: {{ background: '{bg}', foreground: '{fg}', cursor: '#A8C7FA' }}
|
|
990
|
+
}});
|
|
991
|
+
const fit = new FitAddon.FitAddon();
|
|
992
|
+
term.loadAddon(fit);
|
|
993
|
+
term.open(document.getElementById('t'));
|
|
994
|
+
|
|
995
|
+
const postResize = () => {{
|
|
996
|
+
try {{
|
|
997
|
+
fit.fit();
|
|
998
|
+
if (term.cols > 0 && term.rows > 0 && window.webkit?.messageHandlers?.terminalResize) {{
|
|
999
|
+
window.webkit.messageHandlers.terminalResize.postMessage({{ cols: term.cols, rows: term.rows }});
|
|
1000
|
+
}}
|
|
1001
|
+
}} catch(e) {{}}
|
|
1002
|
+
}};
|
|
1003
|
+
|
|
1004
|
+
const ro = new ResizeObserver(() => {{
|
|
1005
|
+
postResize();
|
|
1006
|
+
}});
|
|
1007
|
+
ro.observe(document.getElementById('t'));
|
|
1008
|
+
|
|
1009
|
+
setTimeout(postResize, 30);
|
|
1010
|
+
setTimeout(postResize, 100);
|
|
1011
|
+
setTimeout(postResize, 350);
|
|
1012
|
+
window.addEventListener('resize', postResize);
|
|
1013
|
+
|
|
1014
|
+
term.onData(d => {{
|
|
1015
|
+
if (window.webkit?.messageHandlers?.terminalInput) {{
|
|
1016
|
+
window.webkit.messageHandlers.terminalInput.postMessage(d);
|
|
1017
|
+
}}
|
|
1018
|
+
}});
|
|
1019
|
+
|
|
1020
|
+
window.writeToTerm = b => {{
|
|
1021
|
+
const s = atob(b), u = new Uint8Array(s.length);
|
|
1022
|
+
for (let i = 0; i < s.length; i++) u[i] = s.charCodeAt(i);
|
|
1023
|
+
term.write(new TextDecoder().decode(u));
|
|
1024
|
+
}};
|
|
1025
|
+
</script></body></html>"""
|
|
1026
|
+
|
|
1027
|
+
def get_bookmarks_html(self):
|
|
1028
|
+
bms = []
|
|
1029
|
+
if os.path.exists(BOOKMARKS_FILE):
|
|
1030
|
+
try:
|
|
1031
|
+
with open(BOOKMARKS_FILE, "r", encoding="utf-8") as f:
|
|
1032
|
+
bms = json.load(f)
|
|
1033
|
+
except Exception:
|
|
1034
|
+
pass
|
|
1035
|
+
items = []
|
|
1036
|
+
for b in bms:
|
|
1037
|
+
if isinstance(b, dict) and b.get("url"):
|
|
1038
|
+
u = b["url"]
|
|
1039
|
+
t = b.get("title", u)
|
|
1040
|
+
safe_u = json.dumps(u)
|
|
1041
|
+
del_call = f"window.webkit.messageHandlers.bridge.postMessage({{cmd: 'delete_bookmark', url: {safe_u}}}); setTimeout(() => location.reload(), 100);"
|
|
1042
|
+
items.append(f"<li><a href='{html.escape(u)}'>{html.escape(t)}</a> <button onclick='{del_call}'>Delete</button></li>")
|
|
1043
|
+
rows = "".join(items)
|
|
1044
|
+
return f"<!DOCTYPE html><html><body style='font-family:-apple-system,sans-serif;padding:30px;'><h2>Bookmarks</h2><div style='margin-bottom:15px;'><input id='t' placeholder='Title'><input id='u' placeholder='https://...'><button onclick='window.webkit.messageHandlers.bridge.postMessage({{cmd:\"add_bookmark\",title:document.getElementById(\"t\").value,url:document.getElementById(\"u\").value}});setTimeout(()=>location.reload(),100)'>Add</button></div><ul>{rows or '<li>No bookmarks</li>'}</ul></body></html>"
|
|
1045
|
+
|
|
1046
|
+
def get_history_html(self):
|
|
1047
|
+
records = query_history()
|
|
1048
|
+
items = []
|
|
1049
|
+
for r in records:
|
|
1050
|
+
del_call = f"window.webkit.messageHandlers.bridge.postMessage({{cmd: 'delete_history_item', id: {r[0]}}}); this.closest('tr').remove();"
|
|
1051
|
+
items.append(f"<tr><td>{r[3]}</td><td><a href='{html.escape(r[1])}'>{html.escape(r[2])}</a></td><td><button onclick='{del_call}'>✕</button></td></tr>")
|
|
1052
|
+
rows = "".join(items)
|
|
1053
|
+
return f"<!DOCTYPE html><html><body style='font-family:-apple-system,sans-serif;padding:30px;'><h2>History</h2><button onclick=\"window.webkit.messageHandlers.bridge.postMessage({{cmd:'clear_browsing_data',targets:['history']}});setTimeout(()=>location.reload(),100)\">Clear All</button><table style='width:100%;margin-top:15px;'>{rows or '<tr><td>No history</td></tr>'}</table></body></html>"
|
|
1054
|
+
|
|
1055
|
+
def get_downloads_html(self):
|
|
1056
|
+
return "<!DOCTYPE html><html><body style='font-family:-apple-system,sans-serif;padding:30px;'><h2>Downloads</h2><div id='dl-list'>No active downloads.</div><script>const dls={};window.updateDownload=(id,name,path,status,progress)=>{dls[id]={name,path,status,progress};render()};function render(){const el=document.getElementById('dl-list');el.innerHTML=Object.values(dls).map(d=>`<div style='border:1px solid #ccc;padding:10px;margin-bottom:8px;'><b>${d.name}</b> (${d.status}) <button onclick=\"window.webkit.messageHandlers.bridge.postMessage({cmd:'dl_show',path:'${d.path}'})\">Show in Finder</button></div>`).join('')};window.webkit.messageHandlers.bridge.postMessage({cmd:'dl_request_sync'});</script></body></html>"
|
|
1057
|
+
|
|
1058
|
+
def get_extensions_html(self):
|
|
1059
|
+
scripts = get_installed_userscripts()
|
|
1060
|
+
items = []
|
|
1061
|
+
for s in scripts:
|
|
1062
|
+
f = s["file"]
|
|
1063
|
+
safe_f = json.dumps(f)
|
|
1064
|
+
checked = "checked" if s["enabled"] else ""
|
|
1065
|
+
toggle_call = f"window.webkit.messageHandlers.bridge.postMessage({{cmd: 'toggle_script', file: {safe_f}, enabled: this.checked}})"
|
|
1066
|
+
del_call = f"if(confirm('Delete?')){{window.webkit.messageHandlers.bridge.postMessage({{cmd: 'delete_script', file: {safe_f}}}); setTimeout(() => location.reload(), 100);}}"
|
|
1067
|
+
items.append(
|
|
1068
|
+
f"<div style='border:1px solid #ccc;padding:10px;margin-bottom:8px;'>"
|
|
1069
|
+
f"<b>{html.escape(s['name'])}</b> ({html.escape(f)}) "
|
|
1070
|
+
f"<label><input type='checkbox' {checked} onchange='{toggle_call}'> Enabled</label> "
|
|
1071
|
+
f"<button onclick=\"{del_call}\">Delete</button>"
|
|
1072
|
+
f"</div>"
|
|
1073
|
+
)
|
|
1074
|
+
rows = "".join(items)
|
|
1075
|
+
return f"<!DOCTYPE html><html><body style='font-family:-apple-system,sans-serif;padding:30px;'><h2>Extensions & Userscripts</h2><button onclick='let id=prompt(\"CRX ID or URL:\");if(id)window.webkit.messageHandlers.bridge.postMessage({{cmd:\"install_crx\",id:id}})'>Install CRX</button><div style='margin-top:15px;'>{rows or 'No userscripts installed.'}</div></body></html>"
|
|
1076
|
+
|
|
1077
|
+
# ============================================================================
|
|
1078
|
+
# WEBKIT SUBCLASS & IMAGE CONTEXT MENU
|
|
1079
|
+
# ============================================================================
|
|
1080
|
+
class BrowserWebView(WebKit.WKWebView):
|
|
1081
|
+
def willOpenMenu_withEvent_(self, menu, event):
|
|
1082
|
+
self.open_image_item = None
|
|
1083
|
+
download_item = None
|
|
1084
|
+
|
|
1085
|
+
for item in menu.itemArray():
|
|
1086
|
+
ident = str(item.identifier()) if item.identifier() else ""
|
|
1087
|
+
title = str(item.title()).lower() if item.title() else ""
|
|
1088
|
+
if "openimageinnewwindow" in ident.lower() or "open image in new" in title:
|
|
1089
|
+
self.open_image_item = item
|
|
1090
|
+
item.setTitle_("Open Image in New Tab")
|
|
1091
|
+
elif "downloadimage" in ident.lower() or "download image" in title:
|
|
1092
|
+
download_item = item
|
|
1093
|
+
|
|
1094
|
+
if self.open_image_item:
|
|
1095
|
+
if download_item:
|
|
1096
|
+
download_item.setTarget_(self)
|
|
1097
|
+
download_item.setAction_("onContextDownloadImage:")
|
|
1098
|
+
else:
|
|
1099
|
+
dl = Cocoa.NSMenuItem.alloc().initWithTitle_action_keyEquivalent_("Download Image", "onContextDownloadImage:", "")
|
|
1100
|
+
dl.setTarget_(self)
|
|
1101
|
+
idx = menu.indexOfItem_(self.open_image_item)
|
|
1102
|
+
menu.insertItem_atIndex_(dl, idx + 1)
|
|
1103
|
+
download_item = dl
|
|
1104
|
+
|
|
1105
|
+
save_as = Cocoa.NSMenuItem.alloc().initWithTitle_action_keyEquivalent_("Save Image As…", "onContextSaveImageAs:", "")
|
|
1106
|
+
save_as.setTarget_(self)
|
|
1107
|
+
idx = menu.indexOfItem_(download_item)
|
|
1108
|
+
menu.insertItem_atIndex_(save_as, idx + 1)
|
|
1109
|
+
|
|
1110
|
+
if not any("copy image address" in str(it.title()).lower() for it in menu.itemArray()):
|
|
1111
|
+
copy_addr = Cocoa.NSMenuItem.alloc().initWithTitle_action_keyEquivalent_("Copy Image Address", "onContextCopyImageAddress:", "")
|
|
1112
|
+
copy_addr.setTarget_(self)
|
|
1113
|
+
menu.insertItem_atIndex_(copy_addr, idx + 2)
|
|
1114
|
+
else:
|
|
1115
|
+
controller = getattr(self, "controller", None)
|
|
1116
|
+
target_img_url = getattr(controller, "last_context_image_url", None) if controller else None
|
|
1117
|
+
if target_img_url:
|
|
1118
|
+
menu.addItem_(Cocoa.NSMenuItem.separatorItem())
|
|
1119
|
+
menu.addItemWithTitle_action_keyEquivalent_("Download Image", "onDirectDownloadImage:", "").setTarget_(self)
|
|
1120
|
+
menu.addItemWithTitle_action_keyEquivalent_("Save Image As…", "onDirectSaveImageAs:", "").setTarget_(self)
|
|
1121
|
+
menu.addItemWithTitle_action_keyEquivalent_("Copy Image Address", "onDirectCopyImageAddress:", "").setTarget_(self)
|
|
1122
|
+
|
|
1123
|
+
def onContextDownloadImage_(self, sender):
|
|
1124
|
+
if self.open_image_item:
|
|
1125
|
+
self.pending_image_action = "download"
|
|
1126
|
+
Cocoa.NSApp.sendAction_to_from_(self.open_image_item.action(), self.open_image_item.target(), self.open_image_item)
|
|
1127
|
+
|
|
1128
|
+
def onContextSaveImageAs_(self, sender):
|
|
1129
|
+
if self.open_image_item:
|
|
1130
|
+
self.pending_image_action = "save_as"
|
|
1131
|
+
Cocoa.NSApp.sendAction_to_from_(self.open_image_item.action(), self.open_image_item.target(), self.open_image_item)
|
|
1132
|
+
|
|
1133
|
+
def onContextCopyImageAddress_(self, sender):
|
|
1134
|
+
if self.open_image_item:
|
|
1135
|
+
self.pending_image_action = "copy_address"
|
|
1136
|
+
Cocoa.NSApp.sendAction_to_from_(self.open_image_item.action(), self.open_image_item.target(), self.open_image_item)
|
|
1137
|
+
|
|
1138
|
+
def onDirectDownloadImage_(self, sender):
|
|
1139
|
+
if hasattr(self, "controller") and self.controller and self.controller.last_context_image_url:
|
|
1140
|
+
self.controller.perform_download_image(self.controller.last_context_image_url)
|
|
1141
|
+
|
|
1142
|
+
def onDirectSaveImageAs_(self, sender):
|
|
1143
|
+
if hasattr(self, "controller") and self.controller and self.controller.last_context_image_url:
|
|
1144
|
+
self.controller.perform_save_image_as(self.controller.last_context_image_url)
|
|
1145
|
+
|
|
1146
|
+
def onDirectCopyImageAddress_(self, sender):
|
|
1147
|
+
if hasattr(self, "controller") and self.controller and self.controller.last_context_image_url:
|
|
1148
|
+
self.controller.perform_copy_image_address(self.controller.last_context_image_url)
|
|
1149
|
+
|
|
1150
|
+
# ============================================================================
|
|
1151
|
+
# OMNIBOX & SUGGESTIONS
|
|
1152
|
+
# ============================================================================
|
|
1153
|
+
class ChromeOmniboxField(Cocoa.NSTextField):
|
|
1154
|
+
def initWithFrame_(self, frame):
|
|
1155
|
+
self = objc.super(ChromeOmniboxField, self).initWithFrame_(frame)
|
|
1156
|
+
if self:
|
|
1157
|
+
self.setFocusRingType_(Cocoa.NSFocusRingTypeNone)
|
|
1158
|
+
self.setBordered_(False)
|
|
1159
|
+
self.setDrawsBackground_(False)
|
|
1160
|
+
self.setFont_(Cocoa.NSFont.systemFontOfSize_(12))
|
|
1161
|
+
self.raw_url = ""
|
|
1162
|
+
return self
|
|
1163
|
+
|
|
1164
|
+
def becomeFirstResponder(self):
|
|
1165
|
+
if self.superview():
|
|
1166
|
+
self.superview().setNeedsDisplay_(True)
|
|
1167
|
+
self.setStringValue_(self.raw_url)
|
|
1168
|
+
self.setTextColor_(Cocoa.NSColor.labelColor())
|
|
1169
|
+
self.performSelector_withObject_afterDelay_("selectText:", self, 0.0)
|
|
1170
|
+
return objc.super(ChromeOmniboxField, self).becomeFirstResponder()
|
|
1171
|
+
|
|
1172
|
+
def resignFirstResponder(self):
|
|
1173
|
+
if self.superview():
|
|
1174
|
+
self.superview().setNeedsDisplay_(True)
|
|
1175
|
+
self.setStringValue_(self.raw_url or "")
|
|
1176
|
+
return objc.super(ChromeOmniboxField, self).resignFirstResponder()
|
|
1177
|
+
|
|
1178
|
+
def set_url(self, url):
|
|
1179
|
+
self.raw_url = url
|
|
1180
|
+
fr = self.window().firstResponder() if self.window() else None
|
|
1181
|
+
if fr != self and fr != self.currentEditor():
|
|
1182
|
+
self.setStringValue_(url or "")
|
|
1183
|
+
|
|
1184
|
+
class OmniboxPopupPanel(Cocoa.NSPanel):
|
|
1185
|
+
def canBecomeKeyWindow(self): return False
|
|
1186
|
+
def hasKeyAppearance(self): return True
|
|
1187
|
+
|
|
1188
|
+
class OmniboxSuggestionView(Cocoa.NSView):
|
|
1189
|
+
def initWithFrame_controller_(self, frame, controller):
|
|
1190
|
+
self = objc.super(OmniboxSuggestionView, self).initWithFrame_(frame)
|
|
1191
|
+
if self:
|
|
1192
|
+
self.controller, self.suggestions, self.selected_index, self.tracking_area = controller, [], -1, None
|
|
1193
|
+
return self
|
|
1194
|
+
|
|
1195
|
+
def isFlipped(self): return True
|
|
1196
|
+
def acceptsFirstMouse_(self, event): return True
|
|
1197
|
+
|
|
1198
|
+
def updateTrackingAreas(self):
|
|
1199
|
+
if self.tracking_area: self.removeTrackingArea_(self.tracking_area)
|
|
1200
|
+
opts = Cocoa.NSTrackingMouseMoved | Cocoa.NSTrackingActiveAlways | Cocoa.NSTrackingInVisibleRect
|
|
1201
|
+
self.tracking_area = Cocoa.NSTrackingArea.alloc().initWithRect_options_owner_userInfo_(Cocoa.NSZeroRect, opts, self, None)
|
|
1202
|
+
self.addTrackingArea_(self.tracking_area)
|
|
1203
|
+
objc.super(OmniboxSuggestionView, self).updateTrackingAreas()
|
|
1204
|
+
|
|
1205
|
+
def mouseMoved_(self, event):
|
|
1206
|
+
loc = self.convertPoint_fromView_(event.locationInWindow(), None)
|
|
1207
|
+
idx = int(loc.y // 28)
|
|
1208
|
+
if 0 <= idx < len(self.suggestions) and self.selected_index != idx:
|
|
1209
|
+
self.selected_index = idx; self.setNeedsDisplay_(True)
|
|
1210
|
+
|
|
1211
|
+
def mouseDown_(self, event):
|
|
1212
|
+
loc = self.convertPoint_fromView_(event.locationInWindow(), None)
|
|
1213
|
+
idx = int(loc.y // 28)
|
|
1214
|
+
if 0 <= idx < len(self.suggestions):
|
|
1215
|
+
self.controller.omnibox_panel.select_suggestion(self.suggestions[idx])
|
|
1216
|
+
|
|
1217
|
+
def drawRect_(self, dirtyRect):
|
|
1218
|
+
Cocoa.NSColor.clearColor().setFill()
|
|
1219
|
+
Cocoa.NSRectFill(self.bounds())
|
|
1220
|
+
for i, sugg in enumerate(self.suggestions):
|
|
1221
|
+
rect = Cocoa.NSMakeRect(0, i * 28, self.bounds().size.width, 28)
|
|
1222
|
+
if i == self.selected_index:
|
|
1223
|
+
hex_to_nscolor("#1A73E8").setFill()
|
|
1224
|
+
Cocoa.NSRectFill(rect)
|
|
1225
|
+
text_color = Cocoa.NSColor.whiteColor()
|
|
1226
|
+
else:
|
|
1227
|
+
text_color = Cocoa.NSColor.labelColor()
|
|
1228
|
+
|
|
1229
|
+
icon = "🔍" if sugg["type"] == "search" else ("🕒" if sugg["type"] == "history" else ("🔖" if sugg["type"] == "bookmark" else ("🧮" if sugg["type"] == "math" else "⚡")))
|
|
1230
|
+
attrs = {Cocoa.NSFontAttributeName: Cocoa.NSFont.systemFontOfSize_(13), Cocoa.NSForegroundColorAttributeName: text_color}
|
|
1231
|
+
Cocoa.NSAttributedString.alloc().initWithString_attributes_(f"{icon} {sugg['text']}", attrs).drawAtPoint_(Cocoa.NSMakePoint(12, rect.origin.y + 6))
|
|
1232
|
+
|
|
1233
|
+
class OmniboxPanelCoordinator:
|
|
1234
|
+
def __init__(self, window_controller):
|
|
1235
|
+
self.wc = window_controller
|
|
1236
|
+
self.suggestion_window, self.suggestion_view = None, None
|
|
1237
|
+
self.current_req_id, self.typed_query = 0, ""
|
|
1238
|
+
|
|
1239
|
+
def is_visible(self):
|
|
1240
|
+
return bool(self.suggestion_window and self.suggestion_window.isVisible())
|
|
1241
|
+
|
|
1242
|
+
def handle_text_change(self, query):
|
|
1243
|
+
if not SETTINGS.settings.get("omnibox_suggestions_enabled", True) or not query or not query.strip():
|
|
1244
|
+
self.hide()
|
|
1245
|
+
return
|
|
1246
|
+
|
|
1247
|
+
query_str = query.strip()
|
|
1248
|
+
self.typed_query = query_str
|
|
1249
|
+
self.current_req_id += 1
|
|
1250
|
+
req_id = self.current_req_id
|
|
1251
|
+
|
|
1252
|
+
results = []
|
|
1253
|
+
for qa in ["@settings", "@history", "@bookmarks", "@extensions", "@downloads", "@terminal", "@processes"]:
|
|
1254
|
+
if qa.startswith(query_str.lower()):
|
|
1255
|
+
results.append({"type": "action", "text": qa, "url": f"browser://{qa[1:]}"})
|
|
1256
|
+
|
|
1257
|
+
math_res = safe_math_eval(query_str)
|
|
1258
|
+
if math_res:
|
|
1259
|
+
results.append({"type": "math", "text": f"= {math_res}", "url": math_res, "is_eval": True})
|
|
1260
|
+
|
|
1261
|
+
try:
|
|
1262
|
+
with open(BOOKMARKS_FILE, "r", encoding="utf-8") as f:
|
|
1263
|
+
bms = json.load(f)
|
|
1264
|
+
for b in bms:
|
|
1265
|
+
if isinstance(b, dict):
|
|
1266
|
+
t, u = b.get("title", ""), b.get("url", "")
|
|
1267
|
+
if query_str.lower() in t.lower() or query_str.lower() in u.lower():
|
|
1268
|
+
results.append({"type": "bookmark", "text": t or u, "url": u})
|
|
1269
|
+
except Exception:
|
|
1270
|
+
pass
|
|
1271
|
+
|
|
1272
|
+
for h in query_history(query_str)[:4]:
|
|
1273
|
+
results.append({"type": "history", "text": h[2], "url": h[1]})
|
|
1274
|
+
|
|
1275
|
+
def fetch_remote():
|
|
1276
|
+
try:
|
|
1277
|
+
url = f"http://suggestqueries.google.com/complete/search?client=chrome&q={urllib.parse.quote(query_str)}"
|
|
1278
|
+
req = urllib.request.Request(url, headers={"User-Agent": UA_PRESETS["chrome_mac"]})
|
|
1279
|
+
with urllib.request.urlopen(req, timeout=0.8) as resp:
|
|
1280
|
+
data = json.loads(resp.read().decode('utf-8'))
|
|
1281
|
+
remote = [{"type": "search", "text": s, "url": s} for s in data[1][:5]]
|
|
1282
|
+
if req_id == self.current_req_id:
|
|
1283
|
+
dispatch_to_main(lambda: self.show(results + remote))
|
|
1284
|
+
except Exception:
|
|
1285
|
+
if req_id == self.current_req_id:
|
|
1286
|
+
dispatch_to_main(lambda: self.show(results))
|
|
1287
|
+
|
|
1288
|
+
threading.Thread(target=fetch_remote, daemon=True).start()
|
|
1289
|
+
|
|
1290
|
+
def show(self, suggestions):
|
|
1291
|
+
if not suggestions:
|
|
1292
|
+
self.hide()
|
|
1293
|
+
return
|
|
1294
|
+
if not self.suggestion_window:
|
|
1295
|
+
self.suggestion_window = OmniboxPopupPanel.alloc().initWithContentRect_styleMask_backing_defer_(
|
|
1296
|
+
Cocoa.NSMakeRect(0, 0, 100, 100), Cocoa.NSWindowStyleMaskBorderless, Cocoa.NSBackingStoreBuffered, False
|
|
1297
|
+
)
|
|
1298
|
+
self.suggestion_window.setOpaque_(False)
|
|
1299
|
+
self.suggestion_window.setAcceptsMouseMovedEvents_(True)
|
|
1300
|
+
self.suggestion_window.setBackgroundColor_(Cocoa.NSColor.clearColor())
|
|
1301
|
+
self.suggestion_window.setHasShadow_(True)
|
|
1302
|
+
|
|
1303
|
+
vev = Cocoa.NSVisualEffectView.alloc().initWithFrame_(self.suggestion_window.contentView().bounds())
|
|
1304
|
+
vev.setMaterial_(Cocoa.NSVisualEffectMaterialPopover)
|
|
1305
|
+
vev.setBlendingMode_(Cocoa.NSVisualEffectBlendingModeBehindWindow)
|
|
1306
|
+
vev.setState_(Cocoa.NSVisualEffectStateActive)
|
|
1307
|
+
vev.setWantsLayer_(True)
|
|
1308
|
+
vev.layer().setCornerRadius_(8)
|
|
1309
|
+
self.suggestion_window.setContentView_(vev)
|
|
1310
|
+
|
|
1311
|
+
self.suggestion_view = OmniboxSuggestionView.alloc().initWithFrame_controller_(vev.bounds(), self.wc)
|
|
1312
|
+
vev.addSubview_(self.suggestion_view)
|
|
1313
|
+
|
|
1314
|
+
self.suggestion_view.suggestions = suggestions[:8]
|
|
1315
|
+
self.suggestion_view.selected_index = -1
|
|
1316
|
+
|
|
1317
|
+
rect_in_window = self.wc.omnibox_container.convertRect_toView_(self.wc.omnibox_container.bounds(), None)
|
|
1318
|
+
rect = self.wc.window().convertRectToScreen_(rect_in_window)
|
|
1319
|
+
popup_h = len(self.suggestion_view.suggestions) * 28
|
|
1320
|
+
|
|
1321
|
+
self.suggestion_window.setFrame_display_(Cocoa.NSMakeRect(rect.origin.x, rect.origin.y - popup_h - 4, rect.size.width, popup_h), True)
|
|
1322
|
+
self.suggestion_view.setFrame_(self.suggestion_window.contentView().bounds())
|
|
1323
|
+
self.suggestion_view.setNeedsDisplay_(True)
|
|
1324
|
+
if self.suggestion_window.parentWindow() != self.wc.window():
|
|
1325
|
+
self.wc.window().addChildWindow_ordered_(self.suggestion_window, Cocoa.NSWindowAbove)
|
|
1326
|
+
self.suggestion_window.orderFront_(None)
|
|
1327
|
+
|
|
1328
|
+
def hide(self):
|
|
1329
|
+
if self.suggestion_window and self.suggestion_window.isVisible():
|
|
1330
|
+
if self.suggestion_window.parentWindow():
|
|
1331
|
+
self.wc.window().removeChildWindow_(self.suggestion_window)
|
|
1332
|
+
self.suggestion_window.orderOut_(None)
|
|
1333
|
+
|
|
1334
|
+
def select_suggestion(self, sugg):
|
|
1335
|
+
if sugg.get("is_eval"):
|
|
1336
|
+
val = sugg.get("url", "")
|
|
1337
|
+
self.wc.omnibox.setStringValue_(val)
|
|
1338
|
+
pb = Cocoa.NSPasteboard.generalPasteboard()
|
|
1339
|
+
pb.clearContents()
|
|
1340
|
+
pb.setString_forType_(val, Cocoa.NSPasteboardTypeString)
|
|
1341
|
+
self.hide()
|
|
1342
|
+
return
|
|
1343
|
+
target = sugg.get("url") or sugg.get("text")
|
|
1344
|
+
self.wc.omnibox.setStringValue_(target)
|
|
1345
|
+
editor = self.wc.omnibox.currentEditor()
|
|
1346
|
+
if editor:
|
|
1347
|
+
editor.setString_(target)
|
|
1348
|
+
self.hide()
|
|
1349
|
+
self.wc.onOmniboxEnter_(self.wc.omnibox)
|
|
1350
|
+
|
|
1351
|
+
def handle_command_selector(self, commandSelector):
|
|
1352
|
+
sel = str(commandSelector)
|
|
1353
|
+
if self.is_visible():
|
|
1354
|
+
if sel == "moveDown:":
|
|
1355
|
+
self.suggestion_view.selected_index = min(len(self.suggestion_view.suggestions)-1, self.suggestion_view.selected_index + 1)
|
|
1356
|
+
if self.suggestion_view.selected_index >= 0:
|
|
1357
|
+
s = self.suggestion_view.suggestions[self.suggestion_view.selected_index]
|
|
1358
|
+
self.wc.omnibox.setStringValue_(s.get("url") or s.get("text"))
|
|
1359
|
+
self.suggestion_view.setNeedsDisplay_(True)
|
|
1360
|
+
return True
|
|
1361
|
+
elif sel == "moveUp:":
|
|
1362
|
+
self.suggestion_view.selected_index = max(-1, self.suggestion_view.selected_index - 1)
|
|
1363
|
+
if self.suggestion_view.selected_index >= 0:
|
|
1364
|
+
s = self.suggestion_view.suggestions[self.suggestion_view.selected_index]
|
|
1365
|
+
self.wc.omnibox.setStringValue_(s.get("url") or s.get("text"))
|
|
1366
|
+
else:
|
|
1367
|
+
self.wc.omnibox.setStringValue_(self.typed_query)
|
|
1368
|
+
self.suggestion_view.setNeedsDisplay_(True)
|
|
1369
|
+
return True
|
|
1370
|
+
elif sel == "insertNewline:":
|
|
1371
|
+
if self.suggestion_view.selected_index >= 0:
|
|
1372
|
+
self.select_suggestion(self.suggestion_view.suggestions[self.suggestion_view.selected_index])
|
|
1373
|
+
return True
|
|
1374
|
+
elif sel == "cancelOperation:":
|
|
1375
|
+
self.hide()
|
|
1376
|
+
return True
|
|
1377
|
+
return False
|
|
1378
|
+
|
|
1379
|
+
# ============================================================================
|
|
1380
|
+
# MATERIAL PROGRESS BAR & FIND BAR
|
|
1381
|
+
# ============================================================================
|
|
1382
|
+
class MaterialProgressBar(Cocoa.NSView):
|
|
1383
|
+
def initWithFrame_(self, frame):
|
|
1384
|
+
self = objc.super(MaterialProgressBar, self).initWithFrame_(frame)
|
|
1385
|
+
if self:
|
|
1386
|
+
self.progress = 0.0
|
|
1387
|
+
return self
|
|
1388
|
+
|
|
1389
|
+
def setProgress_(self, val):
|
|
1390
|
+
self.progress = min(1.0, max(0.0, float(val)))
|
|
1391
|
+
self.setNeedsDisplay_(True)
|
|
1392
|
+
|
|
1393
|
+
def drawRect_(self, dirtyRect):
|
|
1394
|
+
if self.progress <= 0.0:
|
|
1395
|
+
return
|
|
1396
|
+
w, h = self.bounds().size.width * self.progress, self.bounds().size.height
|
|
1397
|
+
path = Cocoa.NSBezierPath.bezierPathWithRect_(Cocoa.NSMakeRect(0, 0, w, h))
|
|
1398
|
+
is_dark = False
|
|
1399
|
+
try:
|
|
1400
|
+
if self.effectiveAppearance():
|
|
1401
|
+
is_dark = "Dark" in str(self.effectiveAppearance().name())
|
|
1402
|
+
except Exception:
|
|
1403
|
+
pass
|
|
1404
|
+
hex_to_nscolor("#8AB4F8" if is_dark else "#1A73E8").setFill()
|
|
1405
|
+
path.fill()
|
|
1406
|
+
|
|
1407
|
+
class FindBarView(Cocoa.NSView):
|
|
1408
|
+
def initWithFrame_controller_(self, frame, controller):
|
|
1409
|
+
self = objc.super(FindBarView, self).initWithFrame_(frame)
|
|
1410
|
+
if self:
|
|
1411
|
+
self.controller = controller
|
|
1412
|
+
self.setup_ui()
|
|
1413
|
+
return self
|
|
1414
|
+
|
|
1415
|
+
def drawRect_(self, dirtyRect):
|
|
1416
|
+
path = Cocoa.NSBezierPath.bezierPathWithRoundedRect_xRadius_yRadius_(self.bounds(), 8, 8)
|
|
1417
|
+
get_m3_color("toolbar-bg", self).setFill()
|
|
1418
|
+
path.fill()
|
|
1419
|
+
path.setLineWidth_(1.0)
|
|
1420
|
+
get_m3_color("toolbar-separator", self).setStroke()
|
|
1421
|
+
path.stroke()
|
|
1422
|
+
|
|
1423
|
+
def setup_ui(self):
|
|
1424
|
+
self.search_field = Cocoa.NSSearchField.alloc().initWithFrame_(Cocoa.NSMakeRect(10, 10, 200, 22))
|
|
1425
|
+
self.search_field.setTarget_(self)
|
|
1426
|
+
self.search_field.setAction_("onSearch:")
|
|
1427
|
+
self.search_field.setPlaceholderString_("Find in page…")
|
|
1428
|
+
self.addSubview_(self.search_field)
|
|
1429
|
+
self.close_btn = Cocoa.NSButton.alloc().initWithFrame_(Cocoa.NSMakeRect(218, 10, 24, 22))
|
|
1430
|
+
self.close_btn.setTitle_("✕")
|
|
1431
|
+
self.close_btn.setBezelStyle_(Cocoa.NSBezelStyleInline)
|
|
1432
|
+
self.close_btn.setTarget_(self)
|
|
1433
|
+
self.close_btn.setAction_("onClose:")
|
|
1434
|
+
self.addSubview_(self.close_btn)
|
|
1435
|
+
|
|
1436
|
+
def onSearch_(self, sender):
|
|
1437
|
+
query = self.search_field.stringValue()
|
|
1438
|
+
wv = self.controller.get_active_webview()
|
|
1439
|
+
if wv and query.strip():
|
|
1440
|
+
cfg = WebKit.WKFindConfiguration.alloc().init()
|
|
1441
|
+
cfg.setWraps_(True)
|
|
1442
|
+
wv.findString_withConfiguration_completionHandler_(query, cfg, lambda res: None)
|
|
1443
|
+
|
|
1444
|
+
def onClose_(self, sender):
|
|
1445
|
+
self.setHidden_(True)
|
|
1446
|
+
wv = self.controller.get_active_webview()
|
|
1447
|
+
if wv:
|
|
1448
|
+
self.controller.window().makeFirstResponder_(wv)
|
|
1449
|
+
|
|
1450
|
+
# ============================================================================
|
|
1451
|
+
# QUICK SWITCHER & MEDIA CONTROLS (PIP)
|
|
1452
|
+
# ============================================================================
|
|
1453
|
+
class QuickSwitcherListView(Cocoa.NSView):
|
|
1454
|
+
def initWithFrame_controller_(self, frame, controller):
|
|
1455
|
+
self = objc.super(QuickSwitcherListView, self).initWithFrame_(frame)
|
|
1456
|
+
if self:
|
|
1457
|
+
self.controller = controller
|
|
1458
|
+
return self
|
|
1459
|
+
|
|
1460
|
+
def isFlipped(self): return True
|
|
1461
|
+
|
|
1462
|
+
def drawRect_(self, dirtyRect):
|
|
1463
|
+
for i, tab in enumerate(self.controller.filtered_tabs):
|
|
1464
|
+
rect = Cocoa.NSMakeRect(0, i * 36, self.bounds().size.width, 36)
|
|
1465
|
+
if i == self.controller.selected_index:
|
|
1466
|
+
hex_to_nscolor("#1A73E8").setFill()
|
|
1467
|
+
Cocoa.NSRectFill(rect)
|
|
1468
|
+
tc, sub_color = Cocoa.NSColor.whiteColor(), Cocoa.NSColor.whiteColor()
|
|
1469
|
+
else:
|
|
1470
|
+
tc, sub_color = Cocoa.NSColor.labelColor(), Cocoa.NSColor.secondaryLabelColor()
|
|
1471
|
+
|
|
1472
|
+
attrs = {Cocoa.NSFontAttributeName: Cocoa.NSFont.boldSystemFontOfSize_(13), Cocoa.NSForegroundColorAttributeName: tc}
|
|
1473
|
+
Cocoa.NSAttributedString.alloc().initWithString_attributes_(tab["title"][:60], attrs).drawAtPoint_(Cocoa.NSMakePoint(20, rect.origin.y + 3))
|
|
1474
|
+
sub_attrs = {Cocoa.NSFontAttributeName: Cocoa.NSFont.systemFontOfSize_(11), Cocoa.NSForegroundColorAttributeName: sub_color}
|
|
1475
|
+
Cocoa.NSAttributedString.alloc().initWithString_attributes_(tab["url"][:80], sub_attrs).drawAtPoint_(Cocoa.NSMakePoint(20, rect.origin.y + 19))
|
|
1476
|
+
|
|
1477
|
+
class QuickSwitcherController(Cocoa.NSWindowController):
|
|
1478
|
+
def init(self):
|
|
1479
|
+
w, h = 600, 400
|
|
1480
|
+
window = Cocoa.NSWindow.alloc().initWithContentRect_styleMask_backing_defer_(
|
|
1481
|
+
Cocoa.NSMakeRect(0, 0, w, h), Cocoa.NSWindowStyleMaskBorderless | Cocoa.NSWindowStyleMaskFullSizeContentView, Cocoa.NSBackingStoreBuffered, False
|
|
1482
|
+
)
|
|
1483
|
+
window.setOpaque_(False)
|
|
1484
|
+
window.setBackgroundColor_(Cocoa.NSColor.clearColor())
|
|
1485
|
+
window.setHasShadow_(True)
|
|
1486
|
+
window.center()
|
|
1487
|
+
self = objc.super(QuickSwitcherController, self).initWithWindow_(window)
|
|
1488
|
+
if self:
|
|
1489
|
+
self.setup_ui()
|
|
1490
|
+
return self
|
|
1491
|
+
|
|
1492
|
+
def setup_ui(self):
|
|
1493
|
+
vev = Cocoa.NSVisualEffectView.alloc().initWithFrame_(self.window().contentView().bounds())
|
|
1494
|
+
vev.setMaterial_(Cocoa.NSVisualEffectMaterialPopover)
|
|
1495
|
+
vev.setBlendingMode_(Cocoa.NSVisualEffectBlendingModeBehindWindow)
|
|
1496
|
+
vev.setState_(Cocoa.NSVisualEffectStateActive)
|
|
1497
|
+
vev.setWantsLayer_(True)
|
|
1498
|
+
vev.layer().setCornerRadius_(12)
|
|
1499
|
+
vev.layer().setMasksToBounds_(True)
|
|
1500
|
+
self.window().setContentView_(vev)
|
|
1501
|
+
|
|
1502
|
+
self.search_field = Cocoa.NSTextField.alloc().initWithFrame_(Cocoa.NSMakeRect(20, 345, 560, 35))
|
|
1503
|
+
self.search_field.setFont_(Cocoa.NSFont.systemFontOfSize_(20))
|
|
1504
|
+
self.search_field.setFocusRingType_(Cocoa.NSFocusRingTypeNone)
|
|
1505
|
+
self.search_field.setBordered_(False)
|
|
1506
|
+
self.search_field.setDrawsBackground_(False)
|
|
1507
|
+
self.search_field.setPlaceholderString_("Search Open Tabs…")
|
|
1508
|
+
self.search_field.setDelegate_(self)
|
|
1509
|
+
vev.addSubview_(self.search_field)
|
|
1510
|
+
|
|
1511
|
+
self.list_view = QuickSwitcherListView.alloc().initWithFrame_controller_(Cocoa.NSMakeRect(0, 0, 600, 330), self)
|
|
1512
|
+
vev.addSubview_(self.list_view)
|
|
1513
|
+
self.tabs, self.filtered_tabs, self.selected_index = [], [], 0
|
|
1514
|
+
|
|
1515
|
+
def show(self):
|
|
1516
|
+
self.tabs = []
|
|
1517
|
+
app_delegate = Cocoa.NSApp.delegate()
|
|
1518
|
+
if app_delegate and hasattr(app_delegate, "controllers"):
|
|
1519
|
+
for wc in app_delegate.controllers:
|
|
1520
|
+
for t in wc.tabs:
|
|
1521
|
+
self.tabs.append({
|
|
1522
|
+
"id": t.get("id"),
|
|
1523
|
+
"title": t.get("title", "Tab"),
|
|
1524
|
+
"url": t.get("url", ""),
|
|
1525
|
+
"wc": wc
|
|
1526
|
+
})
|
|
1527
|
+
self.filtered_tabs = self.tabs.copy()
|
|
1528
|
+
self.selected_index = 0
|
|
1529
|
+
self.search_field.setStringValue_("")
|
|
1530
|
+
self.list_view.setNeedsDisplay_(True)
|
|
1531
|
+
self.window().makeKeyAndOrderFront_(None)
|
|
1532
|
+
self.window().makeFirstResponder_(self.search_field)
|
|
1533
|
+
|
|
1534
|
+
def controlTextDidChange_(self, notif):
|
|
1535
|
+
if notif.object() == self.search_field:
|
|
1536
|
+
q = self.search_field.stringValue().lower().strip()
|
|
1537
|
+
self.filtered_tabs = self.tabs.copy() if not q else [t for t in self.tabs if q in t["title"].lower() or q in t["url"].lower()]
|
|
1538
|
+
self.selected_index = 0
|
|
1539
|
+
self.list_view.setNeedsDisplay_(True)
|
|
1540
|
+
|
|
1541
|
+
def control_textView_doCommandBySelector_(self, control, textView, commandSelector):
|
|
1542
|
+
sel = str(commandSelector)
|
|
1543
|
+
if sel == "moveDown:":
|
|
1544
|
+
self.selected_index = min(len(self.filtered_tabs) - 1, self.selected_index + 1)
|
|
1545
|
+
self.list_view.setNeedsDisplay_(True)
|
|
1546
|
+
return True
|
|
1547
|
+
elif sel == "moveUp:":
|
|
1548
|
+
self.selected_index = max(0, self.selected_index - 1)
|
|
1549
|
+
self.list_view.setNeedsDisplay_(True)
|
|
1550
|
+
return True
|
|
1551
|
+
elif sel == "insertNewline:":
|
|
1552
|
+
if self.filtered_tabs and self.selected_index >= 0:
|
|
1553
|
+
t = self.filtered_tabs[self.selected_index]
|
|
1554
|
+
# Fixed: Resolve current tab index dynamically by ID to avoid desync/crash
|
|
1555
|
+
cur_idx = t["wc"].get_tab_index_by_id(t["id"])
|
|
1556
|
+
if cur_idx is not None:
|
|
1557
|
+
t["wc"].window().makeKeyAndOrderFront_(None)
|
|
1558
|
+
t["wc"].switch_to_tab(cur_idx)
|
|
1559
|
+
self.window().orderOut_(None)
|
|
1560
|
+
return True
|
|
1561
|
+
elif sel == "cancelOperation:":
|
|
1562
|
+
self.window().orderOut_(None)
|
|
1563
|
+
return True
|
|
1564
|
+
return False
|
|
1565
|
+
|
|
1566
|
+
class PiPPanelController(Cocoa.NSWindowController):
|
|
1567
|
+
def initWithBrowser_(self, browser_controller):
|
|
1568
|
+
w, h = 240, 80
|
|
1569
|
+
window = Cocoa.NSPanel.alloc().initWithContentRect_styleMask_backing_defer_(
|
|
1570
|
+
Cocoa.NSMakeRect(0, 0, w, h), Cocoa.NSWindowStyleMaskTitled | Cocoa.NSWindowStyleMaskClosable | Cocoa.NSWindowStyleMaskHUDWindow | Cocoa.NSWindowStyleMaskUtilityWindow,
|
|
1571
|
+
Cocoa.NSBackingStoreBuffered, False
|
|
1572
|
+
)
|
|
1573
|
+
window.setTitle_("Media Controls")
|
|
1574
|
+
window.setFloatingPanel_(True)
|
|
1575
|
+
window.setLevel_(Cocoa.NSFloatingWindowLevel)
|
|
1576
|
+
window.setReleasedWhenClosed_(False)
|
|
1577
|
+
self = objc.super(PiPPanelController, self).initWithWindow_(window)
|
|
1578
|
+
if self:
|
|
1579
|
+
self.browser = browser_controller
|
|
1580
|
+
self.setup_ui()
|
|
1581
|
+
return self
|
|
1582
|
+
|
|
1583
|
+
def setup_ui(self):
|
|
1584
|
+
cv = self.window().contentView()
|
|
1585
|
+
def make_btn(title, x, action):
|
|
1586
|
+
btn = Cocoa.NSButton.alloc().initWithFrame_(Cocoa.NSMakeRect(x, 20, 44, 34))
|
|
1587
|
+
btn.setTitle_(title)
|
|
1588
|
+
btn.setBezelStyle_(Cocoa.NSBezelStyleRounded)
|
|
1589
|
+
btn.setTarget_(self)
|
|
1590
|
+
btn.setAction_(action)
|
|
1591
|
+
cv.addSubview_(btn)
|
|
1592
|
+
return btn
|
|
1593
|
+
self.btn_back = make_btn("↺10", 20, "onScrubBack:")
|
|
1594
|
+
self.btn_play = make_btn("⏯", 95, "onPlayPause:")
|
|
1595
|
+
self.btn_fwd = make_btn("10↻", 170, "onScrubFwd:")
|
|
1596
|
+
|
|
1597
|
+
def toggle(self):
|
|
1598
|
+
if self.window().isVisible():
|
|
1599
|
+
self.window().orderOut_(None)
|
|
1600
|
+
else:
|
|
1601
|
+
self.window().makeKeyAndOrderFront_(None)
|
|
1602
|
+
|
|
1603
|
+
def exec_js(self, js):
|
|
1604
|
+
# Fixed: Prefer active window controller or stored browser controller
|
|
1605
|
+
app_delegate = Cocoa.NSApp.delegate()
|
|
1606
|
+
wc = None
|
|
1607
|
+
key_win = Cocoa.NSApp.keyWindow()
|
|
1608
|
+
if key_win and isinstance(key_win.delegate(), Cocoa.NSWindowController) and hasattr(key_win.delegate(), "get_active_webview"):
|
|
1609
|
+
wc = key_win.delegate()
|
|
1610
|
+
elif self.browser:
|
|
1611
|
+
wc = self.browser
|
|
1612
|
+
elif app_delegate and getattr(app_delegate, "controllers", None):
|
|
1613
|
+
wc = app_delegate.controllers[0]
|
|
1614
|
+
|
|
1615
|
+
if wc:
|
|
1616
|
+
wv = wc.get_active_webview()
|
|
1617
|
+
if wv:
|
|
1618
|
+
wv.evaluateJavaScript_completionHandler_(js, None)
|
|
1619
|
+
|
|
1620
|
+
def onPlayPause_(self, s): self.exec_js("var v = document.querySelector('video, audio'); if(v) v.paused ? v.play() : v.pause();")
|
|
1621
|
+
def onScrubBack_(self, s): self.exec_js("var v = document.querySelector('video, audio'); if(v) v.currentTime -= 10;")
|
|
1622
|
+
def onScrubFwd_(self, s): self.exec_js("var v = document.querySelector('video, audio'); if(v) v.currentTime += 10;")
|
|
1623
|
+
# ============================================================================
|
|
1624
|
+
# BROWSER WINDOW CONTROLLER
|
|
1625
|
+
# ============================================================================
|
|
1626
|
+
TITLE_BAR_H = 30
|
|
1627
|
+
|
|
1628
|
+
class BrowserWindowController(Cocoa.NSWindowController):
|
|
1629
|
+
def init(self):
|
|
1630
|
+
return self.initWithIncognito_(False)
|
|
1631
|
+
|
|
1632
|
+
def initWithIncognito_(self, is_incognito=False):
|
|
1633
|
+
self = objc.super(BrowserWindowController, self).init()
|
|
1634
|
+
if self:
|
|
1635
|
+
self.is_incognito = is_incognito
|
|
1636
|
+
self.incognito_data_store = create_incognito_data_store() if is_incognito else None
|
|
1637
|
+
self.tabs, self.groups, self.closed_tabs = [], [], []
|
|
1638
|
+
self.active_tab_index, self.downloads = -1, []
|
|
1639
|
+
self.tab_bar_visible, self.is_fullscreen = True, False
|
|
1640
|
+
self.zen_auto_entered = False
|
|
1641
|
+
self.last_context_image_url = None
|
|
1642
|
+
self.download_delegate = DownloadDelegate.alloc().initWithController_(self)
|
|
1643
|
+
self.omnibox_panel = OmniboxPanelCoordinator(self)
|
|
1644
|
+
self.tab_manager = self
|
|
1645
|
+
self.setup_window()
|
|
1646
|
+
return self
|
|
1647
|
+
|
|
1648
|
+
def setup_window(self):
|
|
1649
|
+
w, h = 1200, 800
|
|
1650
|
+
window = Cocoa.NSWindow.alloc().initWithContentRect_styleMask_backing_defer_(
|
|
1651
|
+
Cocoa.NSMakeRect(100, 100, w, h),
|
|
1652
|
+
Cocoa.NSWindowStyleMaskFullSizeContentView | Cocoa.NSWindowStyleMaskTitled |
|
|
1653
|
+
Cocoa.NSWindowStyleMaskClosable | Cocoa.NSWindowStyleMaskMiniaturizable | Cocoa.NSWindowStyleMaskResizable,
|
|
1654
|
+
Cocoa.NSBackingStoreBuffered, False
|
|
1655
|
+
)
|
|
1656
|
+
window.setTitlebarAppearsTransparent_(True)
|
|
1657
|
+
window.setTitleVisibility_(Cocoa.NSWindowTitleHidden)
|
|
1658
|
+
self.setWindow_(window)
|
|
1659
|
+
window.setCollectionBehavior_(Cocoa.NSWindowCollectionBehaviorFullScreenPrimary)
|
|
1660
|
+
window.setDelegate_(self)
|
|
1661
|
+
|
|
1662
|
+
self.root_view = Cocoa.NSView.alloc().initWithFrame_(Cocoa.NSMakeRect(0, 0, w, h))
|
|
1663
|
+
window.setContentView_(self.root_view)
|
|
1664
|
+
self.setup_ui(w, h)
|
|
1665
|
+
|
|
1666
|
+
if not self.restore_session():
|
|
1667
|
+
self.load_initial_tabs()
|
|
1668
|
+
window.center()
|
|
1669
|
+
wmode = SETTINGS.settings.get("startup_window_mode", "normal")
|
|
1670
|
+
if wmode == "maximized":
|
|
1671
|
+
window.zoom_(None)
|
|
1672
|
+
elif wmode == "fullscreen":
|
|
1673
|
+
window.toggleFullScreen_(None)
|
|
1674
|
+
|
|
1675
|
+
self.sleep_timer = Cocoa.NSTimer.scheduledTimerWithTimeInterval_repeats_block_(60.0, True, lambda t: self.check_sleeping_tabs())
|
|
1676
|
+
window.makeKeyAndOrderFront_(None)
|
|
1677
|
+
|
|
1678
|
+
def load_initial_tabs(self):
|
|
1679
|
+
behavior = SETTINGS.settings.get("startup_behavior", "new_tab")
|
|
1680
|
+
if behavior == "custom_url":
|
|
1681
|
+
self.add_tab(SETTINGS.settings.get("startup_url", DEFAULT_URL))
|
|
1682
|
+
return
|
|
1683
|
+
self.add_tab(DEFAULT_URL)
|
|
1684
|
+
|
|
1685
|
+
def save_session(self):
|
|
1686
|
+
if self.is_incognito:
|
|
1687
|
+
return
|
|
1688
|
+
data = {
|
|
1689
|
+
"active_index": self.active_tab_index,
|
|
1690
|
+
"tabs": [{"url": t.get("url"), "group_id": t.get("group_id"), "pinned": t.get("pinned", False)} for t in self.tabs if t.get("url") and not t.get("url").startswith("browser://")],
|
|
1691
|
+
"groups": [asdict(g) for g in self.groups]
|
|
1692
|
+
}
|
|
1693
|
+
atomic_write_json(SESSION_FILE, data)
|
|
1694
|
+
|
|
1695
|
+
def restore_session(self):
|
|
1696
|
+
if self.is_incognito or SETTINGS.settings.get("startup_behavior") != "restore":
|
|
1697
|
+
return False
|
|
1698
|
+
if not os.path.exists(SESSION_FILE):
|
|
1699
|
+
return False
|
|
1700
|
+
try:
|
|
1701
|
+
with open(SESSION_FILE, "r", encoding="utf-8") as f:
|
|
1702
|
+
data = json.load(f)
|
|
1703
|
+
if isinstance(data, dict):
|
|
1704
|
+
self.groups = [TabGroup(**g) for g in data.get("groups", [])]
|
|
1705
|
+
tabs_data = data.get("tabs", [])
|
|
1706
|
+
if not tabs_data:
|
|
1707
|
+
return False
|
|
1708
|
+
for t in tabs_data:
|
|
1709
|
+
self.add_tab(t.get("url", DEFAULT_URL), pinned=t.get("pinned", False))
|
|
1710
|
+
if t.get("group_id"):
|
|
1711
|
+
self.tabs[-1]["group_id"] = t.get("group_id")
|
|
1712
|
+
idx = data.get("active_index", 0)
|
|
1713
|
+
self.switch_to_tab(max(0, min(idx, len(self.tabs) - 1)))
|
|
1714
|
+
return True
|
|
1715
|
+
elif isinstance(data, list) and data:
|
|
1716
|
+
for u in data:
|
|
1717
|
+
self.add_tab(u)
|
|
1718
|
+
return True
|
|
1719
|
+
except Exception as e:
|
|
1720
|
+
print(f"[Restore Session Error] {e}")
|
|
1721
|
+
return False
|
|
1722
|
+
|
|
1723
|
+
def setup_ui(self, w, h):
|
|
1724
|
+
cv = self.root_view
|
|
1725
|
+
|
|
1726
|
+
# Title bar view placed at the top (hosts macOS traffic lights, drag, double-click maximize)
|
|
1727
|
+
self.title_bar = TitleBarView.alloc().initWithFrame_controller_(Cocoa.NSMakeRect(0, h - TITLE_BAR_H, w, TITLE_BAR_H), self)
|
|
1728
|
+
cv.addSubview_(self.title_bar)
|
|
1729
|
+
|
|
1730
|
+
self.tab_strip = ChromeTabStripView.alloc().initWithFrame_(Cocoa.NSMakeRect(0, h - TITLE_BAR_H - 34, w, 34))
|
|
1731
|
+
self.tab_strip.controller = self
|
|
1732
|
+
cv.addSubview_(self.tab_strip)
|
|
1733
|
+
|
|
1734
|
+
self.toolbar = ChromeToolbarView.alloc().initWithFrame_(Cocoa.NSMakeRect(0, h - TITLE_BAR_H - 68, w, 34))
|
|
1735
|
+
self.toolbar.controller = self
|
|
1736
|
+
cv.addSubview_(self.toolbar)
|
|
1737
|
+
|
|
1738
|
+
self.progress_bar = MaterialProgressBar.alloc().initWithFrame_(Cocoa.NSMakeRect(0, 0, w, 2.5))
|
|
1739
|
+
self.progress_bar.setHidden_(True)
|
|
1740
|
+
self.toolbar.addSubview_(self.progress_bar)
|
|
1741
|
+
|
|
1742
|
+
bm_visible = SETTINGS.settings.get("bookmarks_bar_visible", True)
|
|
1743
|
+
self.bookmarks_bar = BookmarksBarView.alloc().initWithFrame_controller_(Cocoa.NSMakeRect(0, h - TITLE_BAR_H - 92, w, 24), self)
|
|
1744
|
+
self.bookmarks_bar.setHidden_(not bm_visible)
|
|
1745
|
+
cv.addSubview_(self.bookmarks_bar)
|
|
1746
|
+
|
|
1747
|
+
top_bars_h = TITLE_BAR_H + (92 if bm_visible else 68)
|
|
1748
|
+
self.web_container = Cocoa.NSView.alloc().initWithFrame_(Cocoa.NSMakeRect(0, 0, w, h - top_bars_h))
|
|
1749
|
+
cv.addSubview_(self.web_container)
|
|
1750
|
+
|
|
1751
|
+
self.find_bar = FindBarView.alloc().initWithFrame_controller_(Cocoa.NSMakeRect(w - 260, h - 140, 250, 42), self)
|
|
1752
|
+
self.find_bar.setHidden_(True)
|
|
1753
|
+
cv.addSubview_(self.find_bar)
|
|
1754
|
+
|
|
1755
|
+
def btn(title, x, action, tip=""):
|
|
1756
|
+
b = Cocoa.NSButton.alloc().initWithFrame_(Cocoa.NSMakeRect(x, 4, 26, 26))
|
|
1757
|
+
b.setTitle_(title)
|
|
1758
|
+
if tip:
|
|
1759
|
+
b.setToolTip_(tip)
|
|
1760
|
+
b.setBezelStyle_(Cocoa.NSBezelStyleCircular)
|
|
1761
|
+
b.setTarget_(self)
|
|
1762
|
+
b.setAction_(action)
|
|
1763
|
+
self.toolbar.addSubview_(b)
|
|
1764
|
+
return b
|
|
1765
|
+
|
|
1766
|
+
btn("◀", 8, "onGoBack:")
|
|
1767
|
+
btn("▶", 38, "onGoForward:")
|
|
1768
|
+
btn("↻", 68, "onReload:")
|
|
1769
|
+
self.home_btn = btn("🏠", 98, "onGoHome:", "Home Page")
|
|
1770
|
+
self.home_btn.setHidden_(not SETTINGS.settings.get("show_home_button", False))
|
|
1771
|
+
|
|
1772
|
+
omni_x = 132 if SETTINGS.settings.get("show_home_button", False) else 102
|
|
1773
|
+
# Clean uniform 30px step for toolbar buttons, omnibox expands dynamically
|
|
1774
|
+
self.omnibox_container = Cocoa.NSView.alloc().initWithFrame_(Cocoa.NSMakeRect(omni_x, 3, max(180, w - omni_x - 162), 28))
|
|
1775
|
+
self.toolbar.addSubview_(self.omnibox_container)
|
|
1776
|
+
|
|
1777
|
+
self.site_info_btn = Cocoa.NSButton.alloc().initWithFrame_(Cocoa.NSMakeRect(4, 4, 20, 20))
|
|
1778
|
+
self.site_info_btn.setTitle_("🎛️")
|
|
1779
|
+
self.site_info_btn.setBordered_(False)
|
|
1780
|
+
self.site_info_btn.setFont_(Cocoa.NSFont.systemFontOfSize_(12))
|
|
1781
|
+
self.site_info_btn.setTarget_(self)
|
|
1782
|
+
self.site_info_btn.setAction_("onSiteInfo:")
|
|
1783
|
+
self.omnibox_container.addSubview_(self.site_info_btn)
|
|
1784
|
+
|
|
1785
|
+
self.omnibox = ChromeOmniboxField.alloc().initWithFrame_(Cocoa.NSMakeRect(28, 6, self.omnibox_container.bounds().size.width - 56, 16))
|
|
1786
|
+
self.omnibox.setTarget_(self)
|
|
1787
|
+
self.omnibox.setAction_("onOmniboxEnter:")
|
|
1788
|
+
self.omnibox.setDelegate_(self)
|
|
1789
|
+
self.omnibox_container.addSubview_(self.omnibox)
|
|
1790
|
+
|
|
1791
|
+
self.star_btn = Cocoa.NSButton.alloc().initWithFrame_(Cocoa.NSMakeRect(self.omnibox_container.bounds().size.width - 26, 4, 20, 20))
|
|
1792
|
+
self.star_btn.setTitle_("☆")
|
|
1793
|
+
self.star_btn.setToolTip_("Bookmark (Cmd+D)")
|
|
1794
|
+
self.star_btn.setBordered_(False)
|
|
1795
|
+
self.star_btn.setFont_(Cocoa.NSFont.systemFontOfSize_(13))
|
|
1796
|
+
self.star_btn.setTarget_(self)
|
|
1797
|
+
self.star_btn.setAction_("onBookmarkCurrentPage:")
|
|
1798
|
+
self.star_btn.setAutoresizingMask_(Cocoa.NSViewMinXMargin)
|
|
1799
|
+
self.omnibox_container.addSubview_(self.star_btn)
|
|
1800
|
+
|
|
1801
|
+
# Fixed: Buttons pushed cleanly to the right edge with uniform 30px spacing
|
|
1802
|
+
self.pip_btn = btn("⧉", w - 154, "onTogglePiP:", "Media Controls")
|
|
1803
|
+
self.dl_btn = btn("⬇", w - 124, "onShowDownloads:", "Downloads")
|
|
1804
|
+
self.share_btn = btn("↗", w - 94, "onShareCurrentPage:", "Share Page")
|
|
1805
|
+
self.zen_btn = btn("⛶", w - 64, "onToggleZenMode:", "Zen Mode (Opt+Esc)")
|
|
1806
|
+
self.menu_btn = btn("⋮", w - 34, "onMenu:", "Menu")
|
|
1807
|
+
|
|
1808
|
+
def relayout_views(self):
|
|
1809
|
+
w, h = self.root_view.bounds().size.width, self.root_view.bounds().size.height
|
|
1810
|
+
bm_visible = SETTINGS.settings.get("bookmarks_bar_visible", True)
|
|
1811
|
+
is_vert = SETTINGS.settings.get("vertical_tabs_enabled", False)
|
|
1812
|
+
vert_w = 48 if SETTINGS.settings.get("vertical_tabs_collapsed", False) else 220
|
|
1813
|
+
|
|
1814
|
+
if not self.tab_bar_visible:
|
|
1815
|
+
# Zen Mode: Title bar stays at the top to isolate traffic lights from web content
|
|
1816
|
+
self.title_bar.setHidden_(False)
|
|
1817
|
+
self.title_bar.setFrame_(Cocoa.NSMakeRect(0, h - TITLE_BAR_H, w, TITLE_BAR_H))
|
|
1818
|
+
self.tab_strip.setHidden_(True)
|
|
1819
|
+
self.toolbar.setHidden_(True)
|
|
1820
|
+
self.bookmarks_bar.setHidden_(True)
|
|
1821
|
+
self.web_container.setFrame_(Cocoa.NSMakeRect(0, 0, w, h - TITLE_BAR_H))
|
|
1822
|
+
elif is_vert:
|
|
1823
|
+
self.title_bar.setHidden_(False)
|
|
1824
|
+
self.title_bar.setFrame_(Cocoa.NSMakeRect(0, h - TITLE_BAR_H, w, TITLE_BAR_H))
|
|
1825
|
+
self.tab_strip.setHidden_(False)
|
|
1826
|
+
self.tab_strip.setFrame_(Cocoa.NSMakeRect(0, 0, vert_w, h - TITLE_BAR_H))
|
|
1827
|
+
cx, cw, y = vert_w, w - vert_w, h - TITLE_BAR_H - 34
|
|
1828
|
+
self.toolbar.setHidden_(False)
|
|
1829
|
+
self.toolbar.setFrame_(Cocoa.NSMakeRect(cx, y, cw, 34))
|
|
1830
|
+
if bm_visible:
|
|
1831
|
+
y -= 24
|
|
1832
|
+
self.bookmarks_bar.setHidden_(False)
|
|
1833
|
+
self.bookmarks_bar.setFrame_(Cocoa.NSMakeRect(cx, y, cw, 24))
|
|
1834
|
+
else:
|
|
1835
|
+
self.bookmarks_bar.setHidden_(True)
|
|
1836
|
+
self.web_container.setFrame_(Cocoa.NSMakeRect(cx, 0, cw, y))
|
|
1837
|
+
else:
|
|
1838
|
+
self.title_bar.setHidden_(False)
|
|
1839
|
+
self.title_bar.setFrame_(Cocoa.NSMakeRect(0, h - TITLE_BAR_H, w, TITLE_BAR_H))
|
|
1840
|
+
self.tab_strip.setHidden_(False)
|
|
1841
|
+
self.tab_strip.setFrame_(Cocoa.NSMakeRect(0, h - TITLE_BAR_H - 34, w, 34))
|
|
1842
|
+
self.toolbar.setHidden_(False)
|
|
1843
|
+
self.toolbar.setFrame_(Cocoa.NSMakeRect(0, h - TITLE_BAR_H - 68, w, 34))
|
|
1844
|
+
if bm_visible:
|
|
1845
|
+
self.bookmarks_bar.setFrame_(Cocoa.NSMakeRect(0, h - TITLE_BAR_H - 92, w, 24))
|
|
1846
|
+
self.bookmarks_bar.setHidden_(not bm_visible)
|
|
1847
|
+
top_h = TITLE_BAR_H + (92 if bm_visible else 68)
|
|
1848
|
+
self.web_container.setFrame_(Cocoa.NSMakeRect(0, 0, w, h - top_h))
|
|
1849
|
+
|
|
1850
|
+
show_home = SETTINGS.settings.get("show_home_button", False)
|
|
1851
|
+
if hasattr(self, 'home_btn'):
|
|
1852
|
+
self.home_btn.setHidden_(not show_home)
|
|
1853
|
+
omni_x = 132 if show_home else 102
|
|
1854
|
+
tb_w = self.toolbar.bounds().size.width
|
|
1855
|
+
|
|
1856
|
+
# Fixed: Expand Omnibox to fill empty space, eliminated gap between zen and menu buttons
|
|
1857
|
+
if hasattr(self, 'omnibox_container'):
|
|
1858
|
+
box_w = max(180, tb_w - omni_x - 162)
|
|
1859
|
+
self.omnibox_container.setFrame_(Cocoa.NSMakeRect(omni_x, 3, box_w, 28))
|
|
1860
|
+
self.omnibox.setFrame_(Cocoa.NSMakeRect(28, 6, box_w - 56, 16))
|
|
1861
|
+
self.star_btn.setFrameOrigin_(Cocoa.NSMakePoint(box_w - 26, 4))
|
|
1862
|
+
|
|
1863
|
+
if hasattr(self, 'pip_btn'): self.pip_btn.setFrameOrigin_(Cocoa.NSMakePoint(tb_w - 154, 4))
|
|
1864
|
+
if hasattr(self, 'dl_btn'): self.dl_btn.setFrameOrigin_(Cocoa.NSMakePoint(tb_w - 124, 4))
|
|
1865
|
+
if hasattr(self, 'share_btn'): self.share_btn.setFrameOrigin_(Cocoa.NSMakePoint(tb_w - 94, 4))
|
|
1866
|
+
if hasattr(self, 'zen_btn'): self.zen_btn.setFrameOrigin_(Cocoa.NSMakePoint(tb_w - 64, 4))
|
|
1867
|
+
if hasattr(self, 'menu_btn'): self.menu_btn.setFrameOrigin_(Cocoa.NSMakePoint(tb_w - 34, 4))
|
|
1868
|
+
if hasattr(self, 'progress_bar'): self.progress_bar.setFrame_(Cocoa.NSMakeRect(0, 0, tb_w, 2.5))
|
|
1869
|
+
if hasattr(self, 'find_bar'): self.find_bar.setFrameOrigin_(Cocoa.NSMakePoint(self.root_view.bounds().size.width - 260, self.web_container.bounds().size.height - 50))
|
|
1870
|
+
|
|
1871
|
+
wv = self.get_active_webview()
|
|
1872
|
+
if wv:
|
|
1873
|
+
wv.setFrame_(self.web_container.bounds())
|
|
1874
|
+
self.relayout_tabs()
|
|
1875
|
+
|
|
1876
|
+
def relayout_tabs(self):
|
|
1877
|
+
for sub in list(self.tab_strip.subviews()):
|
|
1878
|
+
sub.removeFromSuperview()
|
|
1879
|
+
w = self.tab_strip.bounds().size.width
|
|
1880
|
+
is_vert = SETTINGS.settings.get("vertical_tabs_enabled", False)
|
|
1881
|
+
# Because traffic lights sit in the title bar above, horizontal tabs start cleanly at x = 8.0
|
|
1882
|
+
x = 8.0
|
|
1883
|
+
tab_h = 28 if not is_vert else 32
|
|
1884
|
+
y = self.tab_strip.bounds().size.height - 38 - tab_h if is_vert else 3
|
|
1885
|
+
|
|
1886
|
+
vis = [t for t in self.tabs if not t.get("group_id") or not self.get_group(t["group_id"]).collapsed]
|
|
1887
|
+
pinned_cnt = sum(1 for t in vis if t.get("pinned", False))
|
|
1888
|
+
tab_w = max(60, min(240, (w - x - 44 - (pinned_cnt * 36)) / max(1, len(vis) - pinned_cnt))) if not is_vert else w
|
|
1889
|
+
|
|
1890
|
+
if is_vert:
|
|
1891
|
+
tb = Cocoa.NSButton.alloc().initWithFrame_(Cocoa.NSMakeRect(8, self.tab_strip.bounds().size.height - 32, 24, 24))
|
|
1892
|
+
tb.setTitle_("≡")
|
|
1893
|
+
tb.setBezelStyle_(Cocoa.NSBezelStyleCircular)
|
|
1894
|
+
tb.setTarget_(self)
|
|
1895
|
+
tb.setAction_("onToggleVerticalTabs:")
|
|
1896
|
+
self.tab_strip.addSubview_(tb)
|
|
1897
|
+
|
|
1898
|
+
curr_grp = None
|
|
1899
|
+
for i, tab in enumerate(self.tabs):
|
|
1900
|
+
if tab.get("group_id"):
|
|
1901
|
+
grp = self.get_group(tab["group_id"])
|
|
1902
|
+
if grp and curr_grp != grp.id:
|
|
1903
|
+
curr_grp = grp.id
|
|
1904
|
+
if is_vert:
|
|
1905
|
+
self.tab_strip.addSubview_(TabGroupHeaderView.alloc().initWithFrame_group_controller_(Cocoa.NSMakeRect(0, y, w, 28), grp, self))
|
|
1906
|
+
y -= 30
|
|
1907
|
+
else:
|
|
1908
|
+
self.tab_strip.addSubview_(TabGroupHeaderView.alloc().initWithFrame_group_controller_(Cocoa.NSMakeRect(x, 3, 60, 28), grp, self))
|
|
1909
|
+
x += 64
|
|
1910
|
+
if grp and grp.collapsed:
|
|
1911
|
+
continue
|
|
1912
|
+
|
|
1913
|
+
cur_w = 36 if tab.get("pinned", False) else tab_w
|
|
1914
|
+
if is_vert:
|
|
1915
|
+
tab_v = ChromeTabView.alloc().initWithFrame_controller_index_(Cocoa.NSMakeRect(0, y, w, tab_h), self, i)
|
|
1916
|
+
self.tab_strip.addSubview_(tab_v)
|
|
1917
|
+
y -= (tab_h + 2)
|
|
1918
|
+
else:
|
|
1919
|
+
tab_v = ChromeTabView.alloc().initWithFrame_controller_index_(Cocoa.NSMakeRect(x, 3, cur_w, tab_h), self, i)
|
|
1920
|
+
self.tab_strip.addSubview_(tab_v)
|
|
1921
|
+
x += cur_w + 2
|
|
1922
|
+
|
|
1923
|
+
btn_add = Cocoa.NSButton.alloc().initWithFrame_(Cocoa.NSMakeRect(8 if is_vert else x + 4, y if is_vert else 4, 26, 26))
|
|
1924
|
+
btn_add.setTitle_("+")
|
|
1925
|
+
btn_add.setBezelStyle_(Cocoa.NSBezelStyleCircular)
|
|
1926
|
+
btn_add.setTarget_(self)
|
|
1927
|
+
btn_add.setAction_("onNewTab:")
|
|
1928
|
+
self.tab_strip.addSubview_(btn_add)
|
|
1929
|
+
self.tab_strip.setNeedsDisplay_(True)
|
|
1930
|
+
|
|
1931
|
+
def onToggleVerticalTabs_(self, sender):
|
|
1932
|
+
SETTINGS.settings["vertical_tabs_collapsed"] = not SETTINGS.settings.get("vertical_tabs_collapsed", False)
|
|
1933
|
+
SETTINGS.save()
|
|
1934
|
+
self.relayout_views()
|
|
1935
|
+
|
|
1936
|
+
def add_tab(self, url, pinned=False):
|
|
1937
|
+
config = WebKit.WKWebViewConfiguration.alloc().init()
|
|
1938
|
+
if self.is_incognito:
|
|
1939
|
+
apply_incognito_preferences(config, shared_data_store=self.incognito_data_store)
|
|
1940
|
+
else:
|
|
1941
|
+
ds = WebKit.WKWebsiteDataStore.defaultDataStore()
|
|
1942
|
+
pcfg = create_nw_proxy_config("127.0.0.1", LOCAL_PROXY_PORT)
|
|
1943
|
+
if pcfg and hasattr(ds, "setProxyConfigurations_"):
|
|
1944
|
+
try:
|
|
1945
|
+
ds.setProxyConfigurations_([pcfg])
|
|
1946
|
+
except Exception:
|
|
1947
|
+
pass
|
|
1948
|
+
config.setWebsiteDataStore_(ds)
|
|
1949
|
+
try:
|
|
1950
|
+
config.preferences().setValue_forKey_(True, "developerExtrasEnabled")
|
|
1951
|
+
config.preferences().setValue_forKey_(True, "fullScreenEnabled")
|
|
1952
|
+
if hasattr(config.preferences(), "setJavaScriptCanOpenWindowsAutomatically_"):
|
|
1953
|
+
config.preferences().setJavaScriptCanOpenWindowsAutomatically_(not SETTINGS.settings.get("block_popups", True))
|
|
1954
|
+
if hasattr(config.preferences(), "setJavaScriptEnabled_"):
|
|
1955
|
+
config.preferences().setJavaScriptEnabled_(SETTINGS.settings.get("js_enabled", True))
|
|
1956
|
+
if hasattr(config.preferences(), "setElementFullscreenEnabled_"):
|
|
1957
|
+
config.preferences().setElementFullscreenEnabled_(True)
|
|
1958
|
+
except Exception:
|
|
1959
|
+
pass
|
|
1960
|
+
|
|
1961
|
+
config.setURLSchemeHandler_forURLScheme_(InternalSchemeHandler.alloc().init(), "browser")
|
|
1962
|
+
uc = WebKit.WKUserContentController.alloc().init()
|
|
1963
|
+
uc.addScriptMessageHandler_name_(self, "bridge")
|
|
1964
|
+
uc.addScriptMessageHandler_name_(self, "audio")
|
|
1965
|
+
uc.addScriptMessageHandler_name_(self, "terminalInput")
|
|
1966
|
+
uc.addScriptMessageHandler_name_(self, "terminalResize")
|
|
1967
|
+
|
|
1968
|
+
uc.addUserScript_(WebKit.WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly_(AUDIO_MONITOR_JS, WebKit.WKUserScriptInjectionTimeAtDocumentEnd, False))
|
|
1969
|
+
uc.addUserScript_(WebKit.WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly_(IMAGE_CONTEXT_MENU_JS, WebKit.WKUserScriptInjectionTimeAtDocumentStart, False))
|
|
1970
|
+
uc.addUserScript_(WebKit.WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly_(KEYCHAIN_AUTOSAVE_JS, WebKit.WKUserScriptInjectionTimeAtDocumentEnd, False))
|
|
1971
|
+
uc.addUserScript_(WebKit.WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly_(IN_WINDOW_FULLSCREEN_JS, WebKit.WKUserScriptInjectionTimeAtDocumentEnd, False))
|
|
1972
|
+
|
|
1973
|
+
if SETTINGS.settings.get("cosmetic_adblock_enabled", True):
|
|
1974
|
+
cosmetic_js = "(function() { const st = document.createElement('style'); st.id = '__pybrowser_cosmetic_adblock'; st.textContent = `" + COSMETIC_ADBLOCK_CSS + "`; (document.head || document.documentElement).appendChild(st); })();"
|
|
1975
|
+
uc.addUserScript_(WebKit.WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly_(cosmetic_js, WebKit.WKUserScriptInjectionTimeAtDocumentStart, False))
|
|
1976
|
+
|
|
1977
|
+
if SETTINGS.settings.get("yt_adblock_enabled", True):
|
|
1978
|
+
uc.addUserScript_(WebKit.WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly_(YOUTUBE_AD_SHIELD_JS, WebKit.WKUserScriptInjectionTimeAtDocumentEnd, False))
|
|
1979
|
+
|
|
1980
|
+
for script in get_installed_userscripts():
|
|
1981
|
+
if not script["enabled"]:
|
|
1982
|
+
continue
|
|
1983
|
+
matches = script.get("match", [])
|
|
1984
|
+
conds = []
|
|
1985
|
+
for m in matches:
|
|
1986
|
+
if m == "<all_urls>":
|
|
1987
|
+
conds.append("true")
|
|
1988
|
+
else:
|
|
1989
|
+
escaped_slash = r"\/"
|
|
1990
|
+
reg = m.replace("*", ".*").replace("/", escaped_slash)
|
|
1991
|
+
conds.append(f"new RegExp('^{reg}$').test(location.href)")
|
|
1992
|
+
cond_str = " || ".join(conds) if conds else "true"
|
|
1993
|
+
wrapped = f"if ({cond_str}) {{\n" + script["code"] + "\n}}"
|
|
1994
|
+
uc.addUserScript_(WebKit.WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly_(wrapped, WebKit.WKUserScriptInjectionTimeAtDocumentEnd, False))
|
|
1995
|
+
|
|
1996
|
+
if WK_RULE_LIST and SETTINGS.settings.get("adblock_enabled", True):
|
|
1997
|
+
uc.addContentRuleList_(WK_RULE_LIST)
|
|
1998
|
+
config.setUserContentController_(uc)
|
|
1999
|
+
|
|
2000
|
+
wv = BrowserWebView.alloc().initWithFrame_configuration_(self.web_container.bounds(), config)
|
|
2001
|
+
wv.controller = self
|
|
2002
|
+
wv.setAutoresizingMask_(Cocoa.NSViewWidthSizable | Cocoa.NSViewHeightSizable)
|
|
2003
|
+
wv.setNavigationDelegate_(self)
|
|
2004
|
+
wv.setUIDelegate_(self)
|
|
2005
|
+
wv.setCustomUserAgent_(SETTINGS.get_user_agent())
|
|
2006
|
+
if hasattr(wv, "setInspectable_"):
|
|
2007
|
+
try:
|
|
2008
|
+
wv.setInspectable_(True)
|
|
2009
|
+
except Exception:
|
|
2010
|
+
pass
|
|
2011
|
+
|
|
2012
|
+
initial_zoom = float(SETTINGS.settings.get("default_zoom", 0.8))
|
|
2013
|
+
tab = {
|
|
2014
|
+
"id": str(uuid.uuid4()), "webview": wv, "url": url, "title": "New Tab",
|
|
2015
|
+
"favicon": None, "pinned": pinned, "group_id": None, "zoom": initial_zoom,
|
|
2016
|
+
"is_sleeping": False, "has_audio": False, "is_muted": False, "last_active_timestamp": time.time()
|
|
2017
|
+
}
|
|
2018
|
+
self.apply_zoom_to_webview(wv, tab)
|
|
2019
|
+
for k in ("URL", "title", "estimatedProgress"):
|
|
2020
|
+
try:
|
|
2021
|
+
wv.addObserver_forKeyPath_options_context_(self, k, Cocoa.NSKeyValueObservingOptionNew, None)
|
|
2022
|
+
except Exception:
|
|
2023
|
+
pass
|
|
2024
|
+
|
|
2025
|
+
self.tabs.append(tab)
|
|
2026
|
+
self.relayout_tabs()
|
|
2027
|
+
self.switch_to_tab(len(self.tabs) - 1)
|
|
2028
|
+
req_url = url
|
|
2029
|
+
if SETTINGS.settings.get("https_only", True) and req_url.startswith("http://") and not req_url.startswith("http://localhost") and not req_url.startswith("http://127.0.0.1"):
|
|
2030
|
+
req_url = "https://" + req_url[7:]
|
|
2031
|
+
wv.loadRequest_(Foundation.NSURLRequest.requestWithURL_(Foundation.NSURL.URLWithString_(req_url)))
|
|
2032
|
+
self.save_session()
|
|
2033
|
+
|
|
2034
|
+
def get_tab_index_by_id(self, tab_id):
|
|
2035
|
+
for i, t in enumerate(self.tabs):
|
|
2036
|
+
if t.get("id") == tab_id:
|
|
2037
|
+
return i
|
|
2038
|
+
return None
|
|
2039
|
+
|
|
2040
|
+
def switch_to_tab(self, index):
|
|
2041
|
+
if not (0 <= index < len(self.tabs)):
|
|
2042
|
+
return
|
|
2043
|
+
self.active_tab_index = index
|
|
2044
|
+
self.omnibox_panel.hide()
|
|
2045
|
+
|
|
2046
|
+
tab = self.tabs[index]
|
|
2047
|
+
tab["last_active_timestamp"] = time.time()
|
|
2048
|
+
if tab.get("is_sleeping"):
|
|
2049
|
+
self.wake_tab(index)
|
|
2050
|
+
wv = tab["webview"]
|
|
2051
|
+
|
|
2052
|
+
# Fixed: Keep webviews attached and toggle visibility with setHidden_
|
|
2053
|
+
# This completely preserves DOM focus, text inputs, and render state
|
|
2054
|
+
for i, t in enumerate(self.tabs):
|
|
2055
|
+
twv = t.get("webview")
|
|
2056
|
+
if twv:
|
|
2057
|
+
is_current = (i == index)
|
|
2058
|
+
if twv.superview() != self.web_container and is_current:
|
|
2059
|
+
self.web_container.addSubview_(twv)
|
|
2060
|
+
twv.setHidden_(not is_current)
|
|
2061
|
+
|
|
2062
|
+
wv.setFrame_(self.web_container.bounds())
|
|
2063
|
+
self.omnibox.set_url(tab.get("url", ""))
|
|
2064
|
+
self.apply_zoom_to_webview(wv, tab)
|
|
2065
|
+
self.update_star_status()
|
|
2066
|
+
self.update_progress_for_webview(wv)
|
|
2067
|
+
self.relayout_tabs()
|
|
2068
|
+
|
|
2069
|
+
# Fixed: Restore keyboard focus and text cursor to the active input element automatically
|
|
2070
|
+
self.window().makeFirstResponder_(wv)
|
|
2071
|
+
wv.evaluateJavaScript_completionHandler_(
|
|
2072
|
+
"if (document.activeElement && typeof document.activeElement.focus === 'function') { document.activeElement.focus(); }",
|
|
2073
|
+
None
|
|
2074
|
+
)
|
|
2075
|
+
|
|
2076
|
+
def close_tab(self, index):
|
|
2077
|
+
if 0 <= index < len(self.tabs):
|
|
2078
|
+
tab = self.tabs.pop(index)
|
|
2079
|
+
if tab.get("url") and not tab["url"].startswith("browser://"):
|
|
2080
|
+
self.closed_tabs.append(tab["url"])
|
|
2081
|
+
wv = tab.get("webview")
|
|
2082
|
+
if wv:
|
|
2083
|
+
TERMINAL_MANAGER.close_for_webview(wv)
|
|
2084
|
+
for k in ("URL", "title", "estimatedProgress"):
|
|
2085
|
+
try:
|
|
2086
|
+
wv.removeObserver_forKeyPath_(self, k)
|
|
2087
|
+
except Exception:
|
|
2088
|
+
pass
|
|
2089
|
+
if wv.superview():
|
|
2090
|
+
wv.removeFromSuperview()
|
|
2091
|
+
if not self.tabs:
|
|
2092
|
+
self.window().close()
|
|
2093
|
+
else:
|
|
2094
|
+
if index < self.active_tab_index:
|
|
2095
|
+
self.active_tab_index -= 1
|
|
2096
|
+
self.relayout_tabs()
|
|
2097
|
+
elif index == self.active_tab_index:
|
|
2098
|
+
self.switch_to_tab(max(0, min(index, len(self.tabs) - 1)))
|
|
2099
|
+
else:
|
|
2100
|
+
self.relayout_tabs()
|
|
2101
|
+
self.save_session()
|
|
2102
|
+
|
|
2103
|
+
def reopen_last_closed_tab(self):
|
|
2104
|
+
if self.closed_tabs:
|
|
2105
|
+
self.add_tab(self.closed_tabs.pop())
|
|
2106
|
+
|
|
2107
|
+
def toggle_pin_tab(self, index):
|
|
2108
|
+
if 0 <= index < len(self.tabs):
|
|
2109
|
+
self.tabs[index]["pinned"] = not self.tabs[index].get("pinned", False)
|
|
2110
|
+
self.relayout_tabs()
|
|
2111
|
+
self.save_session()
|
|
2112
|
+
|
|
2113
|
+
def duplicate_tab(self, index):
|
|
2114
|
+
if 0 <= index < len(self.tabs) and self.tabs[index].get("url"):
|
|
2115
|
+
self.add_tab(self.tabs[index]["url"])
|
|
2116
|
+
|
|
2117
|
+
def toggle_mute(self, index):
|
|
2118
|
+
if 0 <= index < len(self.tabs):
|
|
2119
|
+
tab = self.tabs[index]
|
|
2120
|
+
tab["is_muted"] = not tab.get("is_muted", False)
|
|
2121
|
+
wv = tab.get("webview")
|
|
2122
|
+
if wv:
|
|
2123
|
+
if hasattr(wv, "setMediaMuted_"):
|
|
2124
|
+
wv.setMediaMuted_(tab["is_muted"])
|
|
2125
|
+
else:
|
|
2126
|
+
wv.evaluateJavaScript_completionHandler_(f"document.querySelectorAll('audio, video').forEach(el => el.muted = {'true' if tab['is_muted'] else 'false'});", None)
|
|
2127
|
+
self.relayout_tabs()
|
|
2128
|
+
|
|
2129
|
+
def create_group_for_tab(self, index):
|
|
2130
|
+
if 0 <= index < len(self.tabs):
|
|
2131
|
+
gid = str(uuid.uuid4())
|
|
2132
|
+
self.groups.append(TabGroup(gid, "Group", "Blue", False))
|
|
2133
|
+
self.tabs[index]["group_id"] = gid
|
|
2134
|
+
self.relayout_tabs()
|
|
2135
|
+
self.save_session()
|
|
2136
|
+
|
|
2137
|
+
def get_group(self, gid):
|
|
2138
|
+
return next((g for g in self.groups if g.id == gid), None)
|
|
2139
|
+
|
|
2140
|
+
def check_sleeping_tabs(self):
|
|
2141
|
+
now = time.time()
|
|
2142
|
+
for i, tab in enumerate(self.tabs):
|
|
2143
|
+
if i == self.active_tab_index or tab.get("has_audio") or tab.get("is_sleeping"):
|
|
2144
|
+
continue
|
|
2145
|
+
if now - tab.get("last_active_timestamp", now) > 1800:
|
|
2146
|
+
self.sleep_tab(i)
|
|
2147
|
+
|
|
2148
|
+
def sleep_tab(self, index):
|
|
2149
|
+
if not (0 <= index < len(self.tabs)) or index == self.active_tab_index:
|
|
2150
|
+
return
|
|
2151
|
+
tab = self.tabs[index]
|
|
2152
|
+
if tab.get("is_sleeping"):
|
|
2153
|
+
return
|
|
2154
|
+
wv = tab.get("webview")
|
|
2155
|
+
if wv:
|
|
2156
|
+
if wv.URL() and not wv.URL().absoluteString().startswith("about:"):
|
|
2157
|
+
tab["url"] = wv.URL().absoluteString()
|
|
2158
|
+
tab["is_sleeping"] = True
|
|
2159
|
+
wv.loadRequest_(Foundation.NSURLRequest.requestWithURL_(Foundation.NSURL.URLWithString_("about:blank")))
|
|
2160
|
+
else:
|
|
2161
|
+
tab["is_sleeping"] = True
|
|
2162
|
+
self.relayout_tabs()
|
|
2163
|
+
|
|
2164
|
+
def wake_tab(self, index):
|
|
2165
|
+
if not (0 <= index < len(self.tabs)):
|
|
2166
|
+
return
|
|
2167
|
+
tab = self.tabs[index]
|
|
2168
|
+
if not tab.get("is_sleeping"):
|
|
2169
|
+
return
|
|
2170
|
+
tab["is_sleeping"] = False
|
|
2171
|
+
wv = tab.get("webview")
|
|
2172
|
+
if wv and tab.get("url"):
|
|
2173
|
+
wv.loadRequest_(Foundation.NSURLRequest.requestWithURL_(Foundation.NSURL.URLWithString_(tab["url"])))
|
|
2174
|
+
self.relayout_tabs()
|
|
2175
|
+
|
|
2176
|
+
def is_url_bookmarked(self, url):
|
|
2177
|
+
if not url:
|
|
2178
|
+
return False
|
|
2179
|
+
try:
|
|
2180
|
+
with open(BOOKMARKS_FILE, "r", encoding="utf-8") as f:
|
|
2181
|
+
bms = json.load(f)
|
|
2182
|
+
if isinstance(bms, list):
|
|
2183
|
+
return any(isinstance(b, dict) and b.get("url") == url for b in bms)
|
|
2184
|
+
except Exception:
|
|
2185
|
+
return False
|
|
2186
|
+
return False
|
|
2187
|
+
|
|
2188
|
+
def update_star_status(self):
|
|
2189
|
+
wv = self.get_active_webview()
|
|
2190
|
+
url = wv.URL().absoluteString() if wv and wv.URL() else ""
|
|
2191
|
+
if hasattr(self, 'star_btn'):
|
|
2192
|
+
self.star_btn.setTitle_("★" if self.is_url_bookmarked(url) else "☆")
|
|
2193
|
+
|
|
2194
|
+
def onBookmarkCurrentPage_(self, sender=None):
|
|
2195
|
+
wv = self.get_active_webview()
|
|
2196
|
+
if not wv or not wv.URL():
|
|
2197
|
+
return
|
|
2198
|
+
url = wv.URL().absoluteString()
|
|
2199
|
+
if not url or url.startswith("browser://"):
|
|
2200
|
+
return
|
|
2201
|
+
title = str(wv.title()) if wv.title() else url
|
|
2202
|
+
|
|
2203
|
+
try:
|
|
2204
|
+
with open(BOOKMARKS_FILE, "r", encoding="utf-8") as f:
|
|
2205
|
+
bms = json.load(f)
|
|
2206
|
+
if not isinstance(bms, list):
|
|
2207
|
+
bms = []
|
|
2208
|
+
except Exception:
|
|
2209
|
+
bms = []
|
|
2210
|
+
|
|
2211
|
+
if self.is_url_bookmarked(url):
|
|
2212
|
+
bms = [b for b in bms if isinstance(b, dict) and b.get("url") != url]
|
|
2213
|
+
else:
|
|
2214
|
+
bms.append({"title": title, "url": url})
|
|
2215
|
+
|
|
2216
|
+
atomic_write_json(BOOKMARKS_FILE, bms)
|
|
2217
|
+
self.bookmarks_bar.reload_bookmarks()
|
|
2218
|
+
self.update_star_status()
|
|
2219
|
+
|
|
2220
|
+
def perform_download_image(self, img_url):
|
|
2221
|
+
if not SETTINGS.settings.get("downloads_enabled", True) or not img_url:
|
|
2222
|
+
return
|
|
2223
|
+
wv = self.get_active_webview()
|
|
2224
|
+
suggested = sanitize_suggested_filename(img_url)
|
|
2225
|
+
dest_dir = validate_and_get_download_dir()
|
|
2226
|
+
dest_path = os.path.join(dest_dir, suggested)
|
|
2227
|
+
base, ext = os.path.splitext(dest_path)
|
|
2228
|
+
counter = 1
|
|
2229
|
+
while os.path.exists(dest_path):
|
|
2230
|
+
dest_path = f"{base} ({counter}){ext}"
|
|
2231
|
+
counter += 1
|
|
2232
|
+
|
|
2233
|
+
dl_id = self.register_download(os.path.basename(dest_path), dest_path, None)
|
|
2234
|
+
download_image_data(img_url, dest_path, webview=wv, completion_callback=lambda ok: self.finish_download(None, "Completed" if ok else "Failed", dl_id=dl_id))
|
|
2235
|
+
|
|
2236
|
+
def perform_save_image_as(self, img_url):
|
|
2237
|
+
if not SETTINGS.settings.get("downloads_enabled", True) or not img_url:
|
|
2238
|
+
return
|
|
2239
|
+
wv = self.get_active_webview()
|
|
2240
|
+
suggested = sanitize_suggested_filename(img_url)
|
|
2241
|
+
default_dir = validate_and_get_download_dir()
|
|
2242
|
+
|
|
2243
|
+
panel = Cocoa.NSSavePanel.savePanel()
|
|
2244
|
+
panel.setNameFieldStringValue_(suggested)
|
|
2245
|
+
panel.setDirectoryURL_(Foundation.NSURL.fileURLWithPath_(default_dir))
|
|
2246
|
+
if panel.runModal() == Cocoa.NSModalResponseOK and panel.URL():
|
|
2247
|
+
dest_path = panel.URL().path()
|
|
2248
|
+
dl_id = self.register_download(os.path.basename(dest_path), dest_path, None)
|
|
2249
|
+
download_image_data(img_url, dest_path, webview=wv, completion_callback=lambda ok: self.finish_download(None, "Completed" if ok else "Failed", dl_id=dl_id))
|
|
2250
|
+
|
|
2251
|
+
def perform_copy_image_address(self, img_url):
|
|
2252
|
+
if not img_url:
|
|
2253
|
+
return
|
|
2254
|
+
pb = Cocoa.NSPasteboard.generalPasteboard()
|
|
2255
|
+
pb.clearContents()
|
|
2256
|
+
pb.setString_forType_(img_url, Cocoa.NSPasteboardTypeString)
|
|
2257
|
+
|
|
2258
|
+
def apply_zoom_to_webview(self, wv, tab=None):
|
|
2259
|
+
if not wv:
|
|
2260
|
+
return
|
|
2261
|
+
target_zoom = tab.get("zoom", float(SETTINGS.settings.get("default_zoom", 0.8))) if tab else float(SETTINGS.settings.get("default_zoom", 0.8))
|
|
2262
|
+
target_zoom = max(0.5, min(2.0, target_zoom))
|
|
2263
|
+
try:
|
|
2264
|
+
if hasattr(wv, "setAllowsMagnification_"): wv.setAllowsMagnification_(False)
|
|
2265
|
+
if hasattr(wv, "setMagnification_"): wv.setMagnification_(1.0)
|
|
2266
|
+
wv.setPageZoom_(target_zoom)
|
|
2267
|
+
except Exception:
|
|
2268
|
+
pass
|
|
2269
|
+
|
|
2270
|
+
def restore_normal_zoom(self):
|
|
2271
|
+
wv = self.get_active_webview()
|
|
2272
|
+
if wv:
|
|
2273
|
+
self.apply_zoom_to_webview(wv)
|
|
2274
|
+
wv.evaluateJavaScript_completionHandler_(REFLOW_RESTORE_JS, None)
|
|
2275
|
+
|
|
2276
|
+
def handle_fullscreen_enter(self, auto_hide_bars=True):
|
|
2277
|
+
if auto_hide_bars:
|
|
2278
|
+
if not self.tab_bar_visible:
|
|
2279
|
+
# Tab was already in Zen mode before entering fullscreen
|
|
2280
|
+
self.zen_auto_entered = False
|
|
2281
|
+
else:
|
|
2282
|
+
# Tab was in normal mode, auto-enter Zen mode for fullscreen
|
|
2283
|
+
self.zen_auto_entered = True
|
|
2284
|
+
self.tab_bar_visible = False
|
|
2285
|
+
self.relayout_views()
|
|
2286
|
+
wv = self.get_active_webview()
|
|
2287
|
+
if wv:
|
|
2288
|
+
try:
|
|
2289
|
+
wv.setFrame_(self.web_container.bounds())
|
|
2290
|
+
wv.evaluateJavaScript_completionHandler_("if (window.setTerminalFullscreen) window.setTerminalFullscreen(true);", None)
|
|
2291
|
+
except Exception:
|
|
2292
|
+
pass
|
|
2293
|
+
|
|
2294
|
+
def handle_fullscreen_exit(self, restore_bars=True):
|
|
2295
|
+
if restore_bars:
|
|
2296
|
+
# Fixed: Only exit Zen mode if it was entered automatically by fullscreen
|
|
2297
|
+
if self.zen_auto_entered:
|
|
2298
|
+
self.tab_bar_visible = True
|
|
2299
|
+
self.zen_auto_entered = False
|
|
2300
|
+
self.relayout_views()
|
|
2301
|
+
# If tab was already in Zen mode before fullscreen, keep Zen mode on!
|
|
2302
|
+
wv = self.get_active_webview()
|
|
2303
|
+
if wv:
|
|
2304
|
+
wv.evaluateJavaScript_completionHandler_("if (window.setTerminalFullscreen) window.setTerminalFullscreen(false);", None)
|
|
2305
|
+
self.restore_normal_zoom()
|
|
2306
|
+
|
|
2307
|
+
def handle_escape_key(self):
|
|
2308
|
+
if self.find_bar and not self.find_bar.isHidden():
|
|
2309
|
+
self.find_bar.setHidden_(True)
|
|
2310
|
+
return
|
|
2311
|
+
wv = self.get_active_webview()
|
|
2312
|
+
if not self.tab_bar_visible:
|
|
2313
|
+
self.handle_fullscreen_exit(restore_bars=True)
|
|
2314
|
+
return
|
|
2315
|
+
if wv:
|
|
2316
|
+
def on_done(res, err):
|
|
2317
|
+
if res or not self.tab_bar_visible:
|
|
2318
|
+
self.handle_fullscreen_exit(restore_bars=True)
|
|
2319
|
+
else:
|
|
2320
|
+
self.restore_normal_zoom()
|
|
2321
|
+
wv.evaluateJavaScript_completionHandler_(ESCAPE_CHECK_JS, on_done)
|
|
2322
|
+
else:
|
|
2323
|
+
if not self.tab_bar_visible:
|
|
2324
|
+
self.handle_fullscreen_exit(restore_bars=True)
|
|
2325
|
+
else:
|
|
2326
|
+
self.restore_normal_zoom()
|
|
2327
|
+
|
|
2328
|
+
def toggle_tab_bar(self):
|
|
2329
|
+
self.zen_auto_entered = False
|
|
2330
|
+
self.tab_bar_visible = not self.tab_bar_visible
|
|
2331
|
+
self.relayout_views()
|
|
2332
|
+
wv = self.get_active_webview()
|
|
2333
|
+
if wv:
|
|
2334
|
+
is_fs = not self.tab_bar_visible
|
|
2335
|
+
wv.evaluateJavaScript_completionHandler_(f"if (window.setTerminalFullscreen) window.setTerminalFullscreen({'true' if is_fs else 'false'});", None)
|
|
2336
|
+
|
|
2337
|
+
def onToggleZenMode_(self, sender=None): self.toggle_tab_bar()
|
|
2338
|
+
|
|
2339
|
+
def onZoomIn_(self, sender=None):
|
|
2340
|
+
if 0 <= self.active_tab_index < len(self.tabs):
|
|
2341
|
+
tab = self.tabs[self.active_tab_index]
|
|
2342
|
+
z = min(2.0, round(tab.get("zoom", 0.8) + 0.1, 2))
|
|
2343
|
+
tab["zoom"] = z
|
|
2344
|
+
self.apply_zoom_to_webview(tab.get("webview"), tab)
|
|
2345
|
+
|
|
2346
|
+
def onZoomOut_(self, sender=None):
|
|
2347
|
+
if 0 <= self.active_tab_index < len(self.tabs):
|
|
2348
|
+
tab = self.tabs[self.active_tab_index]
|
|
2349
|
+
z = max(0.5, round(tab.get("zoom", 0.8) - 0.1, 2))
|
|
2350
|
+
tab["zoom"] = z
|
|
2351
|
+
self.apply_zoom_to_webview(tab.get("webview"), tab)
|
|
2352
|
+
|
|
2353
|
+
def onResetZoom_(self, sender=None):
|
|
2354
|
+
if 0 <= self.active_tab_index < len(self.tabs):
|
|
2355
|
+
tab = self.tabs[self.active_tab_index]
|
|
2356
|
+
tab["zoom"] = float(SETTINGS.settings.get("default_zoom", 0.8))
|
|
2357
|
+
self.apply_zoom_to_webview(tab.get("webview"), tab)
|
|
2358
|
+
self.restore_normal_zoom()
|
|
2359
|
+
|
|
2360
|
+
def onInspectElement_(self, sender=None):
|
|
2361
|
+
wv = self.get_active_webview()
|
|
2362
|
+
if wv:
|
|
2363
|
+
try:
|
|
2364
|
+
Cocoa.NSApp.sendAction_to_from_(Cocoa.NSSelectorFromString("_showInspector:"), wv, None)
|
|
2365
|
+
except Exception:
|
|
2366
|
+
pass
|
|
2367
|
+
|
|
2368
|
+
def onMenu_(self, sender):
|
|
2369
|
+
menu = Cocoa.NSMenu.alloc().init()
|
|
2370
|
+
menu.addItemWithTitle_action_keyEquivalent_("New Tab", "onNewTab:", "t").setTarget_(self)
|
|
2371
|
+
menu.addItemWithTitle_action_keyEquivalent_("New Window", "onNewWindow:", "n").setTarget_(Cocoa.NSApp.delegate())
|
|
2372
|
+
inc_item = menu.addItemWithTitle_action_keyEquivalent_("New Incognito Window", "onNewIncognitoWindow:", "N")
|
|
2373
|
+
inc_item.setKeyEquivalentModifierMask_(Cocoa.NSEventModifierFlagCommand | Cocoa.NSEventModifierFlagShift)
|
|
2374
|
+
inc_item.setTarget_(Cocoa.NSApp.delegate())
|
|
2375
|
+
menu.addItemWithTitle_action_keyEquivalent_("New Terminal Tab", "openTerminal:", "T").setTarget_(self)
|
|
2376
|
+
menu.addItem_(Cocoa.NSMenuItem.separatorItem())
|
|
2377
|
+
menu.addItemWithTitle_action_keyEquivalent_("Bookmark Current Page", "onBookmarkCurrentPage:", "d").setTarget_(self)
|
|
2378
|
+
bm_text = "Hide Bookmarks Bar" if SETTINGS.settings.get("bookmarks_bar_visible", True) else "Show Bookmarks Bar"
|
|
2379
|
+
menu.addItemWithTitle_action_keyEquivalent_(bm_text, "toggleBookmarksBar:", "b").setTarget_(self)
|
|
2380
|
+
menu.addItemWithTitle_action_keyEquivalent_("Toggle Zen Mode", "onToggleZenMode:", "").setTarget_(self)
|
|
2381
|
+
menu.addItemWithTitle_action_keyEquivalent_("Find in Page", "onFindInPage:", "f").setTarget_(self)
|
|
2382
|
+
menu.addItem_(Cocoa.NSMenuItem.separatorItem())
|
|
2383
|
+
menu.addItemWithTitle_action_keyEquivalent_("Settings", "openSettings:", ",").setTarget_(self)
|
|
2384
|
+
menu.addItemWithTitle_action_keyEquivalent_("History", "openHistory:", "y").setTarget_(self)
|
|
2385
|
+
menu.addItemWithTitle_action_keyEquivalent_("Bookmarks", "openBookmarks:", "O").setTarget_(self)
|
|
2386
|
+
menu.addItemWithTitle_action_keyEquivalent_("Downloads", "openDownloads:", "D").setTarget_(self)
|
|
2387
|
+
menu.addItemWithTitle_action_keyEquivalent_("Extensions", "openExtensions:", "e").setTarget_(self)
|
|
2388
|
+
menu.addItemWithTitle_action_keyEquivalent_("Task Manager", "showTaskManager:", "").setTarget_(Cocoa.NSApp.delegate())
|
|
2389
|
+
menu.addItem_(Cocoa.NSMenuItem.separatorItem())
|
|
2390
|
+
menu.addItemWithTitle_action_keyEquivalent_("Inspect Element", "onInspectElement:", "i").setTarget_(self)
|
|
2391
|
+
menu.addItem_(Cocoa.NSMenuItem.separatorItem())
|
|
2392
|
+
menu.addItemWithTitle_action_keyEquivalent_("Quit PyBrowser", "terminate:", "q").setTarget_(Cocoa.NSApp)
|
|
2393
|
+
menu.popUpMenuPositioningItem_atLocation_inView_(None, Cocoa.NSMakePoint(0, 0), sender)
|
|
2394
|
+
|
|
2395
|
+
def toggleBookmarksBar_(self, sender=None):
|
|
2396
|
+
SETTINGS.settings["bookmarks_bar_visible"] = not SETTINGS.settings.get("bookmarks_bar_visible", True)
|
|
2397
|
+
SETTINGS.save()
|
|
2398
|
+
self.relayout_views()
|
|
2399
|
+
|
|
2400
|
+
def openTerminal_(self, s=None): self.add_tab("browser://terminal")
|
|
2401
|
+
def openSettings_(self, s=None): self.add_tab("browser://settings")
|
|
2402
|
+
def openHistory_(self, s=None): self.add_tab("browser://history")
|
|
2403
|
+
def openBookmarks_(self, s=None): self.add_tab("browser://bookmarks")
|
|
2404
|
+
def openDownloads_(self, s=None): self.add_tab("browser://downloads")
|
|
2405
|
+
def openExtensions_(self, s=None): self.add_tab("browser://extensions")
|
|
2406
|
+
def onNewTab_(self, s=None): self.add_tab(DEFAULT_URL)
|
|
2407
|
+
def onCloseTab_(self, s=None): self.close_tab(self.active_tab_index)
|
|
2408
|
+
def onReopenTab_(self, s=None): self.reopen_last_closed_tab()
|
|
2409
|
+
|
|
2410
|
+
def onGoBack_(self, s=None):
|
|
2411
|
+
wv = self.get_active_webview()
|
|
2412
|
+
if wv and wv.canGoBack(): wv.goBack_(s)
|
|
2413
|
+
|
|
2414
|
+
def onGoForward_(self, s=None):
|
|
2415
|
+
wv = self.get_active_webview()
|
|
2416
|
+
if wv and wv.canGoForward(): wv.goForward_(s)
|
|
2417
|
+
|
|
2418
|
+
def onReload_(self, s=None):
|
|
2419
|
+
wv = self.get_active_webview()
|
|
2420
|
+
if wv: wv.reload()
|
|
2421
|
+
|
|
2422
|
+
def onGoHome_(self, s=None):
|
|
2423
|
+
url = SETTINGS.settings.get("home_url", "https://www.google.com")
|
|
2424
|
+
wv = self.get_active_webview()
|
|
2425
|
+
if wv: wv.loadRequest_(Foundation.NSURLRequest.requestWithURL_(Foundation.NSURL.URLWithString_(url)))
|
|
2426
|
+
|
|
2427
|
+
def onFocusOmnibox_(self, s=None): self.window().makeFirstResponder_(self.omnibox)
|
|
2428
|
+
def onFindInPage_(self, s=None):
|
|
2429
|
+
self.find_bar.setHidden_(False)
|
|
2430
|
+
self.window().makeFirstResponder_(self.find_bar.search_field)
|
|
2431
|
+
|
|
2432
|
+
def onTogglePiP_(self, s=None):
|
|
2433
|
+
Cocoa.NSApp.delegate().pip_panel.browser = self
|
|
2434
|
+
Cocoa.NSApp.delegate().pip_panel.toggle()
|
|
2435
|
+
wv = self.get_active_webview()
|
|
2436
|
+
if wv:
|
|
2437
|
+
wv.evaluateJavaScript_completionHandler_("const v = document.querySelector('video'); if (v && v.webkitSetPresentationMode) v.webkitSetPresentationMode(v.webkitPresentationMode === 'picture-in-picture' ? 'inline' : 'picture-in-picture');", None)
|
|
2438
|
+
|
|
2439
|
+
def onShareCurrentPage_(self, sender):
|
|
2440
|
+
wv = self.get_active_webview()
|
|
2441
|
+
if wv and wv.URL():
|
|
2442
|
+
Cocoa.NSSharingServicePicker.alloc().initWithItems_([wv.URL()]).showRelativeToRect_ofView_preferredEdge_(sender.bounds(), sender, Cocoa.NSMinYEdge)
|
|
2443
|
+
|
|
2444
|
+
def onShowDownloads_(self, sender):
|
|
2445
|
+
menu = Cocoa.NSMenu.alloc().init()
|
|
2446
|
+
if not self.downloads:
|
|
2447
|
+
menu.addItemWithTitle_action_keyEquivalent_("No Recent Downloads", "", "").setEnabled_(False)
|
|
2448
|
+
else:
|
|
2449
|
+
for dl in reversed(self.downloads[-10:]):
|
|
2450
|
+
item = menu.addItemWithTitle_action_keyEquivalent_(f"{dl['name']} ({dl['status']})", "revealDownload:", "")
|
|
2451
|
+
item.setRepresentedObject_(dl['path'])
|
|
2452
|
+
item.setTarget_(self)
|
|
2453
|
+
menu.addItem_(Cocoa.NSMenuItem.separatorItem())
|
|
2454
|
+
menu.addItemWithTitle_action_keyEquivalent_("Open Downloads Manager", "openDownloads:", "").setTarget_(self)
|
|
2455
|
+
menu.popUpMenuPositioningItem_atLocation_inView_(None, Cocoa.NSMakePoint(0, 0), sender)
|
|
2456
|
+
|
|
2457
|
+
def revealDownload_(self, sender):
|
|
2458
|
+
path = sender.representedObject()
|
|
2459
|
+
if path and os.path.exists(path):
|
|
2460
|
+
Cocoa.NSWorkspace.sharedWorkspace().selectFile_inFileViewerRootedAtPath_(path, None)
|
|
2461
|
+
|
|
2462
|
+
def onSiteInfo_(self, sender):
|
|
2463
|
+
wv = self.get_active_webview()
|
|
2464
|
+
if not wv: return
|
|
2465
|
+
url = wv.URL().absoluteString() if wv.URL() else "browser://newtab"
|
|
2466
|
+
parsed = Foundation.NSURL.URLWithString_(url)
|
|
2467
|
+
menu = Cocoa.NSMenu.alloc().init()
|
|
2468
|
+
menu.addItemWithTitle_action_keyEquivalent_(f"Host: {parsed.host() or 'Internal Page'}", "", "").setEnabled_(False)
|
|
2469
|
+
menu.addItemWithTitle_action_keyEquivalent_("Connection: Secure (HTTPS)" if parsed.scheme() == "https" else "Connection: Insecure / Local", "", "").setEnabled_(False)
|
|
2470
|
+
menu.addItem_(Cocoa.NSMenuItem.separatorItem())
|
|
2471
|
+
menu.addItemWithTitle_action_keyEquivalent_("Copy Current URL", "onCopyUrl:", "").setTarget_(self)
|
|
2472
|
+
menu.popUpMenuPositioningItem_atLocation_inView_(None, Cocoa.NSMakePoint(0, 0), sender)
|
|
2473
|
+
|
|
2474
|
+
def onCopyUrl_(self, sender):
|
|
2475
|
+
wv = self.get_active_webview()
|
|
2476
|
+
if wv and wv.URL():
|
|
2477
|
+
pb = Cocoa.NSPasteboard.generalPasteboard()
|
|
2478
|
+
pb.clearContents()
|
|
2479
|
+
pb.setString_forType_(wv.URL().absoluteString(), Cocoa.NSPasteboardTypeString)
|
|
2480
|
+
|
|
2481
|
+
def onSwitchTab1_(self, s): self.switch_to_tab(0)
|
|
2482
|
+
def onSwitchTab2_(self, s): self.switch_to_tab(1)
|
|
2483
|
+
def onSwitchTab3_(self, s): self.switch_to_tab(2)
|
|
2484
|
+
def onSwitchTab4_(self, s): self.switch_to_tab(3)
|
|
2485
|
+
def onSwitchTab5_(self, s): self.switch_to_tab(4)
|
|
2486
|
+
def onSwitchTab6_(self, s): self.switch_to_tab(5)
|
|
2487
|
+
def onSwitchTab7_(self, s): self.switch_to_tab(6)
|
|
2488
|
+
def onSwitchTab8_(self, s): self.switch_to_tab(7)
|
|
2489
|
+
def onSwitchTab9_(self, s):
|
|
2490
|
+
if self.tabs: self.switch_to_tab(len(self.tabs) - 1)
|
|
2491
|
+
|
|
2492
|
+
def get_active_webview(self):
|
|
2493
|
+
if 0 <= self.active_tab_index < len(self.tabs):
|
|
2494
|
+
return self.tabs[self.active_tab_index].get("webview")
|
|
2495
|
+
return None
|
|
2496
|
+
|
|
2497
|
+
def onOmniboxEnter_(self, sender):
|
|
2498
|
+
self.omnibox_panel.hide()
|
|
2499
|
+
val = self.omnibox.stringValue().strip()
|
|
2500
|
+
if not val:
|
|
2501
|
+
return
|
|
2502
|
+
if val.startswith("chrome://"): val = "browser://" + val[9:]
|
|
2503
|
+
elif val.startswith("edge://"): val = "browser://" + val[7:]
|
|
2504
|
+
if val.startswith("localhost") or val.startswith("127.0.0.1"): val = "http://" + val
|
|
2505
|
+
elif not (val.startswith("http://") or val.startswith("https://") or val.startswith("browser://")):
|
|
2506
|
+
val = "https://" + val if ("." in val and " " not in val) else SETTINGS.get_search_url(val)
|
|
2507
|
+
|
|
2508
|
+
if SETTINGS.settings.get("search_in_new_tab", False):
|
|
2509
|
+
self.add_tab(val)
|
|
2510
|
+
else:
|
|
2511
|
+
wv = self.get_active_webview()
|
|
2512
|
+
if wv:
|
|
2513
|
+
wv.loadRequest_(Foundation.NSURLRequest.requestWithURL_(Foundation.NSURL.URLWithString_(val)))
|
|
2514
|
+
self.window().makeFirstResponder_(self.web_container)
|
|
2515
|
+
|
|
2516
|
+
def controlTextDidChange_(self, notif):
|
|
2517
|
+
if notif.object() == self.omnibox:
|
|
2518
|
+
self.omnibox_panel.handle_text_change(self.omnibox.stringValue())
|
|
2519
|
+
|
|
2520
|
+
def control_textView_doCommandBySelector_(self, control, textView, commandSelector):
|
|
2521
|
+
return self.omnibox_panel.handle_command_selector(commandSelector)
|
|
2522
|
+
|
|
2523
|
+
def register_download(self, name, path, obj):
|
|
2524
|
+
dl_id = str(uuid.uuid4())
|
|
2525
|
+
self.downloads.append({"id": dl_id, "name": name, "path": path, "status": "Downloading", "progress": 0.0, "obj": obj})
|
|
2526
|
+
self.dl_btn.setTitle_(f"⬇ ({len(self.downloads)})")
|
|
2527
|
+
self.sync_downloads_page()
|
|
2528
|
+
return dl_id
|
|
2529
|
+
|
|
2530
|
+
def finish_download(self, obj, status, dl_id=None, dl_path=None):
|
|
2531
|
+
for d in reversed(self.downloads):
|
|
2532
|
+
if (dl_id and d.get("id") == dl_id) or (dl_path and d.get("path") == dl_path) or (obj and d.get("obj") == obj):
|
|
2533
|
+
d["status"], d["progress"] = status, 1.0
|
|
2534
|
+
break
|
|
2535
|
+
self.sync_downloads_page()
|
|
2536
|
+
|
|
2537
|
+
def sync_downloads_page(self):
|
|
2538
|
+
for t in self.tabs:
|
|
2539
|
+
wv = t.get("webview")
|
|
2540
|
+
if wv and wv.URL() and "browser://downloads" in wv.URL().absoluteString():
|
|
2541
|
+
for d in self.downloads:
|
|
2542
|
+
id_json = json.dumps(d.get('id'))
|
|
2543
|
+
name_json = json.dumps(d.get('name'))
|
|
2544
|
+
path_json = json.dumps(d.get('path'))
|
|
2545
|
+
status_json = json.dumps(d.get('status'))
|
|
2546
|
+
prog = d.get('progress', 0.0)
|
|
2547
|
+
wv.evaluateJavaScript_completionHandler_(f"window.updateDownload({id_json}, {name_json}, {path_json}, {status_json}, {prog});", None)
|
|
2548
|
+
|
|
2549
|
+
def show_password_banner(self, host, username, password):
|
|
2550
|
+
if not host or not password:
|
|
2551
|
+
return
|
|
2552
|
+
if hasattr(self, 'pw_banner') and self.pw_banner and self.pw_banner.superview():
|
|
2553
|
+
return
|
|
2554
|
+
w = self.web_container.bounds().size.width
|
|
2555
|
+
self.pw_banner = Cocoa.NSVisualEffectView.alloc().initWithFrame_(Cocoa.NSMakeRect(0, self.web_container.bounds().size.height - 40, w, 40))
|
|
2556
|
+
self.pw_banner.setMaterial_(Cocoa.NSVisualEffectMaterialPopover)
|
|
2557
|
+
self.pw_banner.setAutoresizingMask_(Cocoa.NSViewWidthSizable | Cocoa.NSViewMinYMargin)
|
|
2558
|
+
|
|
2559
|
+
lbl = Cocoa.NSTextField.labelWithString_(f"Save password for {host} in PyBrowser Keychain?")
|
|
2560
|
+
lbl.setFrameOrigin_(Cocoa.NSMakePoint(20, 11))
|
|
2561
|
+
lbl.setFont_(Cocoa.NSFont.systemFontOfSize_(12))
|
|
2562
|
+
self.pw_banner.addSubview_(lbl)
|
|
2563
|
+
|
|
2564
|
+
btn_save = Cocoa.NSButton.alloc().initWithFrame_(Cocoa.NSMakeRect(w - 90, 8, 75, 24))
|
|
2565
|
+
btn_save.setTitle_("Save")
|
|
2566
|
+
btn_save.setBezelStyle_(Cocoa.NSBezelStyleRounded)
|
|
2567
|
+
btn_save.setAutoresizingMask_(Cocoa.NSViewMinXMargin)
|
|
2568
|
+
btn_save.setAction_("onSavePassword:")
|
|
2569
|
+
btn_save.setTarget_(self)
|
|
2570
|
+
self.pw_banner.addSubview_(btn_save)
|
|
2571
|
+
|
|
2572
|
+
btn_never = Cocoa.NSButton.alloc().initWithFrame_(Cocoa.NSMakeRect(w - 175, 8, 75, 24))
|
|
2573
|
+
btn_never.setTitle_("Never")
|
|
2574
|
+
btn_never.setBezelStyle_(Cocoa.NSBezelStyleRounded)
|
|
2575
|
+
btn_never.setAutoresizingMask_(Cocoa.NSViewMinXMargin)
|
|
2576
|
+
btn_never.setAction_("onDismissPassword:")
|
|
2577
|
+
btn_never.setTarget_(self)
|
|
2578
|
+
self.pw_banner.addSubview_(btn_never)
|
|
2579
|
+
|
|
2580
|
+
self.pending_pw = {"host": host, "username": username, "password": password}
|
|
2581
|
+
self.web_container.addSubview_(self.pw_banner)
|
|
2582
|
+
|
|
2583
|
+
def onSavePassword_(self, sender):
|
|
2584
|
+
if hasattr(self, 'pending_pw'):
|
|
2585
|
+
p = self.pending_pw
|
|
2586
|
+
save_to_keychain(p["host"], p["username"], p["password"])
|
|
2587
|
+
SETTINGS.settings.setdefault("saved_logins", {})[p["host"]] = p["username"]
|
|
2588
|
+
SETTINGS.save()
|
|
2589
|
+
self.onDismissPassword_(sender)
|
|
2590
|
+
|
|
2591
|
+
def onDismissPassword_(self, sender):
|
|
2592
|
+
if hasattr(self, 'pw_banner') and self.pw_banner:
|
|
2593
|
+
self.pw_banner.removeFromSuperview()
|
|
2594
|
+
self.pw_banner = None
|
|
2595
|
+
|
|
2596
|
+
def update_progress_for_webview(self, wv):
|
|
2597
|
+
if wv != self.get_active_webview():
|
|
2598
|
+
return
|
|
2599
|
+
if not wv.isLoading():
|
|
2600
|
+
self.progress_bar.setHidden_(True)
|
|
2601
|
+
self.progress_bar.setProgress_(0.0)
|
|
2602
|
+
return
|
|
2603
|
+
p = max(0.1, float(wv.estimatedProgress()))
|
|
2604
|
+
self.progress_bar.setHidden_(False)
|
|
2605
|
+
self.progress_bar.setProgress_(p)
|
|
2606
|
+
if p >= 1.0:
|
|
2607
|
+
Cocoa.NSTimer.scheduledTimerWithTimeInterval_repeats_block_(0.2, False, lambda t: self.progress_bar.setHidden_(True))
|
|
2608
|
+
|
|
2609
|
+
def userContentController_didReceiveScriptMessage_(self, ucc, message):
|
|
2610
|
+
name = message.name()
|
|
2611
|
+
if name == "terminalInput":
|
|
2612
|
+
TERMINAL_MANAGER.write_active(message.webView(), message.body())
|
|
2613
|
+
return
|
|
2614
|
+
elif name == "terminalResize":
|
|
2615
|
+
b = message.body()
|
|
2616
|
+
TERMINAL_MANAGER.get_or_create(message.webView(), b.get("cols"), b.get("rows"))
|
|
2617
|
+
return
|
|
2618
|
+
elif name == "audio":
|
|
2619
|
+
b = message.body() if isinstance(message.body(), dict) else {}
|
|
2620
|
+
has_a = (b.get("cmd") == "audio_play")
|
|
2621
|
+
for t in self.tabs:
|
|
2622
|
+
if t.get("webview") == message.webView():
|
|
2623
|
+
t["has_audio"] = has_a
|
|
2624
|
+
break
|
|
2625
|
+
self.relayout_tabs()
|
|
2626
|
+
return
|
|
2627
|
+
|
|
2628
|
+
body = message.body()
|
|
2629
|
+
cmd = body.get("cmd") if isinstance(body, dict) else None
|
|
2630
|
+
|
|
2631
|
+
if cmd == "open_url":
|
|
2632
|
+
if body.get("url"):
|
|
2633
|
+
self.add_tab(body.get("url"))
|
|
2634
|
+
elif cmd == "toggle_fullscreen_terminal":
|
|
2635
|
+
self.toggle_tab_bar()
|
|
2636
|
+
elif cmd == "context_target_image":
|
|
2637
|
+
self.last_context_image_url = body.get("src")
|
|
2638
|
+
elif cmd == "blob_image_data":
|
|
2639
|
+
data_url, dest_path = body.get("data"), body.get("path")
|
|
2640
|
+
if data_url and dest_path:
|
|
2641
|
+
try:
|
|
2642
|
+
raw = base64.b64decode(data_url.split(",", 1)[1])
|
|
2643
|
+
with open(dest_path, "wb") as f:
|
|
2644
|
+
f.write(raw)
|
|
2645
|
+
self.finish_download(None, "Completed", dl_path=dest_path)
|
|
2646
|
+
except Exception as e:
|
|
2647
|
+
print(f"[Blob Image Save Error] {e}")
|
|
2648
|
+
self.finish_download(None, "Failed", dl_path=dest_path)
|
|
2649
|
+
elif cmd == "image_download_failed":
|
|
2650
|
+
self.finish_download(None, "Failed", dl_path=body.get("path"))
|
|
2651
|
+
elif cmd == "toggle_script":
|
|
2652
|
+
fname = body.get("file")
|
|
2653
|
+
if fname:
|
|
2654
|
+
p1 = os.path.join(USERSCRIPTS_DIR, fname)
|
|
2655
|
+
p2 = os.path.join(USERSCRIPTS_DIR, fname + ".disabled")
|
|
2656
|
+
if os.path.exists(p2): os.rename(p2, p1)
|
|
2657
|
+
elif os.path.exists(p1): os.rename(p1, p2)
|
|
2658
|
+
elif cmd == "delete_script":
|
|
2659
|
+
fname = body.get("file")
|
|
2660
|
+
if fname:
|
|
2661
|
+
for p in (os.path.join(USERSCRIPTS_DIR, fname), os.path.join(USERSCRIPTS_DIR, fname + ".disabled")):
|
|
2662
|
+
if os.path.exists(p): os.remove(p)
|
|
2663
|
+
elif cmd == "install_crx":
|
|
2664
|
+
cid = body.get("id")
|
|
2665
|
+
if cid:
|
|
2666
|
+
threading.Thread(target=lambda: install_crx(cid), daemon=True).start()
|
|
2667
|
+
elif cmd == "prompt_save_password":
|
|
2668
|
+
self.show_password_banner(body.get("host", ""), body.get("username", ""), body.get("password", ""))
|
|
2669
|
+
elif cmd == "check_autofill":
|
|
2670
|
+
host = body.get("host")
|
|
2671
|
+
saved_user = SETTINGS.settings.get("saved_logins", {}).get(host)
|
|
2672
|
+
if saved_user:
|
|
2673
|
+
pwd = get_from_keychain(host, saved_user)
|
|
2674
|
+
if pwd:
|
|
2675
|
+
wv = self.get_active_webview()
|
|
2676
|
+
if wv:
|
|
2677
|
+
wv.evaluateJavaScript_completionHandler_(f"let pw = document.querySelector('input[type=password]'); let user = document.querySelector('input[type=text], input[type=email]'); if(pw) pw.value = '{pwd}'; if(user) user.value = '{saved_user}';", None)
|
|
2678
|
+
elif cmd == "save_setting":
|
|
2679
|
+
key, val = body.get("key"), body.get("val")
|
|
2680
|
+
if key == "download_path":
|
|
2681
|
+
val = os.path.expanduser(str(val).strip())
|
|
2682
|
+
SETTINGS.settings[key] = val
|
|
2683
|
+
SETTINGS.save()
|
|
2684
|
+
if key in ("bookmarks_bar_visible", "show_home_button", "vertical_tabs_enabled"):
|
|
2685
|
+
self.relayout_views()
|
|
2686
|
+
elif key == "theme_mode":
|
|
2687
|
+
app_delegate = Cocoa.NSApp.delegate()
|
|
2688
|
+
for c in getattr(app_delegate, "controllers", []):
|
|
2689
|
+
c.title_bar.setNeedsDisplay_(True)
|
|
2690
|
+
c.tab_strip.setNeedsDisplay_(True)
|
|
2691
|
+
c.toolbar.setNeedsDisplay_(True)
|
|
2692
|
+
c.bookmarks_bar.setNeedsDisplay_(True)
|
|
2693
|
+
for sub in c.tab_strip.subviews():
|
|
2694
|
+
sub.setNeedsDisplay_(True)
|
|
2695
|
+
elif key in ("default_zoom",):
|
|
2696
|
+
wv = self.get_active_webview()
|
|
2697
|
+
if wv:
|
|
2698
|
+
self.apply_zoom_to_webview(wv)
|
|
2699
|
+
elif key in ("user_agent_mode", "custom_user_agent"):
|
|
2700
|
+
ua = SETTINGS.get_user_agent()
|
|
2701
|
+
for t in self.tabs:
|
|
2702
|
+
if t.get("webview"):
|
|
2703
|
+
t["webview"].setCustomUserAgent_(ua)
|
|
2704
|
+
elif key == "doh_provider":
|
|
2705
|
+
with DNS_LOCK:
|
|
2706
|
+
DNS_CACHE.clear()
|
|
2707
|
+
elif key in ("adblock_enabled", "adblock_stevenblack_enabled", "adblock_base_enabled", "custom_adblock_domains"):
|
|
2708
|
+
compile_webkit_adblock_rules()
|
|
2709
|
+
elif cmd == "choose_download_directory":
|
|
2710
|
+
# Fixed: Asynchronous modal sheet targeting caller webview without blocking WebKit IPC
|
|
2711
|
+
calling_wv = message.webView()
|
|
2712
|
+
panel = Cocoa.NSOpenPanel.openPanel()
|
|
2713
|
+
panel.setCanChooseDirectories_(True)
|
|
2714
|
+
panel.setCanChooseFiles_(False)
|
|
2715
|
+
panel.setAllowsMultipleSelection_(False)
|
|
2716
|
+
panel.setTreatsFilePackagesAsDirectories_(True)
|
|
2717
|
+
panel.setPrompt_("Select Download Location")
|
|
2718
|
+
cur_dir = validate_and_get_download_dir()
|
|
2719
|
+
panel.setDirectoryURL_(Foundation.NSURL.fileURLWithPath_(cur_dir))
|
|
2720
|
+
|
|
2721
|
+
def on_panel_done(result):
|
|
2722
|
+
if result == Cocoa.NSModalResponseOK and panel.URLs():
|
|
2723
|
+
chosen_path = panel.URLs()[0].path()
|
|
2724
|
+
if os.access(chosen_path, os.W_OK):
|
|
2725
|
+
SETTINGS.settings["download_path"] = chosen_path
|
|
2726
|
+
SETTINGS.save()
|
|
2727
|
+
if calling_wv:
|
|
2728
|
+
path_json = json.dumps(chosen_path)
|
|
2729
|
+
calling_wv.evaluateJavaScript_completionHandler_(
|
|
2730
|
+
f"if (document.getElementById('dl-path-input')) document.getElementById('dl-path-input').value = {path_json}; if (document.getElementById('dl-path-display')) document.getElementById('dl-path-display').innerText = {path_json}; showToast('Download directory updated');", None
|
|
2731
|
+
)
|
|
2732
|
+
else:
|
|
2733
|
+
if calling_wv:
|
|
2734
|
+
calling_wv.evaluateJavaScript_completionHandler_("showToast('Error: Selected directory is not writable');", None)
|
|
2735
|
+
|
|
2736
|
+
panel.beginSheetModalForWindow_completionHandler_(self.window(), on_panel_done)
|
|
2737
|
+
elif cmd == "search_blocked_domains":
|
|
2738
|
+
matches = search_blocked_domains(body.get("query", ""), limit=100)
|
|
2739
|
+
json_res = json.dumps(matches)
|
|
2740
|
+
wv = self.get_active_webview()
|
|
2741
|
+
if wv:
|
|
2742
|
+
wv.evaluateJavaScript_completionHandler_(f"window.renderBlockedResults({json_res});", None)
|
|
2743
|
+
elif cmd == "update_adblock_filters":
|
|
2744
|
+
def fetch_task():
|
|
2745
|
+
count = update_adblock_filters_from_web()
|
|
2746
|
+
def on_done():
|
|
2747
|
+
wv = self.get_active_webview()
|
|
2748
|
+
if wv:
|
|
2749
|
+
wv.evaluateJavaScript_completionHandler_(f"const b = document.getElementById('adblock-count-badge'); if (b) b.innerText = '{count:,} rules'; const btn = document.getElementById('update-filters-btn'); if (btn) {{ btn.innerText = 'Update Lists Now'; btn.disabled = false; }} if (window.showToast) showToast('Adblock filters updated');", None)
|
|
2750
|
+
dispatch_to_main(on_done)
|
|
2751
|
+
threading.Thread(target=fetch_task, daemon=True).start()
|
|
2752
|
+
elif cmd == "clear_browsing_data":
|
|
2753
|
+
targets = body.get("targets", [])
|
|
2754
|
+
if "history" in targets:
|
|
2755
|
+
clear_history()
|
|
2756
|
+
if "favicons" in targets:
|
|
2757
|
+
FaviconCache.clear()
|
|
2758
|
+
if "dns_cache" in targets:
|
|
2759
|
+
with DNS_LOCK:
|
|
2760
|
+
DNS_CACHE.clear()
|
|
2761
|
+
if "cookies_cache" in targets:
|
|
2762
|
+
ds = WebKit.WKWebsiteDataStore.defaultDataStore()
|
|
2763
|
+
ds.removeDataOfTypes_modifiedSince_completionHandler_(WebKit.WKWebsiteDataStore.allWebsiteDataTypes(), Foundation.NSDate.dateWithTimeIntervalSince1970_(0), lambda: None)
|
|
2764
|
+
elif cmd == "reset_defaults":
|
|
2765
|
+
SETTINGS.reset_defaults()
|
|
2766
|
+
self.relayout_views()
|
|
2767
|
+
for tab in self.tabs:
|
|
2768
|
+
wv = tab.get("webview")
|
|
2769
|
+
if wv:
|
|
2770
|
+
self.apply_zoom_to_webview(wv)
|
|
2771
|
+
wv = self.get_active_webview()
|
|
2772
|
+
if wv:
|
|
2773
|
+
wv.reload()
|
|
2774
|
+
elif cmd == "html5_fullscreen_enter":
|
|
2775
|
+
self.handle_fullscreen_enter(auto_hide_bars=True)
|
|
2776
|
+
elif cmd == "html5_fullscreen_exit":
|
|
2777
|
+
self.handle_fullscreen_exit(restore_bars=True)
|
|
2778
|
+
elif cmd == "dl_request_sync":
|
|
2779
|
+
self.sync_downloads_page()
|
|
2780
|
+
elif cmd == "dl_show":
|
|
2781
|
+
p = body.get("path")
|
|
2782
|
+
if p and os.path.exists(p):
|
|
2783
|
+
Cocoa.NSWorkspace.sharedWorkspace().selectFile_inFileViewerRootedAtPath_(p, None)
|
|
2784
|
+
else:
|
|
2785
|
+
dest = validate_and_get_download_dir()
|
|
2786
|
+
if os.path.exists(dest):
|
|
2787
|
+
Cocoa.NSWorkspace.sharedWorkspace().openFile_(dest)
|
|
2788
|
+
elif cmd == "dl_cancel":
|
|
2789
|
+
dl_id = body.get("id")
|
|
2790
|
+
for d in self.downloads:
|
|
2791
|
+
if d["id"] == dl_id and d["status"] == "Downloading":
|
|
2792
|
+
if d.get("obj") and hasattr(d["obj"], "cancel"):
|
|
2793
|
+
d["obj"].cancel()
|
|
2794
|
+
self.finish_download(d.get("obj"), "Canceled", dl_id=dl_id)
|
|
2795
|
+
break
|
|
2796
|
+
elif cmd == "delete_bookmark":
|
|
2797
|
+
target_url = body.get("url")
|
|
2798
|
+
if target_url:
|
|
2799
|
+
try:
|
|
2800
|
+
with open(BOOKMARKS_FILE, "r", encoding="utf-8") as f:
|
|
2801
|
+
bms = json.load(f)
|
|
2802
|
+
except Exception:
|
|
2803
|
+
bms = []
|
|
2804
|
+
atomic_write_json(BOOKMARKS_FILE, [b for b in bms if isinstance(b, dict) and b.get("url") != target_url])
|
|
2805
|
+
self.bookmarks_bar.reload_bookmarks()
|
|
2806
|
+
self.update_star_status()
|
|
2807
|
+
elif cmd == "add_bookmark":
|
|
2808
|
+
u = body.get("url")
|
|
2809
|
+
t = body.get("title") or u
|
|
2810
|
+
if u:
|
|
2811
|
+
try:
|
|
2812
|
+
with open(BOOKMARKS_FILE, "r", encoding="utf-8") as f:
|
|
2813
|
+
bms = json.load(f)
|
|
2814
|
+
except Exception:
|
|
2815
|
+
bms = []
|
|
2816
|
+
if not any(isinstance(b, dict) and b.get("url") == u for b in bms):
|
|
2817
|
+
bms.append({"title": t, "url": u})
|
|
2818
|
+
atomic_write_json(BOOKMARKS_FILE, bms)
|
|
2819
|
+
self.bookmarks_bar.reload_bookmarks()
|
|
2820
|
+
self.update_star_status()
|
|
2821
|
+
elif cmd == "delete_history_item":
|
|
2822
|
+
item_id = body.get("id")
|
|
2823
|
+
if item_id is not None:
|
|
2824
|
+
try:
|
|
2825
|
+
with sqlite3.connect(HISTORY_FILE) as conn:
|
|
2826
|
+
conn.execute("DELETE FROM history WHERE id = ?;", (item_id,))
|
|
2827
|
+
except Exception:
|
|
2828
|
+
pass
|
|
2829
|
+
|
|
2830
|
+
# ========================================================================
|
|
2831
|
+
# WEBKIT DELEGATES
|
|
2832
|
+
# ========================================================================
|
|
2833
|
+
def webView_createWebViewWithConfiguration_forNavigationAction_windowFeatures_(self, wv, cfg, act, feat):
|
|
2834
|
+
pending_action = getattr(wv, "pending_image_action", None)
|
|
2835
|
+
if pending_action:
|
|
2836
|
+
wv.pending_image_action = None
|
|
2837
|
+
img_url = act.request().URL().absoluteString() if act.request().URL() else None
|
|
2838
|
+
if img_url:
|
|
2839
|
+
if pending_action == "save_as": self.perform_save_image_as(img_url)
|
|
2840
|
+
elif pending_action == "download": self.perform_download_image(img_url)
|
|
2841
|
+
elif pending_action == "copy_address": self.perform_copy_image_address(img_url)
|
|
2842
|
+
return None
|
|
2843
|
+
|
|
2844
|
+
url = act.request().URL().absoluteString() if act.request().URL() else None
|
|
2845
|
+
if url:
|
|
2846
|
+
self.add_tab(url)
|
|
2847
|
+
return None
|
|
2848
|
+
|
|
2849
|
+
def webView_decidePolicyForNavigationAction_decisionHandler_(self, wv, action, handler):
|
|
2850
|
+
mods = action.modifierFlags() if hasattr(action, "modifierFlags") else 0
|
|
2851
|
+
if bool(mods & Cocoa.NSEventModifierFlagCommand) and action.navigationType() == WebKit.WKNavigationTypeLinkActivated:
|
|
2852
|
+
url = action.request().URL().absoluteString() if action.request().URL() else None
|
|
2853
|
+
if url:
|
|
2854
|
+
self.add_tab(url)
|
|
2855
|
+
handler(WebKit.WKNavigationActionPolicyCancel)
|
|
2856
|
+
return
|
|
2857
|
+
handler(WebKit.WKNavigationActionPolicyAllow)
|
|
2858
|
+
|
|
2859
|
+
def webView_decidePolicyForNavigationResponse_decisionHandler_(self, wv, resp, handler):
|
|
2860
|
+
if not resp.canShowMIMEType():
|
|
2861
|
+
handler(WebKit.WKNavigationResponsePolicyDownload if SETTINGS.settings.get("downloads_enabled", True) else WebKit.WKNavigationResponsePolicyCancel)
|
|
2862
|
+
return
|
|
2863
|
+
if isinstance(resp.response(), Foundation.NSHTTPURLResponse):
|
|
2864
|
+
disp = resp.response().allHeaderFields().get("Content-Disposition", "") or resp.response().allHeaderFields().get("content-disposition", "")
|
|
2865
|
+
if "attachment" in str(disp).lower():
|
|
2866
|
+
handler(WebKit.WKNavigationResponsePolicyDownload if SETTINGS.settings.get("downloads_enabled", True) else WebKit.WKNavigationResponsePolicyCancel)
|
|
2867
|
+
return
|
|
2868
|
+
handler(WebKit.WKNavigationResponsePolicyAllow)
|
|
2869
|
+
|
|
2870
|
+
def webView_runOpenPanelWithParameters_initiatedByFrame_completionHandler_(self, wv, params, frame, handler):
|
|
2871
|
+
panel = Cocoa.NSOpenPanel.openPanel()
|
|
2872
|
+
panel.setAllowsMultipleSelection_(params.allowsMultipleSelection())
|
|
2873
|
+
panel.setCanChooseFiles_(True)
|
|
2874
|
+
panel.setCanChooseDirectories_(False)
|
|
2875
|
+
handler(panel.URLs() if panel.runModal() == Cocoa.NSModalResponseOK else None)
|
|
2876
|
+
|
|
2877
|
+
def webView_navigationAction_didBecomeDownload_(self, w, a, d):
|
|
2878
|
+
d.setDelegate_(self.download_delegate)
|
|
2879
|
+
|
|
2880
|
+
def webView_navigationResponse_didBecomeDownload_(self, w, r, d):
|
|
2881
|
+
d.setDelegate_(self.download_delegate)
|
|
2882
|
+
|
|
2883
|
+
def webView_didStartProvisionalNavigation_(self, wv, nav):
|
|
2884
|
+
if wv == self.get_active_webview():
|
|
2885
|
+
self.progress_bar.setHidden_(False)
|
|
2886
|
+
self.progress_bar.setProgress_(0.15)
|
|
2887
|
+
self.apply_zoom_to_webview(wv)
|
|
2888
|
+
|
|
2889
|
+
def webView_didFinishNavigation_(self, wv, nav):
|
|
2890
|
+
tab = self.tabs[self.active_tab_index] if 0 <= self.active_tab_index < len(self.tabs) else None
|
|
2891
|
+
self.apply_zoom_to_webview(wv, tab)
|
|
2892
|
+
self.update_star_status()
|
|
2893
|
+
if wv == self.get_active_webview():
|
|
2894
|
+
self.progress_bar.setProgress_(1.0)
|
|
2895
|
+
Cocoa.NSTimer.scheduledTimerWithTimeInterval_repeats_block_(0.2, False, lambda t: self.progress_bar.setHidden_(True))
|
|
2896
|
+
if not self.is_incognito and wv.URL() and SETTINGS.settings.get("history_enabled", True):
|
|
2897
|
+
record_history(wv.URL().absoluteString(), wv.title() or wv.URL().absoluteString())
|
|
2898
|
+
self.save_session()
|
|
2899
|
+
|
|
2900
|
+
def webView_didFailProvisionalNavigation_withError_(self, wv, nav, err):
|
|
2901
|
+
if wv == self.get_active_webview():
|
|
2902
|
+
self.progress_bar.setHidden_(True)
|
|
2903
|
+
self.progress_bar.setProgress_(0.0)
|
|
2904
|
+
|
|
2905
|
+
def webView_didFailNavigation_withError_(self, wv, nav, err):
|
|
2906
|
+
if wv == self.get_active_webview():
|
|
2907
|
+
self.progress_bar.setHidden_(True)
|
|
2908
|
+
self.progress_bar.setProgress_(0.0)
|
|
2909
|
+
|
|
2910
|
+
def webViewWebContentProcessDidTerminate_(self, wv):
|
|
2911
|
+
TERMINAL_MANAGER.close_for_webview(wv)
|
|
2912
|
+
wv.reload()
|
|
2913
|
+
|
|
2914
|
+
def observeValueForKeyPath_ofObject_change_context_(self, keyPath, obj, change, context):
|
|
2915
|
+
if keyPath == "URL" and obj.URL():
|
|
2916
|
+
for t in self.tabs:
|
|
2917
|
+
if t.get("webview") == obj:
|
|
2918
|
+
if t.get("is_sleeping"):
|
|
2919
|
+
break
|
|
2920
|
+
t["url"] = obj.URL().absoluteString()
|
|
2921
|
+
if obj.URL().host():
|
|
2922
|
+
FaviconCache.get_favicon(obj.URL().host(), lambda img, tab=t: self.set_favicon(tab, img))
|
|
2923
|
+
break
|
|
2924
|
+
if self.get_active_webview() == obj:
|
|
2925
|
+
tab = self.tabs[self.active_tab_index] if 0 <= self.active_tab_index < len(self.tabs) else {}
|
|
2926
|
+
if not tab.get("is_sleeping"):
|
|
2927
|
+
self.omnibox.set_url(obj.URL().absoluteString())
|
|
2928
|
+
self.update_star_status()
|
|
2929
|
+
elif keyPath == "title" and obj.title():
|
|
2930
|
+
for t in self.tabs:
|
|
2931
|
+
if t.get("webview") == obj:
|
|
2932
|
+
if not t.get("is_sleeping"):
|
|
2933
|
+
t["title"] = str(obj.title())
|
|
2934
|
+
break
|
|
2935
|
+
self.relayout_tabs()
|
|
2936
|
+
elif keyPath == "estimatedProgress":
|
|
2937
|
+
self.update_progress_for_webview(obj)
|
|
2938
|
+
elif keyPath == "fractionCompleted":
|
|
2939
|
+
try:
|
|
2940
|
+
frac = float(obj.fractionCompleted())
|
|
2941
|
+
for d in self.downloads:
|
|
2942
|
+
if d.get("obj") and hasattr(d["obj"], "progress") and d["obj"].progress() == obj:
|
|
2943
|
+
d["progress"] = frac
|
|
2944
|
+
self.sync_downloads_page()
|
|
2945
|
+
break
|
|
2946
|
+
except Exception:
|
|
2947
|
+
pass
|
|
2948
|
+
|
|
2949
|
+
def set_favicon(self, tab, img):
|
|
2950
|
+
tab["favicon"] = img
|
|
2951
|
+
self.relayout_tabs()
|
|
2952
|
+
|
|
2953
|
+
def windowDidEnterFullScreen_(self, notif):
|
|
2954
|
+
self.is_fullscreen = True
|
|
2955
|
+
self.relayout_views()
|
|
2956
|
+
self.restore_normal_zoom()
|
|
2957
|
+
|
|
2958
|
+
def windowDidExitFullScreen_(self, notif):
|
|
2959
|
+
self.is_fullscreen = False
|
|
2960
|
+
self.relayout_views()
|
|
2961
|
+
self.restore_normal_zoom()
|
|
2962
|
+
|
|
2963
|
+
def windowDidResignKey_(self, notif):
|
|
2964
|
+
if hasattr(self, 'omnibox_panel') and self.omnibox_panel.is_visible():
|
|
2965
|
+
self.omnibox_panel.hide()
|
|
2966
|
+
|
|
2967
|
+
def windowDidResize_(self, notif):
|
|
2968
|
+
self.relayout_views()
|
|
2969
|
+
if self.omnibox_panel.is_visible():
|
|
2970
|
+
self.omnibox_panel.hide()
|
|
2971
|
+
|
|
2972
|
+
def windowDidMove_(self, notif):
|
|
2973
|
+
if self.omnibox_panel.is_visible():
|
|
2974
|
+
self.omnibox_panel.hide()
|
|
2975
|
+
|
|
2976
|
+
def windowWillClose_(self, notif):
|
|
2977
|
+
self.save_session()
|
|
2978
|
+
for t in self.tabs:
|
|
2979
|
+
wv = t.get("webview")
|
|
2980
|
+
if wv:
|
|
2981
|
+
TERMINAL_MANAGER.close_for_webview(wv)
|
|
2982
|
+
for k in ("URL", "title", "estimatedProgress"):
|
|
2983
|
+
try:
|
|
2984
|
+
wv.removeObserver_forKeyPath_(self, k)
|
|
2985
|
+
except Exception:
|
|
2986
|
+
pass
|
|
2987
|
+
app_delegate = Cocoa.NSApp.delegate()
|
|
2988
|
+
if app_delegate and hasattr(app_delegate, "controllers") and self in app_delegate.controllers:
|
|
2989
|
+
app_delegate.controllers.remove(self)
|
|
2990
|
+
|
|
2991
|
+
# ============================================================================
|
|
2992
|
+
# APP DELEGATE
|
|
2993
|
+
# ============================================================================
|
|
2994
|
+
class AppDelegate(Cocoa.NSObject):
|
|
2995
|
+
def applicationDidFinishLaunching_(self, notif):
|
|
2996
|
+
try:
|
|
2997
|
+
init_history_db()
|
|
2998
|
+
load_adblock_filters_from_disk()
|
|
2999
|
+
compile_webkit_adblock_rules()
|
|
3000
|
+
load_dns_cache()
|
|
3001
|
+
start_proxy_server()
|
|
3002
|
+
|
|
3003
|
+
self.controllers = []
|
|
3004
|
+
self.process_manager = ProcessManagerController.alloc().init()
|
|
3005
|
+
self.quick_switcher = QuickSwitcherController.alloc().init()
|
|
3006
|
+
self.setup_menu()
|
|
3007
|
+
|
|
3008
|
+
main_window = BrowserWindowController.alloc().init()
|
|
3009
|
+
self.controllers.append(main_window)
|
|
3010
|
+
self.pip_panel = PiPPanelController.alloc().initWithBrowser_(main_window)
|
|
3011
|
+
Cocoa.NSApp.activateIgnoringOtherApps_(True)
|
|
3012
|
+
|
|
3013
|
+
def click_monitor(event):
|
|
3014
|
+
for c in self.controllers:
|
|
3015
|
+
if c.omnibox_panel.is_visible():
|
|
3016
|
+
if c.omnibox_panel.suggestion_window and event.window() == c.omnibox_panel.suggestion_window:
|
|
3017
|
+
return event
|
|
3018
|
+
pt = event.locationInWindow()
|
|
3019
|
+
if event.window() == c.window():
|
|
3020
|
+
in_omni = Cocoa.NSPointInRect(pt, c.omnibox_container.convertRect_toView_(c.omnibox_container.bounds(), None))
|
|
3021
|
+
if in_omni:
|
|
3022
|
+
return event
|
|
3023
|
+
c.omnibox_panel.hide()
|
|
3024
|
+
return event
|
|
3025
|
+
Cocoa.NSEvent.addLocalMonitorForEventsMatchingMask_handler_(Cocoa.NSEventMaskLeftMouseDown, click_monitor)
|
|
3026
|
+
|
|
3027
|
+
def key_monitor(event):
|
|
3028
|
+
flags = event.modifierFlags()
|
|
3029
|
+
cmd = bool(flags & Cocoa.NSEventModifierFlagCommand)
|
|
3030
|
+
shift = bool(flags & Cocoa.NSEventModifierFlagShift)
|
|
3031
|
+
opt = bool(flags & Cocoa.NSEventModifierFlagOption)
|
|
3032
|
+
key = event.keyCode()
|
|
3033
|
+
chars = event.charactersIgnoringModifiers().lower() if event.charactersIgnoringModifiers() else ""
|
|
3034
|
+
win = Cocoa.NSApp.keyWindow()
|
|
3035
|
+
wc = win.delegate() if win and isinstance(win.delegate(), BrowserWindowController) else None
|
|
3036
|
+
|
|
3037
|
+
if key == 53: # Escape
|
|
3038
|
+
if shift and not cmd and not opt:
|
|
3039
|
+
self.process_manager.show()
|
|
3040
|
+
return None
|
|
3041
|
+
if opt and wc:
|
|
3042
|
+
wc.toggle_tab_bar()
|
|
3043
|
+
return None
|
|
3044
|
+
if wc:
|
|
3045
|
+
wc.handle_escape_key()
|
|
3046
|
+
return None
|
|
3047
|
+
|
|
3048
|
+
if cmd and shift and not opt:
|
|
3049
|
+
if key == 0 or chars == 'a':
|
|
3050
|
+
self.quick_switcher.show()
|
|
3051
|
+
return None
|
|
3052
|
+
|
|
3053
|
+
return event
|
|
3054
|
+
Cocoa.NSEvent.addLocalMonitorForEventsMatchingMask_handler_(Cocoa.NSEventMaskKeyDown, key_monitor)
|
|
3055
|
+
|
|
3056
|
+
def background_filter_check():
|
|
3057
|
+
if not os.path.exists(ADBLOCK_CACHE_FILE) or (time.time() - os.path.getmtime(ADBLOCK_CACHE_FILE) > 604800):
|
|
3058
|
+
update_adblock_filters_from_web()
|
|
3059
|
+
threading.Thread(target=background_filter_check, daemon=True).start()
|
|
3060
|
+
|
|
3061
|
+
except Exception:
|
|
3062
|
+
traceback.print_exc()
|
|
3063
|
+
|
|
3064
|
+
def applicationShouldHandleReopen_hasVisibleWindows_(self, app, flag):
|
|
3065
|
+
if not flag:
|
|
3066
|
+
self.onNewWindow_(None)
|
|
3067
|
+
return True
|
|
3068
|
+
return True
|
|
3069
|
+
|
|
3070
|
+
def onNewWindow_(self, sender):
|
|
3071
|
+
wc = BrowserWindowController.alloc().initWithIncognito_(False)
|
|
3072
|
+
self.controllers.append(wc)
|
|
3073
|
+
|
|
3074
|
+
def onNewIncognitoWindow_(self, sender):
|
|
3075
|
+
wc = BrowserWindowController.alloc().initWithIncognito_(True)
|
|
3076
|
+
self.controllers.append(wc)
|
|
3077
|
+
|
|
3078
|
+
def onNewTerminal_(self, sender):
|
|
3079
|
+
win = Cocoa.NSApp.keyWindow()
|
|
3080
|
+
wc = win.delegate() if win and isinstance(win.delegate(), BrowserWindowController) else (self.controllers[0] if self.controllers else None)
|
|
3081
|
+
if wc:
|
|
3082
|
+
wc.add_tab("browser://terminal")
|
|
3083
|
+
|
|
3084
|
+
def onCloseTab_(self, sender=None):
|
|
3085
|
+
win = Cocoa.NSApp.keyWindow()
|
|
3086
|
+
wc = win.delegate() if win and isinstance(win.delegate(), BrowserWindowController) else None
|
|
3087
|
+
if wc and 0 <= wc.active_tab_index < len(wc.tabs):
|
|
3088
|
+
wc.close_tab(wc.active_tab_index)
|
|
3089
|
+
|
|
3090
|
+
def onReopenTab_(self, sender=None):
|
|
3091
|
+
win = Cocoa.NSApp.keyWindow()
|
|
3092
|
+
wc = win.delegate() if win and isinstance(win.delegate(), BrowserWindowController) else None
|
|
3093
|
+
if wc:
|
|
3094
|
+
wc.reopen_last_closed_tab()
|
|
3095
|
+
|
|
3096
|
+
def showTaskManager_(self, sender):
|
|
3097
|
+
self.process_manager.show()
|
|
3098
|
+
|
|
3099
|
+
def quickSwitcher_(self, sender):
|
|
3100
|
+
self.quick_switcher.show()
|
|
3101
|
+
|
|
3102
|
+
def setup_menu(self):
|
|
3103
|
+
main_menu = Cocoa.NSMenu.alloc().init()
|
|
3104
|
+
app_item = Cocoa.NSMenuItem.alloc().init()
|
|
3105
|
+
app_menu = Cocoa.NSMenu.alloc().init()
|
|
3106
|
+
app_menu.addItemWithTitle_action_keyEquivalent_("Quit PyBrowser", "terminate:", "q")
|
|
3107
|
+
app_item.setSubmenu_(app_menu)
|
|
3108
|
+
main_menu.addItem_(app_item)
|
|
3109
|
+
|
|
3110
|
+
file_item = Cocoa.NSMenuItem.alloc().init()
|
|
3111
|
+
file_menu = Cocoa.NSMenu.alloc().initWithTitle_("File")
|
|
3112
|
+
file_menu.addItemWithTitle_action_keyEquivalent_("New Tab", "onNewTab:", "t")
|
|
3113
|
+
file_menu.addItemWithTitle_action_keyEquivalent_("New Terminal Tab", "onNewTerminal:", "T").setTarget_(self)
|
|
3114
|
+
file_menu.addItemWithTitle_action_keyEquivalent_("New Window", "onNewWindow:", "n").setTarget_(self)
|
|
3115
|
+
inc_item = file_menu.addItemWithTitle_action_keyEquivalent_("New Incognito Window", "onNewIncognitoWindow:", "N")
|
|
3116
|
+
inc_item.setKeyEquivalentModifierMask_(Cocoa.NSEventModifierFlagCommand | Cocoa.NSEventModifierFlagShift)
|
|
3117
|
+
inc_item.setTarget_(self)
|
|
3118
|
+
file_menu.addItem_(Cocoa.NSMenuItem.separatorItem())
|
|
3119
|
+
file_menu.addItemWithTitle_action_keyEquivalent_("Close Tab", "onCloseTab:", "w").setTarget_(self)
|
|
3120
|
+
reopen_item = file_menu.addItemWithTitle_action_keyEquivalent_("Reopen Closed Tab", "onReopenTab:", "T")
|
|
3121
|
+
reopen_item.setKeyEquivalentModifierMask_(Cocoa.NSEventModifierFlagCommand | Cocoa.NSEventModifierFlagShift)
|
|
3122
|
+
reopen_item.setTarget_(self)
|
|
3123
|
+
file_item.setSubmenu_(file_menu)
|
|
3124
|
+
main_menu.addItem_(file_item)
|
|
3125
|
+
|
|
3126
|
+
edit_item = Cocoa.NSMenuItem.alloc().init()
|
|
3127
|
+
edit_menu = Cocoa.NSMenu.alloc().initWithTitle_("Edit")
|
|
3128
|
+
edit_menu.addItemWithTitle_action_keyEquivalent_("Undo", "undo:", "z")
|
|
3129
|
+
redo_item = edit_menu.addItemWithTitle_action_keyEquivalent_("Redo", "redo:", "Z")
|
|
3130
|
+
redo_item.setKeyEquivalentModifierMask_(Cocoa.NSEventModifierFlagCommand | Cocoa.NSEventModifierFlagShift)
|
|
3131
|
+
edit_menu.addItem_(Cocoa.NSMenuItem.separatorItem())
|
|
3132
|
+
edit_menu.addItemWithTitle_action_keyEquivalent_("Cut", "cut:", "x")
|
|
3133
|
+
edit_menu.addItemWithTitle_action_keyEquivalent_("Copy", "copy:", "c")
|
|
3134
|
+
edit_menu.addItemWithTitle_action_keyEquivalent_("Paste", "paste:", "v")
|
|
3135
|
+
edit_menu.addItemWithTitle_action_keyEquivalent_("Select All", "selectAll:", "a")
|
|
3136
|
+
edit_menu.addItem_(Cocoa.NSMenuItem.separatorItem())
|
|
3137
|
+
edit_menu.addItemWithTitle_action_keyEquivalent_("Find in Page", "onFindInPage:", "f")
|
|
3138
|
+
edit_item.setSubmenu_(edit_menu)
|
|
3139
|
+
main_menu.addItem_(edit_item)
|
|
3140
|
+
|
|
3141
|
+
view_item = Cocoa.NSMenuItem.alloc().init()
|
|
3142
|
+
view_menu = Cocoa.NSMenu.alloc().initWithTitle_("View")
|
|
3143
|
+
view_menu.addItemWithTitle_action_keyEquivalent_("Reload Page", "onReload:", "r")
|
|
3144
|
+
view_menu.addItemWithTitle_action_keyEquivalent_("Focus Omnibox", "onFocusOmnibox:", "l")
|
|
3145
|
+
view_menu.addItemWithTitle_action_keyEquivalent_("Picture-in-Picture", "onTogglePiP:", "p")
|
|
3146
|
+
view_menu.addItem_(Cocoa.NSMenuItem.separatorItem())
|
|
3147
|
+
view_menu.addItemWithTitle_action_keyEquivalent_("Zoom In", "onZoomIn:", "+")
|
|
3148
|
+
view_menu.addItemWithTitle_action_keyEquivalent_("Zoom Out", "onZoomOut:", "-")
|
|
3149
|
+
view_menu.addItemWithTitle_action_keyEquivalent_("Actual Size", "onResetZoom:", "0")
|
|
3150
|
+
view_menu.addItem_(Cocoa.NSMenuItem.separatorItem())
|
|
3151
|
+
inspect_item = view_menu.addItemWithTitle_action_keyEquivalent_("Inspect Element", "onInspectElement:", "i")
|
|
3152
|
+
inspect_item.setKeyEquivalentModifierMask_(Cocoa.NSEventModifierFlagCommand | Cocoa.NSEventModifierFlagOption)
|
|
3153
|
+
view_item.setSubmenu_(view_menu)
|
|
3154
|
+
main_menu.addItem_(view_item)
|
|
3155
|
+
|
|
3156
|
+
hist_item = Cocoa.NSMenuItem.alloc().init()
|
|
3157
|
+
hist_menu = Cocoa.NSMenu.alloc().initWithTitle_("History")
|
|
3158
|
+
hist_menu.addItemWithTitle_action_keyEquivalent_("Show Full History", "openHistory:", "y")
|
|
3159
|
+
hist_menu.addItem_(Cocoa.NSMenuItem.separatorItem())
|
|
3160
|
+
hist_menu.addItemWithTitle_action_keyEquivalent_("Back", "onGoBack:", "[")
|
|
3161
|
+
hist_menu.addItemWithTitle_action_keyEquivalent_("Forward", "onGoForward:", "]")
|
|
3162
|
+
hist_item.setSubmenu_(hist_menu)
|
|
3163
|
+
main_menu.addItem_(hist_item)
|
|
3164
|
+
|
|
3165
|
+
win_item = Cocoa.NSMenuItem.alloc().init()
|
|
3166
|
+
win_menu = Cocoa.NSMenu.alloc().initWithTitle_("Window")
|
|
3167
|
+
for i in range(1, 10):
|
|
3168
|
+
win_menu.addItemWithTitle_action_keyEquivalent_(f"Tab {i}", f"onSwitchTab{i}:", str(i))
|
|
3169
|
+
win_menu.addItem_(Cocoa.NSMenuItem.separatorItem())
|
|
3170
|
+
win_menu.addItemWithTitle_action_keyEquivalent_("Quick Switcher", "quickSwitcher:", "k")
|
|
3171
|
+
win_menu.addItemWithTitle_action_keyEquivalent_("Task Manager", "showTaskManager:", "").setTarget_(self)
|
|
3172
|
+
win_item.setSubmenu_(win_menu)
|
|
3173
|
+
main_menu.addItem_(win_item)
|
|
3174
|
+
|
|
3175
|
+
Cocoa.NSApp.setMainMenu_(main_menu)
|