diff --git a/manual_trading_hub/static/app.css b/manual_trading_hub/static/app.css index c22a7da..7ee6c21 100644 --- a/manual_trading_hub/static/app.css +++ b/manual_trading_hub/static/app.css @@ -2928,6 +2928,72 @@ body.login-page { pointer-events: auto; } +.market-draw-menu { + position: fixed; + z-index: 1200; + min-width: 168px; + padding: 4px 0; + border: 1px solid var(--border-soft); + border-radius: 8px; + background: var(--panel-bg, #1a1f2e); + box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45); +} + +.market-draw-menu.hidden { + display: none; +} + +.market-draw-menu-head { + padding: 6px 12px 4px; + font-size: 0.72rem; + font-weight: 600; + color: var(--muted); + text-transform: none; +} + +.market-draw-menu-item { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 7px 12px; + border: 0; + background: transparent; + color: var(--text); + font-size: 0.82rem; + font-family: var(--font); + text-align: left; + cursor: pointer; +} + +.market-draw-menu-item:hover:not(:disabled) { + background: var(--inset-surface); +} + +.market-draw-menu-item:disabled { + opacity: 0.45; + cursor: not-allowed; +} + +.market-draw-menu-item.is-danger { + color: #f87171; +} + +.market-draw-menu-sep { + border: 0; + border-top: 1px solid var(--border-soft); + margin: 4px 0; +} + +.market-draw-menu-kbd { + margin-left: 12px; + padding: 1px 5px; + border-radius: 4px; + background: var(--inset-surface); + color: var(--muted); + font-size: 0.68rem; +} + .market-exchange-badge { position: absolute; left: 50%; diff --git a/manual_trading_hub/static/chart_draw.js b/manual_trading_hub/static/chart_draw.js index bfdc387..d04276b 100644 --- a/manual_trading_hub/static/chart_draw.js +++ b/manual_trading_hub/static/chart_draw.js @@ -64,6 +64,9 @@ let dragActive = false; let dragStartPx = null; let pathPreviewPt = null; + let menuEl = null; + let unsubClick = null; + let mainBound = false; function uid() { return "d" + Date.now().toString(36) + Math.random().toString(36).slice(2, 7); @@ -685,18 +688,25 @@ const h = hostEl.clientHeight; ctx.clearRect(0, 0, w, h); drawings.forEach(function (d) { + if (d.hidden) ctx.globalAlpha = 0.14; renderDrawing(ctx, d, w, h, d.id === selectedId); + if (d.hidden) ctx.globalAlpha = 1; }); if (draft) { const preview = draft.type === "path" ? pathPreviewPt : null; renderDrawing(ctx, draft, w, h, true, preview); } + if (activeTool === "cursor" && selectedId) { + const sel = getDrawingById(selectedId); + if (sel) drawSelectionOverlay(ctx, sel); + } } function commitDrawing(d) { if (!d || !d.type) return; d.id = uid(); drawings.push(d); + selectedId = d.id; draft = null; pathPreviewPt = null; saveDrawings(); @@ -719,17 +729,222 @@ return 0; } - function tryEraseAt(x, y) { + function getDrawingById(id) { + for (let i = 0; i < drawings.length; i++) { + if (drawings[i].id === id) return drawings[i]; + } + return null; + } + + function pickDrawingAt(x, y) { for (let i = drawings.length - 1; i >= 0; i--) { - if (hitTestDrawing(drawings[i], x, y)) { - drawings.splice(i, 1); - selectedId = null; - saveDrawings(); - scheduleRedraw(); - return true; + if (hitTestDrawing(drawings[i], x, y)) return drawings[i]; + } + return null; + } + + function selectDrawing(id) { + selectedId = id || null; + scheduleRedraw(); + } + + function deselectDrawing() { + if (!selectedId) return; + selectedId = null; + hideContextMenu(); + scheduleRedraw(); + } + + function removeDrawing(id, opts) { + if (!id) return; + const force = !!(opts && opts.force); + const d = getDrawingById(id); + if (!d) return; + if (d.locked && !force) return; + drawings = drawings.filter(function (item) { + return item.id !== id; + }); + if (selectedId === id) selectedId = null; + hideContextMenu(); + saveDrawings(); + scheduleRedraw(); + } + + function removeSelectedDrawing() { + if (!selectedId) return; + removeDrawing(selectedId); + } + + function cloneDrawing(id) { + const src = getDrawingById(id); + if (!src || src.locked) return; + const copy = JSON.parse(JSON.stringify(src)); + copy.id = uid(); + copy.locked = false; + const candles = getCandles(); + const timeStep = candles.length > 1 ? Math.abs(candles[1].time - candles[0].time) : 60; + copy.points = (copy.points || []).map(function (p, idx) { + return { + time: p.time + timeStep * (idx + 1), + price: p.price * 1.001, + }; + }); + if (copy.text) copy.text = String(copy.text); + drawings.push(copy); + selectedId = copy.id; + saveDrawings(); + scheduleRedraw(); + } + + function toggleDrawingLock(id) { + const d = getDrawingById(id); + if (!d) return; + d.locked = !d.locked; + saveDrawings(); + scheduleRedraw(); + } + + function toggleDrawingHide(id) { + const d = getDrawingById(id); + if (!d) return; + d.hidden = !d.hidden; + if (d.hidden && selectedId === id) selectedId = null; + hideContextMenu(); + saveDrawings(); + scheduleRedraw(); + } + + function isTypingTarget(el) { + if (!el) return false; + const tag = (el.tagName || "").toUpperCase(); + return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || !!el.isContentEditable; + } + + function ensureContextMenu() { + if (menuEl) return menuEl; + menuEl = document.createElement("div"); + menuEl.id = "market-draw-menu"; + menuEl.className = "market-draw-menu hidden"; + menuEl.setAttribute("role", "menu"); + document.body.appendChild(menuEl); + menuEl.addEventListener("click", function (ev) { + const btn = ev.target.closest("[data-action]"); + if (!btn || btn.disabled) return; + const action = btn.getAttribute("data-action"); + const id = menuEl._targetId; + if (!id) return; + ev.preventDefault(); + ev.stopPropagation(); + if (action === "clone") cloneDrawing(id); + else if (action === "toggle-lock") toggleDrawingLock(id); + else if (action === "toggle-hide") toggleDrawingHide(id); + else if (action === "remove") removeDrawing(id, { force: true }); + if (action !== "remove" && action !== "toggle-hide") { + const d = getDrawingById(id); + if (d) showContextMenu(menuEl._clientX, menuEl._clientY, d); + else hideContextMenu(); + } + }); + document.addEventListener("pointerdown", function (ev) { + if (!menuEl || menuEl.classList.contains("hidden")) return; + if (!menuEl.contains(ev.target)) hideContextMenu(); + }); + return menuEl; + } + + function hideContextMenu() { + if (!menuEl) return; + menuEl.classList.add("hidden"); + menuEl._targetId = null; + } + + function showContextMenu(clientX, clientY, d) { + if (!d) return; + const menu = ensureContextMenu(); + const label = TOOL_LABELS[d.type] || d.type; + const locked = !!d.locked; + menu.innerHTML = + '
" + + '" + + '" + + '" + + '