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:
+21
-2
@@ -267,10 +267,12 @@ def _build_inline_template():
|
|||||||
html = f.read().decode("utf-8")
|
html = f.read().decode("utf-8")
|
||||||
|
|
||||||
placeholder_script = '<script>' + _MODE_SCRIPT_PLACEHOLDER + '</script>'
|
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:
|
if "</head>" in html:
|
||||||
html = html.replace("</head>", placeholder_script + "</head>")
|
html = html.replace("</head>", placeholder_script + _no_select + _no_ctx + "</head>")
|
||||||
else:
|
else:
|
||||||
html = placeholder_script + html
|
html = placeholder_script + _no_select + _no_ctx + html
|
||||||
|
|
||||||
def inline_css(m):
|
def inline_css(m):
|
||||||
p = os.path.join(dist_dir, m.group(1).lstrip("./").replace("/", os.sep))
|
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;")
|
wv.ExecuteScript("window.RHINO_MODE=true;")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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):
|
def on_idle(s, e):
|
||||||
Rhino.RhinoApp.Idle -= on_idle
|
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):
|
def on_loaded(s, e):
|
||||||
try: wv.ExecuteScript("window.RHINO_MODE=true;")
|
try: wv.ExecuteScript("window.RHINO_MODE=true;")
|
||||||
except Exception: pass
|
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.DocumentTitleChanged += on_title_
|
||||||
wv.DocumentLoaded += on_loaded
|
wv.DocumentLoaded += on_loaded
|
||||||
|
|||||||
+8
-2
@@ -23,10 +23,12 @@ if _HERE not in sys.path:
|
|||||||
# Nutzer waehrend Python-Imports + Panel-Registrierung nicht in eine schwarze
|
# Nutzer waehrend Python-Imports + Panel-Registrierung nicht in eine schwarze
|
||||||
# Rhino-Oberflaeche schaut. Skipt automatisch wenn Launcher seinen eigenen
|
# Rhino-Oberflaeche schaut. Skipt automatisch wenn Launcher seinen eigenen
|
||||||
# Splash zeigt (Owner-Marker-Check).
|
# Splash zeigt (Owner-Marker-Check).
|
||||||
try:
|
# 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
|
import _startup_splash as _splash_first
|
||||||
_splash_first.show()
|
_splash_first.show()
|
||||||
except Exception as _ex_splash:
|
except Exception as _ex_splash:
|
||||||
print("[STARTUP] splash early:", _ex_splash)
|
print("[STARTUP] splash early:", _ex_splash)
|
||||||
|
|
||||||
# DIAGNOSE — welcher Python-Engine laeuft hier wirklich? Einmalig beim Start.
|
# DIAGNOSE — welcher Python-Engine laeuft hier wirklich? Einmalig beim Start.
|
||||||
@@ -299,6 +301,10 @@ def _load_all(sender, e):
|
|||||||
Rhino.RhinoDoc.EndOpenDocument += _on_doc_opened
|
Rhino.RhinoDoc.EndOpenDocument += _on_doc_opened
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[STARTUP] EndOpenDocument-Hook:", 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
|
# Projekt-Config bestimmt, welche Module geladen werden. Ohne Config
|
||||||
# (kein Launcher benutzt, oder Datei nicht da) laedt der Host alles.
|
# (kein Launcher benutzt, oder Datei nicht da) laedt der Host alles.
|
||||||
config = _read_project_config()
|
config = _read_project_config()
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -2,12 +2,12 @@
|
|||||||
// Copyright (C) 2026 Karim Gabriele Varano
|
// Copyright (C) 2026 Karim Gabriele Varano
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import Icon from './Icon'
|
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 ref = useRef(null)
|
||||||
const [pos, setPos] = useState({ left: x, top: y })
|
const [pos, setPos] = useState({ left: x, top: y })
|
||||||
|
|
||||||
// Falls Menue rechts/unten ueberlaufen wuerde, links/oben verschieben
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ref.current) return
|
if (!ref.current) return
|
||||||
const rect = ref.current.getBoundingClientRect()
|
const rect = ref.current.getBoundingClientRect()
|
||||||
@@ -41,47 +41,26 @@ export default function ContextMenu({ x, y, items, onClose }) {
|
|||||||
}, [onClose])
|
}, [onClose])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div ref={ref} className="ctx-menu" style={{ top: pos.top, left: pos.left }}>
|
||||||
ref={ref}
|
{title && <>
|
||||||
style={{
|
<div className="ctx-title">{title}</div>
|
||||||
position: 'fixed',
|
<div className="ctx-divider" />
|
||||||
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,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{items.map((it, i) => (
|
{items.map((it, i) => (
|
||||||
it.divider ? (
|
it.divider ? (
|
||||||
<div key={i} style={{ height: 1, background: 'var(--border-light)', margin: '4px 0' }} />
|
<div key={i} className="ctx-divider" />
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
key={i}
|
key={i}
|
||||||
disabled={it.disabled}
|
disabled={it.disabled}
|
||||||
|
className={`ctx-item${it.danger ? ' ctx-item--danger' : ''}`}
|
||||||
onClick={() => { if (!it.disabled) { it.onClick(); onClose() } }}
|
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
|
{it.icon
|
||||||
? <Icon name={it.icon} size={14} style={{ color: it.disabled ? 'var(--text-muted)' : it.danger ? 'var(--danger)' : 'var(--text-secondary)' }} />
|
? <span className="ctx-item__icon"><Icon name={it.icon} size={14} /></span>
|
||||||
: <span style={{ width: 14 }} />}
|
: <span className="ctx-item__icon-gap" />}
|
||||||
<span style={{ flex: 1 }}>{it.label}</span>
|
<span className="ctx-item__label">{it.label}</span>
|
||||||
{it.shortcut && <span style={{ fontSize: 9, color: 'var(--text-muted)', fontFamily: 'var(--font-mono)' }}>{it.shortcut}</span>}
|
{it.shortcut && <span className="ctx-item__shortcut">{it.shortcut}</span>}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -527,7 +527,8 @@ export default function EbenenManager({
|
|||||||
const openContextMenu = (ev, code) => {
|
const openContextMenu = (ev, code) => {
|
||||||
ev.preventDefault()
|
ev.preventDefault()
|
||||||
ev.stopPropagation()
|
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) => [
|
const ctxItems = (code) => [
|
||||||
@@ -707,6 +708,7 @@ export default function EbenenManager({
|
|||||||
{ctxMenu && (
|
{ctxMenu && (
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
x={ctxMenu.x} y={ctxMenu.y}
|
x={ctxMenu.x} y={ctxMenu.y}
|
||||||
|
title={ctxMenu.label}
|
||||||
items={ctxItems(ctxMenu.code)}
|
items={ctxItems(ctxMenu.code)}
|
||||||
onClose={() => setCtxMenu(null)}
|
onClose={() => setCtxMenu(null)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -435,7 +435,8 @@ export default function GeschossManager({
|
|||||||
|
|
||||||
const openContextMenu = (ev, id) => {
|
const openContextMenu = (ev, id) => {
|
||||||
ev.preventDefault(); ev.stopPropagation()
|
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) => {
|
const ctxItems = (id) => {
|
||||||
@@ -557,6 +558,7 @@ export default function GeschossManager({
|
|||||||
{ctxMenu && (
|
{ctxMenu && (
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
x={ctxMenu.x} y={ctxMenu.y}
|
x={ctxMenu.x} y={ctxMenu.y}
|
||||||
|
title={ctxMenu.label}
|
||||||
items={ctxItems(ctxMenu.id)}
|
items={ctxItems(ctxMenu.id)}
|
||||||
onClose={() => setCtxMenu(null)}
|
onClose={() => setCtxMenu(null)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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=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');
|
@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 {
|
.material-symbols-outlined {
|
||||||
font-family: 'Material Symbols Outlined';
|
font-family: 'Material Symbols Outlined';
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ const RootApp = mode === 'gestaltung' ? GestaltungApp
|
|||||||
: mode === 'elemente_properties' ? ElementePropertiesApp
|
: mode === 'elemente_properties' ? ElementePropertiesApp
|
||||||
: App
|
: App
|
||||||
|
|
||||||
|
document.addEventListener('contextmenu', e => e.preventDefault(), true)
|
||||||
|
|
||||||
window.onerror = function (msg, src, line, col, err) {
|
window.onerror = function (msg, src, line, col, err) {
|
||||||
document.body.style.cssText = 'background:#1c1c1e;margin:0;padding:12px'
|
document.body.style.cssText = 'background:#1c1c1e;margin:0;padding:12px'
|
||||||
document.body.innerHTML =
|
document.body.innerHTML =
|
||||||
|
|||||||
Reference in New Issue
Block a user