UX: WebView native-feel + Context-Menu Redesign + Startup-Fix

- 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
This commit is contained in:
2026-06-06 05:27:48 +02:00
parent 18443b60c3
commit 92b4baa285
8 changed files with 185 additions and 43 deletions
+21 -2
View File
@@ -267,10 +267,12 @@ def _build_inline_template():
html = f.read().decode("utf-8")
placeholder_script = '<script>' + _MODE_SCRIPT_PLACEHOLDER + '</script>'
_no_select = '<style>*{-webkit-user-select:none!important;user-select:none!important;}</style>'
_no_ctx = '<script>document.addEventListener("contextmenu",function(e){e.preventDefault();},true);</script>'
if "</head>" in html:
html = html.replace("</head>", placeholder_script + "</head>")
html = html.replace("</head>", placeholder_script + _no_select + _no_ctx + "</head>")
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
+11 -5
View File
@@ -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()
+122
View File
@@ -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;
}
+13 -34
View File
@@ -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 (
<div
ref={ref}
style={{
position: 'fixed',
top: pos.top, left: pos.left,
background: 'var(--bg-dialog)',
border: '1px solid var(--border)',
borderRadius: 'var(--r)',
boxShadow: 'var(--shadow-3)',
padding: '4px 0',
minWidth: 180,
zIndex: 300,
}}
>
<div ref={ref} className="ctx-menu" style={{ top: pos.top, left: pos.left }}>
{title && <>
<div className="ctx-title">{title}</div>
<div className="ctx-divider" />
</>}
{items.map((it, i) => (
it.divider ? (
<div key={i} style={{ height: 1, background: 'var(--border-light)', margin: '4px 0' }} />
<div key={i} className="ctx-divider" />
) : (
<button
key={i}
disabled={it.disabled}
className={`ctx-item${it.danger ? ' ctx-item--danger' : ''}`}
onClick={() => { if (!it.disabled) { it.onClick(); onClose() } }}
onMouseEnter={(ev) => { if (!it.disabled) ev.currentTarget.style.background = 'var(--overlay-hover)' }}
onMouseLeave={(ev) => { ev.currentTarget.style.background = 'transparent' }}
style={{
width: '100%', textAlign: 'left',
padding: '6px 14px', fontSize: 11,
color: it.disabled ? 'var(--text-muted)'
: it.danger ? 'var(--danger)'
: 'var(--text-primary)',
display: 'flex', alignItems: 'center', gap: 10,
borderRadius: 0,
cursor: it.disabled ? 'default' : 'pointer',
background: 'transparent',
}}
>
{it.icon
? <Icon name={it.icon} size={14} style={{ color: it.disabled ? 'var(--text-muted)' : it.danger ? 'var(--danger)' : 'var(--text-secondary)' }} />
: <span style={{ width: 14 }} />}
<span style={{ flex: 1 }}>{it.label}</span>
{it.shortcut && <span style={{ fontSize: 9, color: 'var(--text-muted)', fontFamily: 'var(--font-mono)' }}>{it.shortcut}</span>}
? <span className="ctx-item__icon"><Icon name={it.icon} size={14} /></span>
: <span className="ctx-item__icon-gap" />}
<span className="ctx-item__label">{it.label}</span>
{it.shortcut && <span className="ctx-item__shortcut">{it.shortcut}</span>}
</button>
)
))}
+3 -1
View File
@@ -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 && (
<ContextMenu
x={ctxMenu.x} y={ctxMenu.y}
title={ctxMenu.label}
items={ctxItems(ctxMenu.code)}
onClose={() => setCtxMenu(null)}
/>
+3 -1
View File
@@ -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 && (
<ContextMenu
x={ctxMenu.x} y={ctxMenu.y}
title={ctxMenu.label}
items={ctxItems(ctxMenu.id)}
onClose={() => setCtxMenu(null)}
/>
+10
View File
@@ -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;
+2
View File
@@ -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 =