From 92b4baa285ef3b350974801a9439dcfdea1cb7f0 Mon Sep 17 00:00:00 2001 From: karim Date: Sat, 6 Jun 2026 05:27:48 +0200 Subject: [PATCH] UX: WebView native-feel + Context-Menu Redesign + Startup-Fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Disable text selection (CSS user-select:none) + block browser context menu (contextmenu preventDefault) in all panels - ContextMenu: pill items, accent hover, entrance animation, optional title header — controlled via single ContextMenu.css - EbenenManager + GeschossManager: show layer name as menu title - startup.py: skip splash on Cmd+N when plugin already loaded - startup.py: hook NewDocument so display-modes apply to new docs --- rhino/panel_base.py | 23 +++++- rhino/startup.py | 16 ++-- src/components/ContextMenu.css | 122 +++++++++++++++++++++++++++++ src/components/ContextMenu.jsx | 47 +++-------- src/components/EbenenManager.jsx | 4 +- src/components/GeschossManager.jsx | 4 +- src/index.css | 10 +++ src/main.jsx | 2 + 8 files changed, 185 insertions(+), 43 deletions(-) create mode 100644 src/components/ContextMenu.css diff --git a/rhino/panel_base.py b/rhino/panel_base.py index 932d897..c1c8a50 100644 --- a/rhino/panel_base.py +++ b/rhino/panel_base.py @@ -267,10 +267,12 @@ def _build_inline_template(): html = f.read().decode("utf-8") placeholder_script = '' + _no_select = '' + _no_ctx = '' if "" in html: - html = html.replace("", placeholder_script + "") + html = html.replace("", placeholder_script + _no_select + _no_ctx + "") else: - html = placeholder_script + html + html = placeholder_script + _no_select + _no_ctx + html def inline_css(m): p = os.path.join(dist_dir, m.group(1).lstrip("./").replace("/", os.sep)) @@ -353,6 +355,15 @@ def attach_webview(panel, bridge, mode): wv.ExecuteScript("window.RHINO_MODE=true;") except Exception: pass + try: + wv.ExecuteScript( + "var _ds=document.createElement('style');" + "_ds.textContent='*{-webkit-user-select:none!important;user-select:none!important;}';" + "document.head.appendChild(_ds);" + "document.addEventListener('contextmenu',function(e){e.preventDefault();},true);" + ) + except Exception: + pass def on_idle(s, e): Rhino.RhinoApp.Idle -= on_idle @@ -440,6 +451,14 @@ def open_satellite_window(mode, params=None, title=None, size=(420, 560), def on_loaded(s, e): try: wv.ExecuteScript("window.RHINO_MODE=true;") except Exception: pass + try: + wv.ExecuteScript( + "var _ds=document.createElement('style');" + "_ds.textContent='*{-webkit-user-select:none!important;user-select:none!important;}';" + "document.head.appendChild(_ds);" + "document.addEventListener('contextmenu',function(e){e.preventDefault();},true);" + ) + except Exception: pass wv.DocumentTitleChanged += on_title_ wv.DocumentLoaded += on_loaded diff --git a/rhino/startup.py b/rhino/startup.py index d83619b..8698772 100644 --- a/rhino/startup.py +++ b/rhino/startup.py @@ -23,11 +23,13 @@ if _HERE not in sys.path: # Nutzer waehrend Python-Imports + Panel-Registrierung nicht in eine schwarze # Rhino-Oberflaeche schaut. Skipt automatisch wenn Launcher seinen eigenen # Splash zeigt (Owner-Marker-Check). -try: - import _startup_splash as _splash_first - _splash_first.show() -except Exception as _ex_splash: - print("[STARTUP] splash early:", _ex_splash) +# Skipt auch wenn Plugin bereits in dieser Session geladen ist (z.B. Cmd+N). +if not sc.sticky.get("_dossier_startup_scheduled"): + try: + import _startup_splash as _splash_first + _splash_first.show() + except Exception as _ex_splash: + print("[STARTUP] splash early:", _ex_splash) # DIAGNOSE — welcher Python-Engine laeuft hier wirklich? Einmalig beim Start. print("[STARTUP] Python: {}".format(sys.version)) @@ -299,6 +301,10 @@ def _load_all(sender, e): Rhino.RhinoDoc.EndOpenDocument += _on_doc_opened except Exception as ex: print("[STARTUP] EndOpenDocument-Hook:", ex) + try: + Rhino.RhinoDoc.NewDocument += _on_doc_opened + except Exception as ex: + print("[STARTUP] NewDocument-Hook:", ex) # Projekt-Config bestimmt, welche Module geladen werden. Ohne Config # (kein Launcher benutzt, oder Datei nicht da) laedt der Host alles. config = _read_project_config() diff --git a/src/components/ContextMenu.css b/src/components/ContextMenu.css new file mode 100644 index 0000000..33f967a --- /dev/null +++ b/src/components/ContextMenu.css @@ -0,0 +1,122 @@ +.ctx-menu { + position: fixed; + padding: 5px; + min-width: 200px; + background: var(--bg-dialog); + border: 1px solid var(--border); + border-radius: 13px; + box-shadow: + 0 2px 6px rgba(0,0,0,0.10), + 0 8px 24px rgba(0,0,0,0.18), + 0 0 0 0.5px var(--border); + z-index: 300; + animation: ctx-in 100ms cubic-bezier(0.2, 0, 0.13, 1) both; +} + +@media (prefers-color-scheme: dark) { + .ctx-menu { + box-shadow: + 0 2px 8px rgba(0,0,0,0.4), + 0 12px 32px rgba(0,0,0,0.55), + 0 0 0 0.5px var(--border); + } +} + +@keyframes ctx-in { + from { opacity: 0; transform: scale(0.94) translateY(-5px); } + to { opacity: 1; transform: scale(1) translateY(0); } +} + +.ctx-title { + padding: 6px 12px 4px; + font-size: 10px; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--text-muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.ctx-divider { + height: 1px; + background: var(--border-light); + margin: 4px 2px; +} + +.ctx-item { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: 5px 12px; + font-size: 11.5px; + letter-spacing: 0.01em; + font-family: var(--font); + font-weight: 400; + color: var(--text-primary); + background: transparent; + border: none; + border-radius: 999px; + text-align: left; + cursor: pointer; + transition: background 100ms ease, color 100ms ease; +} + +.ctx-item:hover:not(:disabled) { + background: var(--accent-dim); + color: var(--accent); +} + +.ctx-item:hover:not(:disabled) .ctx-item__icon { + color: var(--accent); +} + +.ctx-item:active:not(:disabled) { + background: var(--active-dim); +} + +.ctx-item:disabled { + opacity: 0.38; + cursor: default; +} + +.ctx-item--danger { + color: var(--danger); +} + +.ctx-item--danger .ctx-item__icon { + color: var(--danger) !important; +} + +.ctx-item--danger:hover:not(:disabled) { + color: var(--danger); + background: color-mix(in srgb, var(--danger) 10%, transparent); +} + +.ctx-item__icon { + color: var(--text-secondary); + flex-shrink: 0; + transition: color 100ms ease; +} + +.ctx-item__icon-gap { + width: 14px; + flex-shrink: 0; +} + +.ctx-item__label { + flex: 1; +} + +.ctx-item__shortcut { + font-size: 9px; + font-family: var(--font-mono); + color: var(--text-muted); + background: var(--bg-item); + border: 1px solid var(--border-light); + border-radius: 999px; + padding: 1px 6px; + letter-spacing: 0.04em; +} diff --git a/src/components/ContextMenu.jsx b/src/components/ContextMenu.jsx index 6c23efb..a270b39 100644 --- a/src/components/ContextMenu.jsx +++ b/src/components/ContextMenu.jsx @@ -2,12 +2,12 @@ // Copyright (C) 2026 Karim Gabriele Varano import { useEffect, useRef, useState } from 'react' import Icon from './Icon' +import './ContextMenu.css' -export default function ContextMenu({ x, y, items, onClose }) { +export default function ContextMenu({ x, y, items, onClose, title }) { const ref = useRef(null) const [pos, setPos] = useState({ left: x, top: y }) - // Falls Menue rechts/unten ueberlaufen wuerde, links/oben verschieben useEffect(() => { if (!ref.current) return const rect = ref.current.getBoundingClientRect() @@ -41,47 +41,26 @@ export default function ContextMenu({ x, y, items, onClose }) { }, [onClose]) return ( -
+
+ {title && <> +
{title}
+
+ } {items.map((it, i) => ( it.divider ? ( -
+
) : ( ) ))} diff --git a/src/components/EbenenManager.jsx b/src/components/EbenenManager.jsx index f854568..a3311e6 100644 --- a/src/components/EbenenManager.jsx +++ b/src/components/EbenenManager.jsx @@ -527,7 +527,8 @@ export default function EbenenManager({ const openContextMenu = (ev, code) => { ev.preventDefault() ev.stopPropagation() - setCtxMenu({ x: ev.clientX, y: ev.clientY, code }) + const label = _findInTree(ebenen, code)?.name || code + setCtxMenu({ x: ev.clientX, y: ev.clientY, code, label }) } const ctxItems = (code) => [ @@ -707,6 +708,7 @@ export default function EbenenManager({ {ctxMenu && ( setCtxMenu(null)} /> diff --git a/src/components/GeschossManager.jsx b/src/components/GeschossManager.jsx index 179d1b3..f9a4f69 100644 --- a/src/components/GeschossManager.jsx +++ b/src/components/GeschossManager.jsx @@ -435,7 +435,8 @@ export default function GeschossManager({ const openContextMenu = (ev, id) => { ev.preventDefault(); ev.stopPropagation() - setCtxMenu({ x: ev.clientX, y: ev.clientY, id }) + const label = zeichnungsebenen.find(x => x.id === id)?.name || id + setCtxMenu({ x: ev.clientX, y: ev.clientY, id, label }) } const ctxItems = (id) => { @@ -557,6 +558,7 @@ export default function GeschossManager({ {ctxMenu && ( setCtxMenu(null)} /> diff --git a/src/index.css b/src/index.css index daacda1..87996c6 100644 --- a/src/index.css +++ b/src/index.css @@ -2,6 +2,16 @@ @import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200'); +* { + -webkit-user-select: none; + user-select: none; +} + +input, textarea { + -webkit-user-select: text; + user-select: text; +} + .material-symbols-outlined { font-family: 'Material Symbols Outlined'; font-weight: normal; diff --git a/src/main.jsx b/src/main.jsx index 57eea42..4356daa 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -62,6 +62,8 @@ const RootApp = mode === 'gestaltung' ? GestaltungApp : mode === 'elemente_properties' ? ElementePropertiesApp : App +document.addEventListener('contextmenu', e => e.preventDefault(), true) + window.onerror = function (msg, src, line, col, err) { document.body.style.cssText = 'background:#1c1c1e;margin:0;padding:12px' document.body.innerHTML =