Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e96ca22ef3 | ||
|
|
44afaa3133 | ||
|
|
ce46b5d97e |
@@ -13,6 +13,10 @@ You are the Lead Systems Architect specializing in Godot 4 and idiomatic GDScrip
|
||||
3. **Design:** Define API payload shapes, static GDScript type hints, node hierarchies, and signal flows.
|
||||
4. **Log:** Append trade-offs to `docs/architecture/tech_debt_and_optimizations.md`.
|
||||
|
||||
## Feature worktree
|
||||
|
||||
Before writing a feature document, confirm that the orchestrator has asked the user whether this task should use a Git worktree and supplied the selected project path and branch. If the user has not chosen yet, ask the orchestrator to get that preference before writing. Use the supplied path whether it is a worktree or the current checkout; do not create a worktree or switch branches yourself. Follow `AGENTS.md`.
|
||||
|
||||
### Shared Stage inspector UI
|
||||
For an in-stage selected-object inspector, first read `docs/specs/stage_panel_ui_system.md`
|
||||
and inspect `scripts/shared/stage_ui/` plus the `stage_panels` theme tokens. Use the existing
|
||||
|
||||
@@ -32,6 +32,8 @@ Clothing currently has no standalone format. Author clothing as layered shapes i
|
||||
|
||||
## Review workflow
|
||||
|
||||
Before creating an asset review bundle, confirm that the orchestrator has asked the user whether this task should use a Git worktree and supplied the selected project path and branch. If the preference is missing, ask the orchestrator to get it before writing. Use the selected path whether it is a worktree or the current checkout; do not create a worktree or switch branches yourself. Follow `AGENTS.md`.
|
||||
|
||||
Create every unapproved deliverable under `content/review/artist/<request-slug>/`. Use descriptive, versioned filenames and never overwrite an existing production asset unless the user explicitly requests replacement.
|
||||
|
||||
Each review bundle must contain:
|
||||
|
||||
@@ -40,6 +40,7 @@ Wait for the user to answer before analyzing or proposing feature work.
|
||||
|
||||
## Editing limits
|
||||
|
||||
- **Worktree choice:** Before preparing an accepted phase brief or updating the backlog, confirm that the orchestrator has asked the user whether this task should use a Git worktree and supplied the selected project path and branch. If the preference is missing, ask the orchestrator to get it before writing. Use the supplied path whether it is a worktree or the current checkout. Do not create a worktree, switch branches, or write into another checkout yourself. Follow `AGENTS.md`.
|
||||
- You may edit only `docs/ideas/feature_backlog.md` and accepted phase briefs under `docs/plans/` during normal use. Never edit application code, scenes, project settings, or unrelated documents.
|
||||
- Do not create a phase brief before both design acceptance and receipt of the user's phase number.
|
||||
- Keep idea discussion conversational and concise. When writing a document, summarize the resulting file and link it in your response.
|
||||
|
||||
@@ -9,6 +9,7 @@ You are a Godot 4 / GDScript 2.0 implementation developer. Adhere to coding stan
|
||||
|
||||
## Rules
|
||||
|
||||
- **Worktree choice:** Before persistent edits, confirm that the orchestrator has asked the user whether this task should use a Git worktree and supplied the selected project path and branch. If the preference is missing, ask the orchestrator to get it before editing. Use the supplied path whether it is a worktree or the current checkout. Do not create a worktree, switch branches, merge, or edit another checkout yourself. Follow `AGENTS.md` for the selected path.
|
||||
- **Targeted Edits Only:** Do NOT output full unchanged files. Output only modified functions, concise diffs, or specific node configurations.
|
||||
- **Static Typing:** Enforce explicit type hints for all signatures and variables.
|
||||
- **Decoupling:** Follow "call down, signal up".
|
||||
|
||||
@@ -5,6 +5,19 @@ model_reasoning_effort = "high"
|
||||
sandbox_mode = "read-only"
|
||||
|
||||
developer_instructions = """
|
||||
## Worktree choice
|
||||
|
||||
You are read-only and must not create worktrees or write project files. For any handoff that will create project files, confirm that the orchestrator has asked the user whether to use a worktree and supplied the selected project path and branch. Do not require a worktree when the user chooses the current checkout.
|
||||
|
||||
## 0. Editor Visual Style Guide
|
||||
|
||||
Before making, mocking, reviewing, or suggesting any Prop Editor or Stickman Editor UI/UX
|
||||
change, read `docs/guides/editor_visual_style_guide.md`. Use its documented implemented
|
||||
patterns and shared theme ownership as the starting point, then check relevant feature specs
|
||||
and current controls for workflow-specific behavior. Clearly distinguish current behavior
|
||||
from recommendations. If a proposal intentionally departs from the guide, explain why and
|
||||
identify the departure in the proposal or spec. Follow explicit user direction when it differs.
|
||||
|
||||
## 1. Kid-Friendly UI Guardrails (10-Year-Old Persona)
|
||||
|
||||
* **Shared Stage Inspector UI:** Before proposing an in-stage selected-object inspector, read
|
||||
|
||||
@@ -16,4 +16,5 @@ You are a technical writer for Godot 4 / GDScript projects. You document game ar
|
||||
|
||||
1. **Targeted Edits Only:** Touch only the specific markdown sections affected by recent changes. Never output full unchanged files.
|
||||
2. **Scannable & Technical:** Lead directly with code blocks, tables, and structured lists. Eliminate marketing fluff or generic introductions.
|
||||
3. **Worktree Choice:** Before any persistent documentation work, confirm that the orchestrator has asked the user whether this task should use a Git worktree and supplied the selected project path and branch. If the preference is missing, ask the orchestrator to get it first. Use the selected path whether it is a worktree or the current checkout; do not create a worktree yourself.
|
||||
"""
|
||||
@@ -7,6 +7,10 @@ description: "Create or revise Stickman Studio props, stickmen, embedded clothin
|
||||
|
||||
Use the `artist` subagent for project graphical-asset and animation work. This skill manages delegation, review, approval, and handoff; it does not replace the Artist's visual judgment.
|
||||
|
||||
## Choose a worktree before creating files
|
||||
|
||||
Before creating an asset review bundle or other project files, ask whether the user wants this task to use a Git worktree unless they have already stated a preference. For small, self-contained asset changes, offer the current checkout as the default. Wait for the answer before creating a worktree. Follow `AGENTS.md`, and pass Artist the absolute path and branch the user selected, whether that is a worktree or the current checkout. Do not require a worktree when the user declines one.
|
||||
|
||||
## Delegate to Artist
|
||||
|
||||
Spawn `artist` with `agent_type: "artist"`. Reuse the same Artist for revisions while it is available. Tell it that other agents may share the workspace and that it must preserve unrelated changes.
|
||||
|
||||
@@ -37,6 +37,8 @@ Treat the user's clear statement that they are ready to work on the discussed, s
|
||||
|
||||
## Stage 3: Write and hand off the brief
|
||||
|
||||
Once the user is ready and the phase number is known, have Designer create the accepted product/design brief at `docs/plans/phase_<number>_<feature_slug>.md`, following `docs/plans/README.md`. The brief should cover the goal and audience, desired child outcome, behavior/flow and feedback, story or directing value where relevant, success criteria, included and excluded scope, known risks/dependencies, project references, and remaining questions for the spec-pipeline. Keep architecture/API decisions for the architect's later technical specification in `docs/specs/`.
|
||||
Once the user is ready and the phase number is known, ask whether the brief and later feature work should use a Git worktree unless the user has already stated a preference for this task. Follow the choice and the `Feature branches and worktrees` rules in `AGENTS.md`; do not create a worktree before the user answers. Use the selected checkout consistently for the brief, later specs, implementation, and verification. Pass its absolute path and current branch to Designer. Do not switch branches or write outside the selected checkout.
|
||||
|
||||
Have Designer create the accepted product/design brief at `docs/plans/phase_<number>_<feature_slug>.md`, following `docs/plans/README.md`. The brief should cover the goal and audience, desired child outcome, behavior/flow and feedback, story or directing value where relevant, success criteria, included and excluded scope, known risks/dependencies, project references, and remaining questions for the spec-pipeline. Keep architecture/API decisions for the architect's later technical specification in `docs/specs/`.
|
||||
|
||||
Do not overwrite an existing phase brief without the user's direction. Confirm the resulting file exists, update the matching backlog entry to `Planned` with a link to the brief, then tell the user where the plan is using a clickable local file link. Stop there; this skill creates the design handoff, not the technical spec or implementation.
|
||||
|
||||
@@ -5,8 +5,17 @@ description: "Stickman Studio feature workflow: UI/UX and architecture specs, im
|
||||
|
||||
## Execution Logic
|
||||
|
||||
### Feature branch and worktree setup
|
||||
|
||||
Before creating a worktree for a task, ask the user whether they want one and wait for the answer. For small, self-contained fixes or documentation changes, offer the current checkout as the default; for larger feature phases, explain the benefit of a dedicated worktree. Do not create a worktree before the user chooses. Keep the user's choice for the task from the first persistent artifact through implementation and verification.
|
||||
|
||||
Follow the `Feature branches and worktrees` rules in `AGENTS.md`: inspect `git status --short`, the current branch, and worktree list before the first persistent change. If the user chooses the current checkout, stay on its branch. If they choose a worktree, reuse a matching one or create `.worktrees/<feature-slug>` on `feature/<feature-slug>` from the integration branch, including the phase number when known. Do not switch branches, transfer uncommitted changes without direction, or merge automatically.
|
||||
|
||||
Pass every delegated agent the absolute selected project path and its branch, whether that path is a worktree or the current checkout. Tell each agent to use it for all repository reads, writes, Godot runs, and visual checks. Do not stop file-writing work solely because the user chose not to use a worktree.
|
||||
|
||||
1. **Phase 0a: UI/UX Proposal & Alternative Ideas (User-Facing Features Only)**
|
||||
- `@uiux` analyzes the user's initial idea for cognitive friction, accessibility, and kid-friendliness.
|
||||
- Before proposing, mocking, reviewing, or suggesting UI/UX changes, `@uiux` reads `docs/guides/editor_visual_style_guide.md` and checks the relevant implemented controls/specs. Treat the guide as the current visual baseline; distinguish implemented behavior from recommendations and explain intentional departures. This also applies to direct `@uiux` requests outside the pipeline.
|
||||
- For an in-stage selected-object inspector, `@uiux` first reviews `docs/specs/stage_panel_ui_system.md` and the current shared controls under `scripts/shared/stage_ui/`; proposals should build on that system unless the interaction needs justify a specific exception.
|
||||
- Follow the concept-count and visual-mockup protocol in `.codex/agents/uiux.toml`; it also applies when the user asks `@uiux` for ideas outside a formal pipeline run.
|
||||
- `@uiux` **STOP:** Waits for user feedback/selection before writing the UI spec.
|
||||
@@ -15,6 +24,7 @@ description: "Stickman Studio feature workflow: UI/UX and architecture specs, im
|
||||
2. **Phase 0b: UI Spec Generation**
|
||||
- User approves an option or provides feedback.
|
||||
- `@uiux` outputs the UI/UX specification (control layouts, widget behaviors, visual feedback states, and cursor logic).
|
||||
- Carry forward the style guide's applicable patterns and token ownership. If the approved direction departs from them, record the reason and the specific difference in the spec.
|
||||
- For Stage inspector work, name which shared components and theme tokens the design uses or proposes to extend. Treat dialogs and other editor panels as separate workflows unless their interaction model fits the inspector kit.
|
||||
|
||||
3. **Phase 1a: Architecture Proposal & Clarification**
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Godot 4+ specific ignores
|
||||
.godot/
|
||||
.worktrees/
|
||||
.images/
|
||||
/android/
|
||||
addons/
|
||||
|
||||
@@ -6,6 +6,17 @@
|
||||
- Headless tests + command-line scene playback exist (see "Command-line testing & debug playback" below); the GUI editor runs via `run/main_scene`.
|
||||
- **Main scene:** `res://scenes/main.tscn` (set as `run/main_scene` in `project.godot`; hosts the Stickman Editor)
|
||||
|
||||
## Feature branches and worktrees
|
||||
|
||||
- Before creating a Git worktree for a task, ask the user whether they want this task to use one. Do not create a worktree until they answer. For small, self-contained fixes and documentation changes, offer the current checkout as the default; for larger feature phases, explain why a worktree may help and let the user choose.
|
||||
- Read-only discussion and proposals do not need a worktree. Before the first persistent change, inspect `git status --short`, `git branch --show-current`, and `git worktree list`.
|
||||
- If the user chooses the current checkout, keep working on its current branch. Do not switch branches, stash, reset, clean, move, or overwrite unrelated work.
|
||||
- If the user chooses a worktree, reuse a matching one when present. Otherwise, create `.worktrees/<feature-slug>` with branch `feature/<feature-slug>`, based on the integration branch (usually `main`). Include the phase number in the slug when known, such as `phase-20-stage-search`. Example: `git worktree add .worktrees/phase-20-stage-search -b feature/phase-20-stage-search main`. Confirm the path and branch with `git worktree list` and `git branch --show-current`.
|
||||
- If relevant uncommitted changes need to be carried into a chosen worktree, ask how to handle them before proceeding.
|
||||
- Pass every subagent the absolute path and branch the user chose, whether that is a worktree or the current checkout. All project reads, writes, Godot commands, and visual checks must target that path. Do not require a worktree when the user chose the current checkout.
|
||||
- Do not merge feature branches automatically. Keep each feature branch available for review and merge only when the user asks or the active workflow explicitly authorizes it.
|
||||
- `.worktrees/` is a local worktree container and must remain gitignored.
|
||||
|
||||
## Repository layout
|
||||
|
||||
- `scripts/` and `scenes/` stay as separate roots with matching feature groups. Editor controllers live in `scripts/editors/{stickman,prop,stage}`; stage, stickman runtime, props, terrain, and speech systems live in `scripts/{stage,characters/stickman,props,terrain,speech}`.
|
||||
@@ -78,6 +89,10 @@ the configured `run/main_scene`.
|
||||
custom UI/UX agent defined in `.codex/agents/uiux.toml`. Treat that prefix as an explicit
|
||||
delegation request. The UI/UX agent is read-only; carry its design guidance into implementation
|
||||
separately when implementation is requested.
|
||||
- Before making, mocking, reviewing, or suggesting Prop or Stickman UI/UX changes, have `@uiux`
|
||||
read `docs/guides/editor_visual_style_guide.md` and consult the relevant implemented patterns
|
||||
and feature specs. Treat the guide as the current visual baseline; call out intentional
|
||||
departures and distinguish current behavior from recommendations.
|
||||
- For new or revised child-facing dialogs, popups, and short setup flows, follow the
|
||||
[Dialog Design Principles](docs/specs/dialog_design_principles.md). Use rounded containers
|
||||
and controls, clear borders, blue primary actions with bold white labels, comfortable button
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Editor Visual Style Guide
|
||||
|
||||
**Scope:** Establish a practical visual and interaction direction for the Prop Editor and Stickman Editor, using the Sandbox Stage UI as a product reference.
|
||||
**Status:** Guidance for future editor design and implementation. It documents current patterns and gaps; it does not assert that the editors already share the Stage theme or components.
|
||||
**Status:** Guidance for current and future editor UI. Prop and Stickman share `EditorVisualTheme` for editor chrome and selected reusable controls; Stage-only layouts and components remain Stage-specific unless explicitly generalized.
|
||||
|
||||
## 1. Product direction
|
||||
|
||||
@@ -22,9 +22,10 @@ The Sandbox Stage establishes a useful baseline for hierarchy, contrast, child-f
|
||||
| --- | --- | --- |
|
||||
| Sandbox Stage | SandboxTheme loads validated tokens from [sandbox_theme.json](../../sandbox_theme.json). Stage inspector controls live under [scripts/shared/stage_ui](../../scripts/shared/stage_ui/). | Use as the product reference for semantic colors, readable type, focus, action roles, and reusable theme access. |
|
||||
| Stage inspector | The approved [Stage panel UI system](../specs/stage_panel_ui_system.md) targets in-stage selected-object cards. | Do not insert StageInspectorPanel into editor inspectors by default. Borrow fitting rules and state behavior; use its components only if the layout and ownership match. |
|
||||
| Stage dialogs | [StageDialogStyler](../../scripts/shared/stage_ui/stage_dialog_styler.gd) styles child-facing dialogs hosted by the Stage overlay. | Reuse its visual contract. Its overlay host and lifecycle are Stage-specific unless deliberately generalized. |
|
||||
| Prop Editor | [PropTheme](../../scripts/editors/prop/prop_theme.gd) reads [prop_theme.json](../../prop_theme.json); the controller creates a local inspector Theme. The main scene uses split containers, a scrollable inspector, modes, and a nested Attach preview. | Keep values token-driven and scoped. Current Prop colors and target sizes differ from Stage tokens. |
|
||||
| Stickman Editor | [stickman_editor.gd](../../scripts/editors/stickman/stickman_editor.gd) coordinates ten BodyPartPanels and the Whole Stickman preview. The scene currently uses fixed top-bar offsets and a three-column workspace. | The canvas/preview model is established, but it has no apparent SandboxTheme or PropTheme integration. Harmonization is future work. |
|
||||
| Stage and editor dialogs | [StageDialogStyler](../../scripts/shared/stage_ui/stage_dialog_styler.gd) draws the shared confirmation card over the active scene while retaining native dialog signals. The style reads `popups.confirmation` from SandboxTheme. | Use this shared card for Prop and Stickman confirmation, recovery, and short action dialogs. Native file picker content remains native. |
|
||||
| Shared editor chrome | [EditorVisualTheme](../../scripts/shared/editor_ui/editor_visual_theme.gd) loads validated tokens from [editor_visual_theme.json](../../editor_visual_theme.json) and builds each editor's local control Theme. `EditorSelectionDropdown` and `EditorContextMenu` supply shared selection and canvas-menu styling. | Use shared semantic tokens and controls for matching editor workflows. Keep editor-specific document behavior in its owner. |
|
||||
| Prop Editor | [PropTheme](../../scripts/editors/prop/prop_theme.gd) reads [prop_theme.json](../../prop_theme.json) for material, geometry, and canvas defaults; editor chrome comes from `EditorVisualTheme`. The main scene uses split containers, a scrollable inspector, modes, and a nested Attach preview. | Keep Prop artwork/material values and canvas contrast in `PropTheme`; use shared editor tokens for chrome, inspector states, and selection dropdown styling. |
|
||||
| Stickman Editor | [stickman_editor.gd](../../scripts/editors/stickman/stickman_editor.gd) uses `EditorVisualTheme` for chrome and coordinates ten BodyPartPanels in a two-column Overview beside the Whole Stickman preview, plus a Focus view for one large panel. | Overview columns and panels expand to the workspace size, retaining a 230 × 160 px minimum and scrolling when space is short. Keep the splitter draggable, preserve panel state, and show the part title inside its panel in both views. |
|
||||
| Child-facing dialogs | [Dialog Design Principles](../specs/dialog_design_principles.md) documents the rounded, labeled-action pattern and stable resizing behavior. | Apply these rules to editor dialogs, adapting ownership and host behavior to each editor. |
|
||||
|
||||
The Prop Editor user guide at [prop_editor_user_guide.md](prop_editor_user_guide.md) describes its current controls and layout. Use it to avoid documenting proposed changes as existing behavior.
|
||||
@@ -41,7 +42,7 @@ Use color to reinforce meaning consistently. Keep labels, shapes, icons, or stat
|
||||
| Raised surface | global.surface_color #252B38FA | Use for toolbars, inspector shells, and cards. Distinguish adjacent surfaces by value, border, or spacing. |
|
||||
| Primary text | global.text_color #F5F7FA | Use for titles, control names, and important values. |
|
||||
| Secondary text | global.muted_text_color #AAB2C0 | Use for helper text and secondary status; keep readable at the chosen size. |
|
||||
| EDIT accent | mode_colors.edit #22C6FF | Suitable for active edit mode, selection accents, and editor guidance. It is not the current PropTheme accent. |
|
||||
| EDIT accent | mode_colors.edit #22C6FF | Shared editor accent for active modes, selection accents, and editor guidance through `EditorVisualTheme`. |
|
||||
| DIRECT accent | mode_colors.direct #FFB300 | Reserved for the Stage's directing mode. Do not use as a generic warning color in editors. |
|
||||
| PLAY accent | mode_colors.play #33DD77 | Reserved for Stage playback state. |
|
||||
| Primary action | Stage dialog/action blue #087DBA | Use for the single most important forward action in a dialog. |
|
||||
@@ -51,16 +52,14 @@ Use color to reinforce meaning consistently. Keep labels, shapes, icons, or stat
|
||||
|
||||
These values are current Stage reference values, not a mandate to paste literal hex values into editor controls. Resolve colors through validated theme accessors. If an editor needs a warmer or quieter local accent, define a semantic editor token and document its mapping to Stage meaning.
|
||||
|
||||
### 3.2 Current token differences to resolve during harmonization
|
||||
### 3.2 Shared chrome and intentional editor differences
|
||||
|
||||
The current palettes are related but not at parity:
|
||||
Both standalone editors now use `EditorVisualTheme` for shared chrome roles, sizing, and local control Themes. Keep these domain-specific values in their existing owner:
|
||||
|
||||
- PropTheme uses ui.colors.accent **#4FA3FF**, while SandboxTheme EDIT uses **#22C6FF**.
|
||||
- PropTheme defaults its button and row minimum heights to **36 px** and its mode button minimum height to **56 px**. Stage action targets are **48 px**, and child-facing UI guidance calls for at least **44 × 44 logical pixels**.
|
||||
- PropTheme's canvas is #1F1F1F; its inspector token is #262C39. The Stage uses #171B24F2 / #252B38FA for global background and surface.
|
||||
- The older Stickman Editor has no apparent editor theme integration. Its scene uses 32 px menu/top bars, a 28 px status bar, 4 px preview corners, and a 14 px preview title; body-part headings are around 13–14 px. It defines preview surface and borders directly in the scene.
|
||||
- `PropTheme` retains material tints, primitive defaults, outline widths, and the Prop canvas surface/grid. Keep its neutral charcoal canvas distinct from the blue-tinted editor panels. Stickman canvas grid and selection colors remain in `EditorVisualTheme.stickman_canvas` tokens.
|
||||
- `EditorVisualTheme` owns common editor surfaces, text, EDIT accent, focus, action roles, menu sizes, and minimum targets. `SandboxTheme` continues to own Stage-specific panels and dialog tokens.
|
||||
|
||||
Treat these as migration work, not evidence that the editors already match the Stage. Before implementation, decide whether to harmonize the accent, minimum targets, border/focus states, and surface roles. Preserve canvas contrast where it serves drawing readability.
|
||||
Preserve these boundaries when extending the UI: share semantic chrome roles where workflows match, and keep canvas, material, and runtime-domain colors local to their owner.
|
||||
|
||||
### 3.3 Type, spacing, and shape
|
||||
|
||||
@@ -96,7 +95,7 @@ For drawing handles, keep a contrasting backing/halo where the canvas can contai
|
||||
### 4.1 Use the right pattern for the job
|
||||
|
||||
- **In-stage selected-object inspector:** follow StageInspectorPanel, choice groups, and toggle rows in the [Stage UI spec](../specs/stage_panel_ui_system.md). Its title/Close header, fixed header with scrolling body, and safe-rect fitting are specific to a floating selection card.
|
||||
- **Editor inspector:** favor a persistent column beside the canvas, with labeled groups and vertical scrolling. The Prop Editor already uses a split container and scrollable inspector. Reuse spacing, hierarchy, colors, and control states; do not force the floating Stage card structure into this column.
|
||||
- **Editor inspector:** favor a persistent column beside the canvas, with labeled groups and vertical scrolling. The Prop Editor already uses a split container and scrollable inspector. Separate sections with rounded cards, readable headings, colorful section icons, and concise helper copy; keep related values grouped in compact tiles. Reuse Stage's hierarchy and state clarity, but do not force the floating Stage card structure into this column.
|
||||
- **Temporary targeting step:** the Stage capture bar is for a short-lived click-on-stage task. An editor can borrow the prompt-plus-visible-Cancel contract when it has a similar temporary canvas capture step, but should not copy the bar just because it is visible in Stage.
|
||||
- **Modal dialog:** follow [Dialog Design Principles](../specs/dialog_design_principles.md). Keep a labeled **Cancel**, **Done**, or **Back** action; use one visually clear primary action; preserve the clicked control's anchor when content grows; cap height and scroll content on short viewports.
|
||||
- **Canvas tools:** keep mode and document controls outside the draw area where possible. Put contextual handles and temporary prompts near the object only if they do not obscure the task.
|
||||
@@ -111,7 +110,7 @@ For drawing handles, keep a contrasting backing/halo where the canvas can contai
|
||||
6. Keep canvas-specific rendering tokens separate from chrome tokens. Grid, artwork, shape fill, selection halo, vertex handles, and preview ghosts have different contrast jobs.
|
||||
7. Do not serialize UI state unless it is part of the authored document. Tool preference, document content, preview-only state, and persistence should be deliberately separated.
|
||||
|
||||
The current code already demonstrates local theme ownership in Prop Editor (_ui_theme) and Stage token validation/cache behavior in SandboxTheme. This guide recommends extending those strengths; it does not claim the editor themes are currently unified.
|
||||
Both standalone editors build their chrome Theme from `EditorVisualTheme`; Prop also uses `PropTheme` for prop-specific art and material defaults. Stage remains the reference for fitting, dialog hierarchy, and child-facing language, but its in-stage inspector layout is not a default editor-inspector component.
|
||||
|
||||
### 4.3 Responsive workspace behavior
|
||||
|
||||
@@ -120,7 +119,22 @@ The current code already demonstrates local theme ownership in Prop Editor (_ui_
|
||||
- Use container-driven rows (FlowContainer, HBoxContainer, VBoxContainer) instead of fixed offsets for control groups that can wrap or grow with theme fonts.
|
||||
- Keep top actions and mode controls reachable at smaller window sizes. Wrap or stack controls only when necessary, and preserve their order and mode meaning.
|
||||
- Keep toolbars, status bars, and dialogs inside safe margins. A dynamic dialog should grow from a stable anchor and scroll rather than jump or recenter after each choice.
|
||||
- For Stickman Editor, the current scene uses fixed top bar offsets and a three-column layout. A responsive pass should specifically validate narrower windows, custom fonts, longer translations, and panel resizing before claiming parity with Prop Editor's split workspace.
|
||||
- Stickman Overview uses two equal, flexible columns of five body-part drawing panels beside the Whole Stickman preview. Let both columns fill the Overview pane and share extra width; let the five rows share extra height while preserving a 230 × 160 px minimum panel size. Scroll the Overview when the viewport is shorter than its minimum content size.
|
||||
- Keep the Overview/preview splitter draggable. On narrow windows, use the existing Focus view to give one body-part panel the available space; keep Whole Stickman access available through its dedicated control. Switching views must preserve each panel instance, authored geometry, selection, and view transform. In Focus, use the title inside the active panel as the only canvas heading; do not repeat it above the panel.
|
||||
- Validate the layout at wide and narrow windows, with raised fonts and resized panels, before claiming responsive behavior.
|
||||
|
||||
### 4.4 Shared confirmation dialog style
|
||||
|
||||
Use the Sandbox Stage confirmation card for child-facing confirmation, recovery, and short action dialogs in both standalone editors. The shared implementation is `StageDialogStyler`, with color, sizing, and spacing values from `SandboxTheme.popups.confirmation`.
|
||||
|
||||
- Draw a dim backdrop and a centered, rounded dark card. Use the themed card width (560 px target), 22 px inset, 18 px corners, and 2 px border; fit the card inside the safe viewport.
|
||||
- Put a contextual icon badge and a left-aligned 28 px title in the header. Put the message in a left-aligned 20 px body. Wrap longer copy and scroll the body if the viewport is short.
|
||||
- Order actions as primary, destructive, then **Cancel**. Use Stage blue for Save, Restore, and other forward actions; use the danger treatment for Discard, Start Over, and other destructive choices; use the neutral surface for Cancel. Make action labels at least 52 px high and 112 px wide, with 12 px gaps.
|
||||
- Keep the visible **Cancel** action even when Escape is supported. Do not show a native title-bar **×**. Focus the primary action when present; for a destructive-only prompt, focus Cancel.
|
||||
- Keep the native `AcceptDialog` or `ConfirmationDialog` as the signal source and open its shared overlay through `StageDialogStyler`. This keeps confirmation, custom-action, cancellation, keyboard, and safe-viewport behavior consistent.
|
||||
- Apply this pattern to Prop recovery, unsaved changes before New/Close/Stage return, off-origin save choices, anchor rename, and error acknowledgement; apply it to Stickman Start Over, unsaved changes, grid setup, and error acknowledgement. File pickers keep their native file-selection interface.
|
||||
|
||||
Treat the card as one shared product pattern. Do not create editor-local confirmation palettes or button-role variants.
|
||||
|
||||
## 5. Editor flows and state maps
|
||||
|
||||
@@ -195,7 +209,7 @@ For either editor, the expected child-facing sequence is:
|
||||
|
||||
1. State the action in the title and one short sentence.
|
||||
2. Show the effect or consequence when it may be surprising.
|
||||
3. Present one primary action, any needed destructive action, and a visible **Cancel**.
|
||||
3. Present one primary action, any needed destructive action, and a visible **Cancel**. If the only forward-looking choice is destructive (such as Start Over), keep it in the danger role and focus Cancel initially.
|
||||
4. Keep focus in the dialog, preserve the current anchor during resize, and return focus to the invoking control after close where practical.
|
||||
5. On success, close the dialog and update the affected editor state once. On cancel, leave the document unchanged.
|
||||
|
||||
@@ -208,6 +222,11 @@ This is a target interaction contract for editor dialogs. Existing controls shou
|
||||
- Support mouse and touch-size targets; target at least **44 × 44 logical pixels** for child-facing actions. Prefer Stage's **48 px** action target where the layout allows.
|
||||
- Keep keyboard navigation visible and complete: Tab reaches meaningful controls, Enter/Space activates them, Escape cancels temporary tasks, and focus does not disappear behind a selected style.
|
||||
- Keep right-click and keyboard shortcuts as optional accelerators. A visible menu item or button should remain available for important operations.
|
||||
- Keep the top-level File/Edit/View menu titles plain and use familiar emoji beside readable action labels inside their dropdowns. Match the Sandbox Stage menu scale: 22 px menu-bar labels and 26 px popup labels. Group file actions as New/Open/Recent, then Save/Save As, with separators before a distinct Rename or Start Over action where present. Use the restrained charcoal popup surface for menu-bar dropdowns; reserve the raised Studio List surface for canvas right-click menus.
|
||||
- Style single-choice editor pickers as a dark field with the chosen label and chevron. Popup rows keep readable text and may include a meaningful domain icon, as in Prop material and shape choices; do not show radio/check glyphs or an empty indicator lane. The current choice gets a full teal surface and cyan outline. Keep hover and keyboard focus visible, and give the field and rows at least 44 logical pixels of height. Use the shared `EditorSelectionDropdown` control for Prop and Stickman editor pickers.
|
||||
- Standardize Prop and Stickman canvas context menus on the **Studio List** pattern: a compact vertical list on a raised editor surface, with a subtle border, rounded corners, readable 20 px labels, and 48 px rows. Resolve surface, border, text, hover, EDIT accent, focus, and destructive colors through `EditorVisualTheme` tokens rather than local color literals.
|
||||
- Group related actions with spacing and separators, and keep each menu's actions specific to the current mode and hit target. Hover uses a lighter surface with a slim EDIT-cyan cue; keyboard focus has a distinct, high-contrast focus outline. Open with no focused row so mouse users do not mistake an action for a selection; show the focus outline after Up/Down navigation, and clear it when the pointer takes over. Give destructive actions explicit labels, separate them from regular actions, and place them last when practical; do not use color as their only cue.
|
||||
- Keep labels as the source of meaning and use icons sparingly and consistently. Open the menu near the pointer, keep the clicked point or shape edge visible, and clamp the menu within the viewport.
|
||||
- For dragging, preview changes continuously, show which handle is active, commit on release, and provide a clear cancel path when the operation is consequential or multi-step.
|
||||
- Avoid hit-test surprises: decorative labels/icons ignore mouse input; a large row toggle has one activation target; a canvas guide is visual only.
|
||||
- Preserve the world point under the cursor during zoom. Keep selected gizmos and handles visible above artwork and grid.
|
||||
@@ -228,7 +247,7 @@ Prefer concise, concrete verbs and familiar terms:
|
||||
| “Choose where this prop attaches.” | Internal terms such as “anchor payload” |
|
||||
| “Unsaved changes. Save before starting a new prop?” | Generic “Are you sure?” without naming the consequence |
|
||||
|
||||
Emoji can help distinguish common actions, as used by Prop Editor menus, but should be an optional cue rather than the only meaning. Keep the text label understandable if an emoji font is unavailable. Use the same wording for the same action in menu, dialog, toolbar, tooltip, and user guide.
|
||||
Emoji can help distinguish common actions, as used by both editor menus, but should be an optional cue rather than the only meaning. Keep the text label understandable if an emoji font is unavailable. Use the same wording for the same action in menu, dialog, toolbar, tooltip, and user guide.
|
||||
|
||||
### 6.3 Icons and emoji
|
||||
|
||||
@@ -236,7 +255,8 @@ Emoji can help distinguish common actions, as used by Prop Editor menus, but sho
|
||||
- Keep the text label as the source of meaning. Provide a tooltip or accessible name for icon-only canvas tools.
|
||||
- Use icons consistently for the same action across menus, toolbars, empty states, and dialogs. Avoid changing icon meaning between editors.
|
||||
- Avoid using an emoji's color as the only state cue; fonts and platforms may render it differently.
|
||||
- The Prop Editor already uses emoji in File and mode labels. The Stickman Editor currently uses plain menu labels. Treat a visual icon pass as a recommendation that should be applied consistently, not as a current shared convention.
|
||||
- Both editors use icons beside readable menu action labels, with separators between distinct groups. Keep top-level File/Edit/View titles plain; place icons on actions and use the same symbol for the same operation across editors.
|
||||
- In the Prop inspector, use colorful, flat icons for section headings, material choices, 2D shape choices, and artwork tags. Keep text labels authoritative. Show tag selection with the selected surface and border; do not add a separate check badge. Shape icons must depict planar 2D geometry.
|
||||
|
||||
### 6.4 Empty, loading, and error states
|
||||
|
||||
@@ -249,7 +269,7 @@ The current editors have different levels of state guidance. The following table
|
||||
| Prop load/recovery | Load failures use an error dialog; autosave recovery offers Restore or Discard. | Name the file/action in the message, preserve the current artwork on failure, and keep Restore/Discard outcomes distinct. Loading feedback should not imply that the document is already saved. |
|
||||
| Stickman panel before drawing | The ten body-part panels are the main workspace; the panel script owns drawing and selection behavior. | Give each blank panel a concise, low-contrast drawing cue if discovery testing shows it is needed. Remove it on first shape so it does not compete with artwork. |
|
||||
| Stickman load/clear | Load and save use file dialogs; Clear has a confirmation dialog; error dialogs report failures. | Keep the current document intact on a failed load. State the consequence in Clear confirmation and offer a visible cancel path. |
|
||||
| Save / dirty state | Prop Editor has status/title and autosave behavior; Stickman Editor status bar reports cursor coordinates and snap state. | Make save status easy to find in both editors. Do not treat autosave as a user-visible successful save unless the file was actually saved to the chosen destination. |
|
||||
| Save / dirty state | Prop Editor has status/title and autosave behavior; Stickman Editor's status bar reports cursor coordinates, snap state, and save state. | Keep save status easy to find in both editors. Do not treat autosave as a user-visible successful save unless the file was actually saved to the chosen destination. |
|
||||
|
||||
Do not add spinners or blocking overlays for synchronous work without evidence that the operation takes long enough to need them. If an operation becomes asynchronous, keep progress near the task and preserve access to cancellation where safe.
|
||||
|
||||
@@ -317,14 +337,13 @@ This is recommended implementation order, not a claim that every current editor
|
||||
|
||||
### Recommended harmonization order
|
||||
|
||||
1. **Agree on token ownership and names.** Define shared semantic roles for background, surface, text, muted text, EDIT accent, focus, primary/destructive action, spacing, radii, borders, and minimum targets. Keep canvas-specific colors separate.
|
||||
2. **Resolve token drift.** Decide whether Prop's #4FA3FF accent and 36 px button/row minima should move toward the Stage reference, or remain intentional editor overrides. Record the choice and add an accessibility rationale for target sizes.
|
||||
3. **Bring Stickman chrome into token ownership.** Move its scene-level colors, font sizes, spacing, and bar sizing into a validated theme source while preserving the panel/preview composition that supports drawing.
|
||||
4. **Improve responsive layout.** Replace brittle fixed offsets where font or window growth can clip controls; establish minimum column widths and wrapping behavior for both editors.
|
||||
5. **Unify states and dialogs.** Verify focus, disabled reasons, save/error status, destructive confirmation, and cancel behavior. Reuse dialog styling only after its host/lifecycle works in the editor.
|
||||
6. **Validate visually at agreed sizes.** Compare the editor screenshots with the Stage reference and adjust intentional differences explicitly.
|
||||
1. **Maintain token ownership.** `EditorVisualTheme` owns shared standalone-editor chrome; `PropTheme` owns prop-domain and canvas defaults; `SandboxTheme` owns Stage-specific UI. Document any intentional override at the token or component that owns it.
|
||||
2. **Audit remaining overrides.** Check for editor chrome values that still bypass `EditorVisualTheme`; preserve the Prop canvas surface/material palette and Stickman canvas drawing tokens where they serve artwork contrast.
|
||||
3. **Document responsive limits.** Record minimum usable widths, toolbar wrapping, how the two Stickman Overview columns share space beside the preview, and when Focus or scrolling takes over.
|
||||
4. **Audit remaining state gaps.** Verify focus, disabled reasons, save/error status, destructive confirmation, and cancel behavior. Both editors use the shared `StageDialogStyler` card for confirmation, recovery, and short action dialogs.
|
||||
5. **Validate visually at agreed sizes.** Compare the editor screenshots with the Stage reference and adjust intentional differences explicitly.
|
||||
|
||||
This order is a recommendation. It does not select a new shared-theme class or authorize a source migration; choose the theme architecture when implementation scope is defined.
|
||||
This order is a maintenance checklist for the current shared chrome and editor-specific drawing patterns; it does not propose replacing the established theme architecture.
|
||||
|
||||
### Visual QA checklist
|
||||
|
||||
@@ -344,8 +363,8 @@ This order is a recommendation. It does not select a new shared-theme class or a
|
||||
To make this guide a stronger long-term design reference, add these pieces as the editor UI evolves:
|
||||
|
||||
1. **Annotated screenshots at canonical window sizes.** Include each editor's initial screen, every mode, selection/reshape states, Attach preview, and one modal. Mark which visuals are current and which are target.
|
||||
2. **A shared token inventory.** Decide semantic names and authoritative ownership for surfaces, accents, focus, destructive actions, canvas grid, selection halo, typography, spacing, and minimum target sizes. Reconcile SandboxTheme, PropTheme, and Stickman Editor scene-level values in a planned migration.
|
||||
3. **Responsive breakpoints and minimum workspace sizes.** Record minimum usable canvas/inspector widths, when controls wrap, how the Attach preview stacks, and how Stickman Editor's three columns adapt.
|
||||
2. **A theme ownership map.** Document how `SandboxTheme`, `EditorVisualTheme`, and `PropTheme` divide Stage UI, shared editor chrome, and Prop domain/canvas values. Note intentional overrides and identify any remaining editor chrome that bypasses its shared token.
|
||||
3. **Responsive breakpoints and minimum workspace sizes.** Record minimum usable canvas/inspector widths, when controls wrap, how the Attach preview stacks, and how the two Stickman body-panel columns share space with the Whole Stickman preview.
|
||||
4. **Accessibility and contrast acceptance checks.** Specify contrast targets, keyboard tab order, focus visibility, touch target checks, color-vision-safe cues, and high-font-size behavior. The current references provide useful states and target sizes but do not establish a measured accessibility audit.
|
||||
5. **Motion and sound rules.** Define when transitions, selection flashes, save feedback, and error feedback animate or use sound; keep them brief, informative, and respectful of reduced-motion preferences.
|
||||
6. **Content and localization rules.** Include label length budgets, wrapping/truncation behavior, tooltip standards, pluralization, and translation expansion checks.
|
||||
|
||||
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 83 KiB |
@@ -21,8 +21,8 @@ Use this guide when creating or revising child-facing dialogs, popups, and short
|
||||
## Theme and implementation
|
||||
|
||||
- Put reusable colors, sizes, spacing, radii, and border widths in `SandboxTheme` or `PropTheme` tokens. Apply local stylebox overrides instead of mutating a shared theme resource.
|
||||
- For Sandbox Stage modal windows, use `StageDialogStyler` in `scripts/shared/stage_ui/stage_dialog_styler.gd` for the shared shell, typography, and primary, secondary, or destructive button roles. Custom Control-based dialogs can reuse its `panel_style()` and `style_button()` helpers.
|
||||
- Render the shared modal surface as a Control in the stage UI CanvasLayer. Keep the native `AcceptDialog` or `ConfirmationDialog` hidden as the signal source; a full-screen native Window can replace the stage with a blank surface on some render paths.
|
||||
- Use `StageDialogStyler` in `scripts/shared/stage_ui/stage_dialog_styler.gd` for Sandbox Stage and standalone Prop/Stickman confirmation, recovery, and short action dialogs. It owns the shared shell, typography, and primary, secondary, or destructive button roles through `SandboxTheme.popups.confirmation`. Custom Control-based dialogs can reuse its `panel_style()` and `style_button()` helpers.
|
||||
- Render the shared modal surface as a Control in the active scene's UI CanvasLayer. Keep the native `AcceptDialog` or `ConfirmationDialog` hidden as the signal source; a full-screen native Window can replace the active scene with a blank surface on some render paths. Native `FileDialog` contents remain native.
|
||||
- Keep larger text from clipping: use containers that can expand or wrap, and size the dialog after its visible content has been laid out.
|
||||
- Preserve clear hover, pressed, selected, focused, and disabled states. Do not use color as the only indication of a state.
|
||||
- Adapt the arrangement to each workflow while keeping this shared visual language. The Choose Scenery dialog in `scripts/shared/ui/asset_selector.gd` and its `popups.asset_selector` theme tokens are the current implementation reference.
|
||||
|
||||
@@ -98,7 +98,7 @@ StickmanEditor : Control (document, save/load, dirty, active part, view choice)
|
||||
|
||||
There is exactly one `BodyPartPanel` instance per part and one `WholeStickmanPreview` instance. The preview remains a permanent sibling of the body-part workspace in `WorkspaceRow`; it is not duplicated or serialized for a view switch. The controller records each panel's original Overview parent and index. Entering Focus reparents the active panel into `FocusPanelHost`; leaving Focus restores it at the original index. Changing focused part restores the outgoing panel before moving the incoming panel. The controller's existing panel dictionary and signal connections remain stable.
|
||||
|
||||
`StickmanEditor` owns `enum WorkspaceView { OVERVIEW, FOCUS }`, `_active_part_name: String`, `_view_choice_was_manual: bool`, and the pending view/part request. `BodyPartPanel` owns its shapes, selected shape, zoom, and pan; the preview owns its assembly transform and zoom/pan. The part picker changes `_active_part_name` without calling `BodyPartPanel.select()`, which currently selects the topmost shape and would overwrite the child's shape selection. Clicking a panel updates the active part through the existing selection signal. The active part gets a text label and visible border cue in Overview and Focus.
|
||||
`StickmanEditor` owns `enum WorkspaceView { OVERVIEW, FOCUS }`, `_active_part_name: String`, `_view_choice_was_manual: bool`, and the pending view/part request. `BodyPartPanel` owns its shapes, selected shape, zoom, and pan; the preview owns its assembly transform and zoom/pan. The part picker changes `_active_part_name` without calling `BodyPartPanel.select()`, which currently selects the topmost shape and would overwrite the child's shape selection. Clicking a panel updates the active part through the existing selection signal. The active part gets its title inside the panel and a visible border cue in Overview and Focus; Focus does not add a duplicate heading above the canvas.
|
||||
|
||||
Suggested typed additions:
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"background": "#171B24F2",
|
||||
"surface": "#252B38FA",
|
||||
"surface_hover": "#30394A",
|
||||
"selected_surface": "#103B4D",
|
||||
"text": "#F5F7FA",
|
||||
"text_muted": "#AAB2C0",
|
||||
"edit": "#22C6FF",
|
||||
@@ -24,6 +25,8 @@
|
||||
"section": 22,
|
||||
"title": 26,
|
||||
"menu": 20,
|
||||
"menu_bar": 22,
|
||||
"menu_popup": 26,
|
||||
"status": 16,
|
||||
"mode": 24,
|
||||
"prop_name": 20,
|
||||
@@ -32,7 +35,13 @@
|
||||
"spacing": { "small": 8, "medium": 12, "large": 16 },
|
||||
"radii": { "control": 8, "panel": 10 },
|
||||
"borders": { "normal": 1, "focus": 2 },
|
||||
"targets": { "button_min_height": 44, "row_min_height": 44, "mode_button_min_height": 56 }
|
||||
"targets": {
|
||||
"button_min_height": 44,
|
||||
"row_min_height": 44,
|
||||
"mode_button_min_height": 56,
|
||||
"context_menu_row_height": 48,
|
||||
"context_menu_min_width": 208
|
||||
}
|
||||
},
|
||||
"editors": { "prop": {}, "stickman": {} },
|
||||
"stickman_canvas": {
|
||||
|
||||
@@ -159,3 +159,6 @@ layout_mode = 2
|
||||
text = ""
|
||||
horizontal_alignment = 2
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="StageDialogLayer" type="CanvasLayer" parent="."]
|
||||
layer = 100
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[gd_scene load_steps=3 format=3]
|
||||
[gd_scene load_steps=4 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/editors/stickman/body_part_panel.gd" id="1_panel_script"]
|
||||
[ext_resource type="Script" path="res://scripts/shared/editor_ui/editor_context_menu.gd" id="2_context_menu_script"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBox_drawing"]
|
||||
bg_color = Color(0.12, 0.12, 0.12, 1)
|
||||
@@ -14,6 +15,7 @@ border_color = Color(0.25, 0.25, 0.25, 1)
|
||||
custom_minimum_size = Vector2(230, 160)
|
||||
offset_right = 230.0
|
||||
offset_bottom = 160.0
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
script = ExtResource("1_panel_script")
|
||||
|
||||
@@ -38,9 +40,9 @@ mouse_filter = 1
|
||||
clip_contents = true
|
||||
theme_override_styles/panel = SubResource("StyleBox_drawing")
|
||||
|
||||
[node name="ContextMenu" type="PopupMenu" parent="."]
|
||||
[node name="ContextMenu" type="PopupPanel" parent="."]
|
||||
unique_name_in_owner = true
|
||||
item_count = 0
|
||||
script = ExtResource("2_context_menu_script")
|
||||
|
||||
[node name="ColorPickerPopup" type="PopupPanel" parent="."]
|
||||
unique_name_in_owner = true
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://6wqo0sp4eij6"]
|
||||
[gd_scene load_steps=4 format=3 uid="uid://6wqo0sp4eij6"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dlacmxk257ro" path="res://scripts/editors/stickman/stickman_editor.gd" id="1_editor_script"]
|
||||
[ext_resource type="Script" uid="uid://codv3g6qw1tef" path="res://scripts/editors/stickman/whole_stickman_preview.gd" id="2_preview_script"]
|
||||
[ext_resource type="Script" path="res://scripts/shared/editor_ui/editor_selection_dropdown.gd" id="3_selection_dropdown_script"]
|
||||
|
||||
[node name="StickmanEditor" type="Control"]
|
||||
layout_mode = 3
|
||||
@@ -60,9 +61,10 @@ layout_mode = 2
|
||||
text = "Focus on Part"
|
||||
toggle_mode = true
|
||||
|
||||
[node name="PartPicker" type="OptionButton" parent="ChromeRoot/WorkspaceToolbar"]
|
||||
[node name="PartPicker" type="MenuButton" parent="ChromeRoot/WorkspaceToolbar"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
script = ExtResource("3_selection_dropdown_script")
|
||||
|
||||
[node name="AddShapeButton" type="Button" parent="ChromeRoot/WorkspaceToolbar"]
|
||||
unique_name_in_owner = true
|
||||
@@ -115,16 +117,22 @@ vertical_scroll_mode = 1
|
||||
[node name="OverviewColumns" type="HBoxContainer" parent="ChromeRoot/WorkspaceRow/BodyPartWorkspace/OverviewScroll"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="LeftColumn" type="VBoxContainer" parent="ChromeRoot/WorkspaceRow/BodyPartWorkspace/OverviewScroll/OverviewColumns"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_constants/separation = 6
|
||||
|
||||
[node name="CenterColumn" type="VBoxContainer" parent="ChromeRoot/WorkspaceRow/BodyPartWorkspace/OverviewScroll/OverviewColumns"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_constants/separation = 6
|
||||
|
||||
[node name="FocusHost" type="VBoxContainer" parent="ChromeRoot/WorkspaceRow/BodyPartWorkspace"]
|
||||
@@ -138,12 +146,6 @@ grow_vertical = 2
|
||||
visible = false
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="FocusPartLabel" type="Label" parent="ChromeRoot/WorkspaceRow/BodyPartWorkspace/FocusHost"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Head"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="FocusPanelHost" type="Control" parent="ChromeRoot/WorkspaceRow/BodyPartWorkspace/FocusHost"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
@@ -219,9 +221,9 @@ filters = PackedStringArray("*.stk ; Stickman Files")
|
||||
|
||||
[node name="ClearConfirmDialog" type="ConfirmationDialog" parent="."]
|
||||
unique_name_in_owner = true
|
||||
title = "Start Over"
|
||||
title = "Start Over?"
|
||||
ok_button_text = "Start Over"
|
||||
dialog_text = "Start over with a blank stickman?"
|
||||
dialog_text = "This will clear your current stickman. Start over?"
|
||||
|
||||
[node name="UnsavedChangesDialog" type="ConfirmationDialog" parent="."]
|
||||
unique_name_in_owner = true
|
||||
@@ -254,3 +256,6 @@ max_value = 100.0
|
||||
value = 15.0
|
||||
step = 1.0
|
||||
rounded = true
|
||||
|
||||
[node name="StageDialogLayer" type="CanvasLayer" parent="."]
|
||||
layer = 100
|
||||
|
||||
@@ -76,6 +76,7 @@ const PropFileScript := preload("res://scripts/editors/prop/prop_file.gd")
|
||||
const PropAnchorOverlayScript := preload("res://scripts/props/prop_anchor_overlay.gd")
|
||||
const PropAnchorDataScript := preload("res://scripts/props/prop_anchor_data.gd")
|
||||
const WholeStickmanPreviewScript := preload("res://scripts/editors/stickman/whole_stickman_preview.gd")
|
||||
const EditorContextMenuScript := preload("res://scripts/shared/editor_ui/editor_context_menu.gd")
|
||||
|
||||
const SELECTION_CORE_COLOR := Color.WHITE
|
||||
const SELECTION_HALO_COLOR := Color(0.0, 0.0, 0.0, 0.85)
|
||||
@@ -122,7 +123,7 @@ var _grid_size: int = 15
|
||||
var _snap_enabled: bool = false
|
||||
var _has_clipboard: bool = false
|
||||
var _create_point_armed: bool = false
|
||||
var _context_menu: PopupMenu
|
||||
var _context_menu: EditorContextMenu
|
||||
var _edit_mode: EditMode = EditMode.TRANSFORM
|
||||
var _anchor_overlay: RefCounted
|
||||
var _origin_marker_color: Color = ORIGIN_MARKER_DEFAULT_COLOR
|
||||
@@ -189,7 +190,7 @@ func _ready() -> void:
|
||||
_anchor_overlay = PropAnchorOverlayScript.new()
|
||||
_anchor_overlay.anchors_changed.connect(func(data: Dictionary) -> void: anchors_changed.emit(data); queue_redraw())
|
||||
_anchor_overlay.anchor_selected.connect(func(anchor_id: String) -> void: anchor_selected.emit(anchor_id); queue_redraw())
|
||||
_context_menu = PopupMenu.new()
|
||||
_context_menu = EditorContextMenuScript.new() as EditorContextMenu
|
||||
_context_menu.name = "ContextMenu"
|
||||
add_child(_context_menu)
|
||||
if not _context_menu.id_pressed.is_connected(_on_context_menu_id_pressed):
|
||||
@@ -983,7 +984,12 @@ func set_has_clipboard(state: bool) -> void:
|
||||
## Sets the font size of the right-click context menu (Round 4, theme-driven).
|
||||
func set_context_menu_font_size(size: int) -> void:
|
||||
if _context_menu != null:
|
||||
_context_menu.add_theme_font_size_override("font_size", size)
|
||||
_context_menu.set_font_size(size)
|
||||
|
||||
|
||||
func set_context_menu_visual_theme(visual_theme: EditorVisualTheme, editor: StringName) -> void:
|
||||
if _context_menu != null:
|
||||
_context_menu.configure(visual_theme, editor)
|
||||
|
||||
|
||||
func is_cursor_over_drawing(global_pos: Vector2) -> bool:
|
||||
@@ -1335,8 +1341,8 @@ func _handle_points_right_click(mb: InputEventMouseButton, world_pos: Vector2) -
|
||||
if context_shape >= 0 and context_vertex >= 0:
|
||||
_context_menu.set_meta("context_shape", context_shape)
|
||||
_context_menu.set_meta("context_vertex", context_vertex)
|
||||
_context_menu.add_item("Remove Point", MENU_REMOVE_POINT)
|
||||
_context_menu.popup_on_parent(Rect2(mb.global_position, Vector2.ONE))
|
||||
_context_menu.add_destructive_item("Remove Point", MENU_REMOVE_POINT)
|
||||
_context_menu.popup_at(mb.global_position, _global_point_target_rect(world_pos))
|
||||
return
|
||||
|
||||
# Segment proximity -> Create Point (selects the shape first).
|
||||
@@ -1348,7 +1354,7 @@ func _handle_points_right_click(mb: InputEventMouseButton, world_pos: Vector2) -
|
||||
queue_redraw()
|
||||
shape_selected.emit(seg_shape)
|
||||
_context_menu.add_item("Create Point", MENU_CREATE_POINT)
|
||||
_context_menu.popup_on_parent(Rect2(mb.global_position, Vector2.ONE))
|
||||
_context_menu.popup_at(mb.global_position, _global_shape_target_rect(seg_shape))
|
||||
return
|
||||
|
||||
# Interior body: select only, no menu.
|
||||
@@ -1381,17 +1387,18 @@ func _handle_transform_right_click(mb: InputEventMouseButton, world_pos: Vector2
|
||||
_context_menu.set_meta("click_position", world_pos)
|
||||
queue_redraw()
|
||||
shape_selected.emit(hit_shape)
|
||||
_context_menu.add_item("Copy", MENU_COPY)
|
||||
_context_menu.add_item("Copy Shape", MENU_COPY)
|
||||
if _has_clipboard:
|
||||
_context_menu.add_item("Paste", MENU_PASTE)
|
||||
_context_menu.add_item("Erase", MENU_DELETE)
|
||||
_context_menu.add_item("Paste Shape", MENU_PASTE)
|
||||
_context_menu.add_separator()
|
||||
_context_menu.add_item("Flip Left-Right", MENU_MIRROR_X)
|
||||
_context_menu.add_item("Flip Up-Down", MENU_MIRROR_Y)
|
||||
_context_menu.add_separator()
|
||||
_context_menu.add_item("Bring Forward", MENU_BRING_FORWARD)
|
||||
_context_menu.add_item("Send Back", MENU_SEND_BACK)
|
||||
_context_menu.popup_on_parent(Rect2(mb.global_position, Vector2.ONE))
|
||||
_context_menu.add_separator()
|
||||
_context_menu.add_destructive_item("Erase Shape", MENU_DELETE)
|
||||
_context_menu.popup_at(mb.global_position, _global_shape_target_rect(hit_shape))
|
||||
return
|
||||
|
||||
# Empty canvas -> add primitives.
|
||||
@@ -1403,8 +1410,8 @@ func _handle_transform_right_click(mb: InputEventMouseButton, world_pos: Vector2
|
||||
_context_menu.add_item("Add Circle", MENU_ADD_CIRCLE)
|
||||
if _has_clipboard:
|
||||
_context_menu.add_separator()
|
||||
_context_menu.add_item("Paste", MENU_PASTE)
|
||||
_context_menu.popup_on_parent(Rect2(mb.global_position, Vector2.ONE))
|
||||
_context_menu.add_item("Paste Shape", MENU_PASTE)
|
||||
_context_menu.popup_at(mb.global_position)
|
||||
|
||||
|
||||
func _handle_left_press(world_pos: Vector2) -> void:
|
||||
@@ -1857,6 +1864,19 @@ func _screen_to_world(pos: Vector2) -> Vector2:
|
||||
return (pos - _pan_offset) / _zoom
|
||||
|
||||
|
||||
func _global_point_target_rect(world_pos: Vector2) -> Rect2:
|
||||
var screen_pos := global_position + _pan_offset + world_pos * _zoom
|
||||
return Rect2(screen_pos - Vector2(6.0, 6.0), Vector2(12.0, 12.0))
|
||||
|
||||
|
||||
func _global_shape_target_rect(shape_idx: int) -> Rect2:
|
||||
if shape_idx < 0 or shape_idx >= shapes.size():
|
||||
return Rect2()
|
||||
var world_bounds: Rect2 = _shape_bbox(shapes[shape_idx])
|
||||
var screen_position := global_position + _pan_offset + world_bounds.position * _zoom
|
||||
return Rect2(screen_position, world_bounds.size * _zoom).grow(4.0)
|
||||
|
||||
|
||||
func _snap_to_grid(pos: Vector2) -> Vector2:
|
||||
var gs := float(_grid_size)
|
||||
if gs <= 0.0:
|
||||
|
||||
@@ -13,6 +13,8 @@ extends Control
|
||||
const PropFileScript := preload("res://scripts/editors/prop/prop_file.gd")
|
||||
const PropThemeScript := preload("res://scripts/editors/prop/prop_theme.gd")
|
||||
const EditorVisualThemeScript := preload("res://scripts/shared/editor_ui/editor_visual_theme.gd")
|
||||
const SandboxThemeScript := preload("res://scripts/stage/sandbox_theme.gd")
|
||||
const StageDialogStylerScript := preload("res://scripts/shared/stage_ui/stage_dialog_styler.gd")
|
||||
const PropBlockScript := preload("res://scripts/props/prop_block.gd")
|
||||
const PropCanvasScript := preload("res://scripts/editors/prop/prop_canvas.gd")
|
||||
const PropThumbnailScript := preload("res://scripts/shared/thumbnails/prop_thumbnail.gd")
|
||||
@@ -20,6 +22,7 @@ const ThumbnailCacheScript := preload("res://scripts/shared/thumbnails/thumbnail
|
||||
const PropAnchorDataScript := preload("res://scripts/props/prop_anchor_data.gd")
|
||||
const PropAttachPreviewScene := preload("res://scenes/editors/prop/prop_attach_preview.tscn")
|
||||
const ContentPathsScript := preload("res://scripts/shared/content_paths.gd")
|
||||
const PropInspectorIconsScript := preload("res://scripts/editors/prop/prop_inspector_icons.gd")
|
||||
|
||||
const PRESET_LABELS: PackedStringArray = ["None", "Wood", "Rubber", "Cardboard", "Metal"]
|
||||
const PRIMITIVE_IDS: PackedStringArray = ["box", "plank", "triangle", "line", "circle"]
|
||||
@@ -116,6 +119,7 @@ static func should_warn_off_origin(has_geometry: bool, bounds: Rect2) -> bool:
|
||||
|
||||
var _theme: PropTheme
|
||||
var _editor_visual_theme: EditorVisualTheme
|
||||
var _dialog_theme: SandboxTheme
|
||||
var _material_preset: int = PropBlockScript.MaterialPreset.NONE
|
||||
var _current_path: String = ""
|
||||
var _dirty: bool = false
|
||||
@@ -128,11 +132,14 @@ var _thumb_cache: ThumbnailCache
|
||||
var _ui_theme: Theme
|
||||
|
||||
# Inspector controls
|
||||
var _preset_option: OptionButton
|
||||
var _material_section: VBoxContainer
|
||||
var _preset_option: PropInspectorDropdown
|
||||
var _material_section: PanelContainer
|
||||
var _mass_label: Label
|
||||
var _mass_value: Label
|
||||
var _friction_label: Label
|
||||
var _friction_value: Label
|
||||
var _bounce_label: Label
|
||||
var _bounce_value: Label
|
||||
var _fill_button: ColorPickerButton
|
||||
var _outline_button: ColorPickerButton
|
||||
var _width_spin: SpinBox
|
||||
@@ -143,14 +150,14 @@ var _vertex_label: Label
|
||||
var _delete_button: Button
|
||||
var _create_point_button: Button
|
||||
var _create_point_hint: Label
|
||||
var _shape_section: VBoxContainer
|
||||
var _primitive_option: OptionButton
|
||||
var _shape_section: PanelContainer
|
||||
var _primitive_option: PropInspectorDropdown
|
||||
var _params_box: VBoxContainer
|
||||
var _param_spins: Dictionary = {}
|
||||
var _add_button: Button
|
||||
var _updating_inspector: bool = false
|
||||
var _tag_checks: Dictionary = {}
|
||||
var _tag_section: VBoxContainer
|
||||
var _tag_section: PanelContainer
|
||||
var _stage_authoring: bool = false
|
||||
var _stage_return_pending: bool = false
|
||||
var _stage_place_button: Button
|
||||
@@ -177,7 +184,7 @@ var _points_button: Button
|
||||
var _attach_button: Button
|
||||
|
||||
# Add-shape inspector section (hidden in Reshape mode, Round 3)
|
||||
var _add_section: VBoxContainer
|
||||
var _add_section: PanelContainer
|
||||
var _anchor_section: VBoxContainer
|
||||
var _anchor_list: ItemList
|
||||
var _anchor_role: OptionButton
|
||||
@@ -218,6 +225,8 @@ func _ready() -> void:
|
||||
get_tree().auto_accept_quit = false
|
||||
_editor_visual_theme = EditorVisualThemeScript.new()
|
||||
_editor_visual_theme.load_from(EditorVisualTheme.DEFAULT_PATH)
|
||||
_dialog_theme = SandboxThemeScript.new()
|
||||
_dialog_theme.load_from(SandboxThemeScript.DEFAULT_PATH)
|
||||
_theme = PropThemeScript.new()
|
||||
_theme.load_from(PropThemeScript.DEFAULT_PATH)
|
||||
_thumb_cache = ThumbnailCacheScript.new()
|
||||
@@ -254,6 +263,7 @@ func _ready() -> void:
|
||||
_canvas.set_grid_size(_theme.grid_size())
|
||||
_canvas.set_snap_enabled(false)
|
||||
_canvas.set_has_clipboard(false)
|
||||
_canvas.set_context_menu_visual_theme(_editor_visual_theme, &"prop")
|
||||
_canvas.set_context_menu_font_size(_theme.font_context_menu())
|
||||
_canvas.set_anchor_data(_anchors)
|
||||
var origin_marker_color: Color = _theme.text_muted_color()
|
||||
@@ -503,26 +513,26 @@ func _setup_menu_bar() -> void:
|
||||
var file_menu := PopupMenu.new()
|
||||
file_menu.name = "FileMenu"
|
||||
file_menu.add_item("🆕 New", MENU_NEW)
|
||||
file_menu.add_item("💾 Save", MENU_SAVE)
|
||||
file_menu.add_item("📄 Save As...", MENU_SAVE_AS)
|
||||
file_menu.add_item("📂 Load...", MENU_LOAD)
|
||||
file_menu.add_separator()
|
||||
file_menu.add_item("📂 Load…", MENU_LOAD)
|
||||
var recent_popup := PopupMenu.new()
|
||||
recent_popup.name = "RecentProps"
|
||||
recent_popup.id_pressed.connect(_on_recent_prop_selected)
|
||||
file_menu.add_child(recent_popup)
|
||||
file_menu.add_submenu_item("🕑 Recent", "RecentProps")
|
||||
file_menu.add_submenu_item("🕑 Open Recent", "RecentProps")
|
||||
file_menu.add_separator()
|
||||
file_menu.add_item("💾 Save", MENU_SAVE)
|
||||
file_menu.add_item("📄 Save As…", MENU_SAVE_AS)
|
||||
file_menu.id_pressed.connect(_on_menu_id_pressed)
|
||||
file_menu.about_to_popup.connect(_update_recent_menu)
|
||||
_menu_bar.add_child(file_menu)
|
||||
_menu_bar.set_menu_title(_menu_bar.get_menu_count() - 1, "📁 File")
|
||||
_menu_bar.set_menu_title(_menu_bar.get_menu_count() - 1, "File")
|
||||
|
||||
var view_menu := PopupMenu.new()
|
||||
view_menu.name = "ViewMenu"
|
||||
view_menu.add_item("🔍 Reset View", MENU_RESET_VIEWS)
|
||||
view_menu.add_item("🔍 Reset Views", MENU_RESET_VIEWS)
|
||||
view_menu.id_pressed.connect(_on_menu_id_pressed)
|
||||
_menu_bar.add_child(view_menu)
|
||||
_menu_bar.set_menu_title(_menu_bar.get_menu_count() - 1, "👁️ View")
|
||||
_menu_bar.set_menu_title(_menu_bar.get_menu_count() - 1, "View")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -617,25 +627,31 @@ func _setup_dialogs() -> void:
|
||||
_editor_visual_theme.style_button_role(_save_dialog.get_cancel_button(), &"secondary", &"prop")
|
||||
_editor_visual_theme.style_button_role(_load_dialog.get_ok_button(), &"primary", &"prop")
|
||||
_editor_visual_theme.style_button_role(_load_dialog.get_cancel_button(), &"secondary", &"prop")
|
||||
_editor_visual_theme.style_button_role(_error_dialog.get_ok_button(), &"secondary", &"prop")
|
||||
_error_dialog.get_ok_button().text = "Got it"
|
||||
StageDialogStylerScript.apply(
|
||||
_error_dialog, _dialog_theme, _dialog_theme.font(),
|
||||
_error_dialog.get_ok_button()
|
||||
)
|
||||
_build_exit_dialog()
|
||||
_rename_anchor_dialog = ConfirmationDialog.new()
|
||||
_rename_anchor_dialog.title = "Rename Anchor"
|
||||
_rename_anchor_dialog.ok_button_text = "Rename"
|
||||
_rename_anchor_dialog.theme = _ui_theme
|
||||
_editor_visual_theme.style_button_role(_rename_anchor_dialog.get_ok_button(), &"primary", &"prop")
|
||||
_editor_visual_theme.style_button_role(_rename_anchor_dialog.get_cancel_button(), &"secondary", &"prop")
|
||||
_rename_anchor_dialog.set_meta(StageDialogStylerScript.ICON_META, "📝")
|
||||
_rename_anchor_edit = LineEdit.new()
|
||||
_rename_anchor_edit.placeholder_text = "Anchor name"
|
||||
_rename_anchor_dialog.add_child(_rename_anchor_edit)
|
||||
add_child(_rename_anchor_dialog)
|
||||
StageDialogStylerScript.apply(
|
||||
_rename_anchor_dialog, _dialog_theme, _dialog_theme.font(),
|
||||
_rename_anchor_dialog.get_ok_button()
|
||||
)
|
||||
_rename_anchor_edit.text_changed.connect(_update_rename_anchor_confirmation)
|
||||
_rename_anchor_edit.text_submitted.connect(_on_rename_anchor_text_submitted)
|
||||
_rename_anchor_dialog.confirmed.connect(_commit_anchor_rename)
|
||||
_rename_anchor_dialog.canceled.connect(_clear_pending_anchor_rename)
|
||||
|
||||
func _on_rename_anchor_text_submitted(_text: String) -> void:
|
||||
if _rename_anchor_dialog == null or not _rename_anchor_dialog.visible:
|
||||
if _rename_anchor_dialog == null or not StageDialogStylerScript.is_open(_rename_anchor_dialog):
|
||||
return
|
||||
var ok_button := _rename_anchor_dialog.get_ok_button()
|
||||
if not ok_button.disabled:
|
||||
@@ -886,30 +902,31 @@ func _on_whole_prop_rotate_requested(angle_rad: float) -> void:
|
||||
|
||||
func _build_exit_dialog() -> void:
|
||||
_exit_dialog = ConfirmationDialog.new()
|
||||
_exit_dialog.title = "⚠️ Unsaved Changes"
|
||||
_exit_dialog.title = "Unsaved Changes"
|
||||
_exit_dialog.set_meta(StageDialogStylerScript.ICON_META, "💾")
|
||||
_exit_dialog.dialog_text = "You have unsaved changes. Save them before quitting?"
|
||||
_exit_dialog.ok_button_text = "💾 Save"
|
||||
var discard_button: Button = _exit_dialog.add_button("🗑️ Discard", false, "discard")
|
||||
_exit_dialog.get_cancel_button().text = "❌ Cancel"
|
||||
_exit_dialog.theme = _ui_theme
|
||||
_editor_visual_theme.style_button_role(_exit_dialog.get_ok_button(), &"primary", &"prop")
|
||||
_editor_visual_theme.style_button_role(discard_button, &"destructive", &"prop")
|
||||
_editor_visual_theme.style_button_role(_exit_dialog.get_cancel_button(), &"secondary", &"prop")
|
||||
_exit_dialog.ok_button_text = "Save"
|
||||
var discard_button: Button = _exit_dialog.add_button("Discard", false, "discard")
|
||||
_exit_dialog.get_cancel_button().text = "Cancel"
|
||||
add_child(_exit_dialog)
|
||||
StageDialogStylerScript.apply(
|
||||
_exit_dialog, _dialog_theme, _dialog_theme.font(),
|
||||
_exit_dialog.get_ok_button(), [discard_button]
|
||||
)
|
||||
_exit_dialog.confirmed.connect(_on_exit_save)
|
||||
_exit_dialog.custom_action.connect(_on_exit_custom_action)
|
||||
_exit_dialog.canceled.connect(_on_exit_cancel)
|
||||
_off_origin_save_dialog = ConfirmationDialog.new()
|
||||
_off_origin_save_dialog.title = "🎯 Center Prop?"
|
||||
_off_origin_save_dialog.dialog_text = "This prop is far from (0,0). Centering it keeps its shapes and anchors together.\n\nWhat would you like to do?"
|
||||
_off_origin_save_dialog.ok_button_text = "🎯 Center & Save"
|
||||
var save_anyway_button: Button = _off_origin_save_dialog.add_button("💾 Save Anyway", false, "save_anyway")
|
||||
_off_origin_save_dialog.get_cancel_button().text = "❌ Cancel"
|
||||
_off_origin_save_dialog.theme = _ui_theme
|
||||
_editor_visual_theme.style_button_role(_off_origin_save_dialog.get_ok_button(), &"primary", &"prop")
|
||||
_editor_visual_theme.style_button_role(save_anyway_button, &"secondary", &"prop")
|
||||
_editor_visual_theme.style_button_role(_off_origin_save_dialog.get_cancel_button(), &"secondary", &"prop")
|
||||
_off_origin_save_dialog.ok_button_text = "Center & Save"
|
||||
var save_anyway_button: Button = _off_origin_save_dialog.add_button("Save Anyway", false, "save_anyway")
|
||||
_off_origin_save_dialog.get_cancel_button().text = "Cancel"
|
||||
add_child(_off_origin_save_dialog)
|
||||
StageDialogStylerScript.apply(
|
||||
_off_origin_save_dialog, _dialog_theme, _dialog_theme.font(),
|
||||
_off_origin_save_dialog.get_ok_button(), [], [save_anyway_button]
|
||||
)
|
||||
_off_origin_save_dialog.confirmed.connect(func() -> void: await _execute_pending_save(true))
|
||||
_off_origin_save_dialog.custom_action.connect(func(action: StringName) -> void:
|
||||
if action == &"save_anyway": await _execute_pending_save(false))
|
||||
@@ -931,7 +948,7 @@ func _request_close() -> void:
|
||||
_pending_quit = true
|
||||
_pending_new = false
|
||||
_exit_dialog.dialog_text = "You have unsaved changes. Save them before quitting?"
|
||||
_exit_dialog.popup_centered()
|
||||
StageDialogStylerScript.popup(_exit_dialog)
|
||||
|
||||
|
||||
func _request_new() -> void:
|
||||
@@ -942,7 +959,7 @@ func _request_new() -> void:
|
||||
_pending_new = true
|
||||
_pending_quit = false
|
||||
_exit_dialog.dialog_text = "You have unsaved changes. Save them before starting a new prop?"
|
||||
_exit_dialog.popup_centered()
|
||||
StageDialogStylerScript.popup(_exit_dialog)
|
||||
|
||||
|
||||
func _request_stage_return() -> void:
|
||||
@@ -958,11 +975,11 @@ func _request_stage_return() -> void:
|
||||
_pending_new = false
|
||||
_pending_quit = false
|
||||
_exit_dialog.dialog_text = "Save your artwork before returning? Saving places it on the Stage."
|
||||
_exit_dialog.popup_centered()
|
||||
StageDialogStylerScript.popup(_exit_dialog)
|
||||
|
||||
|
||||
func _on_exit_save() -> void:
|
||||
_exit_dialog.hide()
|
||||
StageDialogStylerScript.hide(_exit_dialog)
|
||||
if _current_path.is_empty():
|
||||
_save_dialog.popup_centered()
|
||||
return
|
||||
@@ -971,7 +988,7 @@ func _on_exit_save() -> void:
|
||||
|
||||
func _on_exit_custom_action(action: StringName) -> void:
|
||||
if action == &"discard":
|
||||
_exit_dialog.hide()
|
||||
StageDialogStylerScript.hide(_exit_dialog)
|
||||
if not _stage_authoring:
|
||||
_clear_autosave()
|
||||
if _stage_return_pending:
|
||||
@@ -1046,47 +1063,60 @@ func _on_menu_id_pressed(id: int) -> void:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _build_inspector() -> void:
|
||||
var header_size: int = _theme.font_header()
|
||||
var row_height: int = _theme.row_min_height()
|
||||
_inspector_list.add_theme_constant_override("separation", 10)
|
||||
_inspector_list.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
|
||||
# Material preset section
|
||||
_material_section = VBoxContainer.new()
|
||||
_material_section.add_theme_constant_override("separation", 6)
|
||||
_material_section = _make_inspector_card()
|
||||
_inspector_list.add_child(_material_section)
|
||||
var material_title := Label.new()
|
||||
material_title.text = "Material"
|
||||
material_title.add_theme_font_size_override("font_size", header_size)
|
||||
_material_section.add_child(material_title)
|
||||
var material_content: VBoxContainer = _inspector_card_content(_material_section)
|
||||
material_content.add_child(_make_inspector_header("Material", PropInspectorIconsScript.section("material")))
|
||||
|
||||
_preset_option = OptionButton.new()
|
||||
for label: String in PRESET_LABELS:
|
||||
_preset_option.add_item(label)
|
||||
_preset_option = PropInspectorDropdown.new()
|
||||
for index: int in PRESET_LABELS.size():
|
||||
_preset_option.add_icon_item(
|
||||
PropInspectorIconsScript.material(index, _theme.material_tint(index, "fill")),
|
||||
PRESET_LABELS[index]
|
||||
)
|
||||
_preset_option.selected = _material_preset
|
||||
_preset_option.custom_minimum_size.y = float(_theme.button_min_height())
|
||||
_preset_option.configure(
|
||||
_ui_theme,
|
||||
_theme.inspector_background_color(),
|
||||
_editor_visual_theme.chrome_color(&"selected_surface", &"prop"),
|
||||
_theme.border_color(),
|
||||
_theme.accent_color(),
|
||||
_editor_visual_theme.chrome_color(&"focus", &"prop"),
|
||||
_theme.text_color(),
|
||||
_theme.font_body()
|
||||
)
|
||||
_preset_option.item_selected.connect(_on_preset_selected)
|
||||
_material_section.add_child(_preset_option)
|
||||
material_content.add_child(_preset_option)
|
||||
|
||||
_mass_label = Label.new()
|
||||
_friction_label = Label.new()
|
||||
_bounce_label = Label.new()
|
||||
_material_section.add_child(_mass_label)
|
||||
_material_section.add_child(_friction_label)
|
||||
_material_section.add_child(_bounce_label)
|
||||
var stats := HBoxContainer.new()
|
||||
stats.add_theme_constant_override("separation", 6)
|
||||
material_content.add_child(stats)
|
||||
var mass_tile: Array[Label] = _make_stat_tile(stats, "Mass")
|
||||
_mass_label = mass_tile[0]
|
||||
_mass_value = mass_tile[1]
|
||||
var grip_tile: Array[Label] = _make_stat_tile(stats, "Surface grip")
|
||||
_friction_label = grip_tile[0]
|
||||
_friction_value = grip_tile[1]
|
||||
var bounce_tile: Array[Label] = _make_stat_tile(stats, "Bounce")
|
||||
_bounce_label = bounce_tile[0]
|
||||
_bounce_value = bounce_tile[1]
|
||||
_update_physics_labels()
|
||||
|
||||
# Selected-shape section
|
||||
_shape_section = VBoxContainer.new()
|
||||
_shape_section.add_theme_constant_override("separation", 6)
|
||||
_shape_section = _make_inspector_card()
|
||||
_inspector_list.add_child(_shape_section)
|
||||
|
||||
var shape_title := Label.new()
|
||||
shape_title.text = "Selected Shape"
|
||||
shape_title.add_theme_font_size_override("font_size", header_size)
|
||||
_shape_section.add_child(shape_title)
|
||||
var shape_content: VBoxContainer = _inspector_card_content(_shape_section)
|
||||
shape_content.add_child(_make_inspector_header("Selected Shape", PropInspectorIconsScript.section("selected_shape")))
|
||||
|
||||
var fill_row := HBoxContainer.new()
|
||||
fill_row.custom_minimum_size.y = float(row_height)
|
||||
_shape_section.add_child(fill_row)
|
||||
shape_content.add_child(fill_row)
|
||||
var fill_label := Label.new()
|
||||
fill_label.text = "Fill:"
|
||||
fill_label.custom_minimum_size = Vector2(90.0, 0.0)
|
||||
@@ -1099,7 +1129,7 @@ func _build_inspector() -> void:
|
||||
|
||||
var outline_row := HBoxContainer.new()
|
||||
outline_row.custom_minimum_size.y = float(row_height)
|
||||
_shape_section.add_child(outline_row)
|
||||
shape_content.add_child(outline_row)
|
||||
var outline_label := Label.new()
|
||||
outline_label.text = "Outline:"
|
||||
outline_label.custom_minimum_size = Vector2(90.0, 0.0)
|
||||
@@ -1112,7 +1142,7 @@ func _build_inspector() -> void:
|
||||
|
||||
var width_row := HBoxContainer.new()
|
||||
width_row.custom_minimum_size.y = float(row_height)
|
||||
_shape_section.add_child(width_row)
|
||||
shape_content.add_child(width_row)
|
||||
var width_label := Label.new()
|
||||
width_label.text = "Width:"
|
||||
width_label.custom_minimum_size = Vector2(90.0, 0.0)
|
||||
@@ -1128,7 +1158,7 @@ func _build_inspector() -> void:
|
||||
|
||||
_radius_row = HBoxContainer.new()
|
||||
_radius_row.custom_minimum_size.y = float(row_height)
|
||||
_shape_section.add_child(_radius_row)
|
||||
shape_content.add_child(_radius_row)
|
||||
var radius_label := Label.new()
|
||||
radius_label.text = "Radius:"
|
||||
radius_label.custom_minimum_size = Vector2(90.0, 0.0)
|
||||
@@ -1145,10 +1175,10 @@ func _build_inspector() -> void:
|
||||
_reshaped_note = Label.new()
|
||||
_reshaped_note.text = "✏️ This shape was reshaped"
|
||||
_reshaped_note.add_theme_color_override("font_color", _theme.text_muted_color())
|
||||
_shape_section.add_child(_reshaped_note)
|
||||
shape_content.add_child(_reshaped_note)
|
||||
|
||||
_vertex_label = Label.new()
|
||||
_shape_section.add_child(_vertex_label)
|
||||
shape_content.add_child(_vertex_label)
|
||||
|
||||
_create_point_button = Button.new()
|
||||
_create_point_button.text = "📍 Create Point"
|
||||
@@ -1156,46 +1186,55 @@ func _build_inspector() -> void:
|
||||
_create_point_button.custom_minimum_size.y = float(_theme.button_min_height())
|
||||
_editor_visual_theme.style_button_role(_create_point_button, &"primary", &"prop")
|
||||
_create_point_button.pressed.connect(_on_create_point_pressed)
|
||||
_shape_section.add_child(_create_point_button)
|
||||
shape_content.add_child(_create_point_button)
|
||||
_create_point_hint = Label.new()
|
||||
_create_point_hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
_create_point_hint.add_theme_color_override("font_color", _theme.text_muted_color())
|
||||
_shape_section.add_child(_create_point_hint)
|
||||
shape_content.add_child(_create_point_hint)
|
||||
|
||||
_delete_button = Button.new()
|
||||
_delete_button.text = "Erase Shape"
|
||||
_delete_button.custom_minimum_size.y = float(_theme.button_min_height())
|
||||
_editor_visual_theme.style_button_role(_delete_button, &"destructive", &"prop")
|
||||
_delete_button.pressed.connect(_on_delete_shape_pressed)
|
||||
_shape_section.add_child(_delete_button)
|
||||
shape_content.add_child(_delete_button)
|
||||
|
||||
# Add-shape section (hidden in Reshape mode, Round 3)
|
||||
_add_section = VBoxContainer.new()
|
||||
_add_section.add_theme_constant_override("separation", 8)
|
||||
_add_section = _make_inspector_card()
|
||||
_inspector_list.add_child(_add_section)
|
||||
var add_content: VBoxContainer = _inspector_card_content(_add_section)
|
||||
add_content.add_child(_make_inspector_header("Add Shape", PropInspectorIconsScript.section("add_shape")))
|
||||
|
||||
var add_title := Label.new()
|
||||
add_title.text = "Add Shape"
|
||||
add_title.add_theme_font_size_override("font_size", header_size)
|
||||
_add_section.add_child(add_title)
|
||||
|
||||
_primitive_option = OptionButton.new()
|
||||
for label: String in PRIMITIVE_LABELS:
|
||||
_primitive_option.add_item(label)
|
||||
_primitive_option = PropInspectorDropdown.new()
|
||||
for index: int in PRIMITIVE_LABELS.size():
|
||||
_primitive_option.add_icon_item(
|
||||
PropInspectorIconsScript.shape(PRIMITIVE_IDS[index], _primitive_icon_color(PRIMITIVE_IDS[index])),
|
||||
PRIMITIVE_LABELS[index]
|
||||
)
|
||||
_primitive_option.custom_minimum_size.y = float(_theme.button_min_height())
|
||||
_primitive_option.configure(
|
||||
_ui_theme,
|
||||
_theme.inspector_background_color(),
|
||||
_editor_visual_theme.chrome_color(&"selected_surface", &"prop"),
|
||||
_theme.border_color(),
|
||||
_theme.accent_color(),
|
||||
_editor_visual_theme.chrome_color(&"focus", &"prop"),
|
||||
_theme.text_color(),
|
||||
_theme.font_body()
|
||||
)
|
||||
_primitive_option.item_selected.connect(_on_primitive_selected)
|
||||
_add_section.add_child(_primitive_option)
|
||||
add_content.add_child(_primitive_option)
|
||||
|
||||
_params_box = VBoxContainer.new()
|
||||
_params_box.add_theme_constant_override("separation", 4)
|
||||
_add_section.add_child(_params_box)
|
||||
add_content.add_child(_params_box)
|
||||
|
||||
_add_button = Button.new()
|
||||
_add_button.text = "Add a Shape"
|
||||
_add_button.custom_minimum_size.y = float(_theme.button_min_height())
|
||||
_editor_visual_theme.style_button_role(_add_button, &"primary", &"prop")
|
||||
_add_button.pressed.connect(_on_add_pressed)
|
||||
_add_section.add_child(_add_button)
|
||||
add_content.add_child(_add_button)
|
||||
|
||||
_apply_control_theme(_inspector_list)
|
||||
_rebuild_primitive_params()
|
||||
@@ -1204,6 +1243,117 @@ func _build_inspector() -> void:
|
||||
_refresh_tag_controls()
|
||||
|
||||
|
||||
func _make_inspector_card() -> PanelContainer:
|
||||
var card := PanelContainer.new()
|
||||
card.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = _theme.inspector_background_color().darkened(0.07)
|
||||
style.border_color = _theme.border_color()
|
||||
style.set_border_width_all(1)
|
||||
style.set_corner_radius_all(10)
|
||||
style.content_margin_left = 10.0
|
||||
style.content_margin_top = 10.0
|
||||
style.content_margin_right = 10.0
|
||||
style.content_margin_bottom = 10.0
|
||||
card.add_theme_stylebox_override("panel", style)
|
||||
var content := VBoxContainer.new()
|
||||
content.add_theme_constant_override("separation", 8)
|
||||
card.add_child(content)
|
||||
return card
|
||||
|
||||
|
||||
func _inspector_card_content(card: PanelContainer) -> VBoxContainer:
|
||||
return card.get_child(0) as VBoxContainer
|
||||
|
||||
|
||||
func _make_inspector_header(title: String, icon: Texture2D) -> HBoxContainer:
|
||||
var header := HBoxContainer.new()
|
||||
header.add_theme_constant_override("separation", 8)
|
||||
var icon_view := TextureRect.new()
|
||||
icon_view.texture = icon
|
||||
icon_view.custom_minimum_size = Vector2(30.0, 30.0)
|
||||
icon_view.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
icon_view.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
icon_view.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
header.add_child(icon_view)
|
||||
var label := Label.new()
|
||||
label.text = title
|
||||
label.add_theme_font_size_override("font_size", _theme.font_header())
|
||||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
header.add_child(label)
|
||||
return header
|
||||
|
||||
|
||||
func _make_stat_tile(parent: Control, caption_text: String) -> Array[Label]:
|
||||
var tile := PanelContainer.new()
|
||||
tile.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = _editor_visual_theme.chrome_color(&"background", &"prop").darkened(0.02)
|
||||
style.border_color = _theme.border_color()
|
||||
style.set_border_width_all(1)
|
||||
style.set_corner_radius_all(8)
|
||||
style.content_margin_left = 7.0
|
||||
style.content_margin_top = 7.0
|
||||
style.content_margin_right = 7.0
|
||||
style.content_margin_bottom = 7.0
|
||||
tile.add_theme_stylebox_override("panel", style)
|
||||
parent.add_child(tile)
|
||||
var content := VBoxContainer.new()
|
||||
content.add_theme_constant_override("separation", 1)
|
||||
tile.add_child(content)
|
||||
var caption := Label.new()
|
||||
caption.text = caption_text
|
||||
caption.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
caption.add_theme_font_size_override("font_size", 14)
|
||||
caption.add_theme_color_override("font_color", _theme.text_muted_color())
|
||||
content.add_child(caption)
|
||||
var value := Label.new()
|
||||
value.add_theme_font_size_override("font_size", 22)
|
||||
content.add_child(value)
|
||||
return [caption, value]
|
||||
|
||||
|
||||
func _style_tag_button(button: Button) -> void:
|
||||
var text_color: Color = _theme.text_color()
|
||||
var surface: Color = _editor_visual_theme.chrome_color(&"background", &"prop")
|
||||
var border: Color = _theme.border_color()
|
||||
var accent: Color = _theme.accent_color()
|
||||
button.add_theme_stylebox_override("normal", _make_tag_style(surface, border, 1))
|
||||
button.add_theme_stylebox_override("hover", _make_tag_style(surface.lightened(0.12), accent, 1))
|
||||
button.add_theme_stylebox_override("pressed", _make_tag_style(_editor_visual_theme.chrome_color(&"selected_surface", &"prop"), accent, 2))
|
||||
button.add_theme_stylebox_override("focus", _make_tag_style(Color(0.0, 0.0, 0.0, 0.0), _editor_visual_theme.chrome_color(&"focus", &"prop"), 2))
|
||||
button.add_theme_color_override("font_color", text_color)
|
||||
button.add_theme_color_override("font_hover_color", text_color)
|
||||
button.add_theme_color_override("font_pressed_color", text_color)
|
||||
button.add_theme_color_override("icon_normal_color", Color.WHITE)
|
||||
button.add_theme_color_override("icon_hover_color", Color.WHITE)
|
||||
button.add_theme_color_override("icon_pressed_color", Color.WHITE)
|
||||
|
||||
|
||||
func _make_tag_style(fill: Color, border: Color, border_width: int) -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = fill
|
||||
style.border_color = border
|
||||
style.set_border_width_all(border_width)
|
||||
style.set_corner_radius_all(8)
|
||||
style.content_margin_left = 7.0
|
||||
style.content_margin_top = 5.0
|
||||
style.content_margin_right = 7.0
|
||||
style.content_margin_bottom = 5.0
|
||||
return style
|
||||
|
||||
|
||||
func _primitive_icon_color(primitive_id: String) -> Color:
|
||||
match primitive_id:
|
||||
"box": return Color("#70C8F0")
|
||||
"plank": return Color("#F3B65D")
|
||||
"triangle": return Color("#66C681")
|
||||
"line": return Color("#DDE7F5")
|
||||
"circle": return Color("#C9A3FF")
|
||||
return _theme.accent_color()
|
||||
|
||||
|
||||
func _build_stage_authoring_controls() -> void:
|
||||
var top_bar: HBoxContainer = get_node_or_null(NodePath("ChromeRoot/TopBar")) as HBoxContainer
|
||||
if top_bar == null:
|
||||
@@ -1244,29 +1394,30 @@ func _apply_authoring_context() -> void:
|
||||
|
||||
|
||||
func _build_tag_inspector() -> void:
|
||||
_tag_section = VBoxContainer.new()
|
||||
_tag_section.add_theme_constant_override("separation", 4)
|
||||
_tag_section = _make_inspector_card()
|
||||
_inspector_list.add_child(_tag_section)
|
||||
var title := Label.new()
|
||||
title.text = "Artwork Tags"
|
||||
title.add_theme_font_size_override("font_size", _theme.font_header())
|
||||
_tag_section.add_child(title)
|
||||
var tag_content: VBoxContainer = _inspector_card_content(_tag_section)
|
||||
tag_content.add_child(_make_inspector_header("Artwork Tags", PropInspectorIconsScript.section("tags")))
|
||||
var hint := Label.new()
|
||||
hint.text = "Choose a few so this art is easy to find."
|
||||
hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
_tag_section.add_child(hint)
|
||||
hint.add_theme_color_override("font_color", _theme.text_muted_color())
|
||||
tag_content.add_child(hint)
|
||||
var rows := FlowContainer.new()
|
||||
rows.add_theme_constant_override("h_separation", 6)
|
||||
rows.add_theme_constant_override("v_separation", 4)
|
||||
_tag_section.add_child(rows)
|
||||
rows.add_theme_constant_override("v_separation", 6)
|
||||
tag_content.add_child(rows)
|
||||
for index: int in ART_TAG_IDS.size():
|
||||
var tag_id: String = ART_TAG_IDS[index]
|
||||
var check := CheckBox.new()
|
||||
check.text = ART_TAG_LABELS[index]
|
||||
check.custom_minimum_size.y = float(_theme.button_min_height())
|
||||
check.toggled.connect(_on_art_tag_toggled.bind(tag_id))
|
||||
rows.add_child(check)
|
||||
_tag_checks[tag_id] = check
|
||||
var tag_button := Button.new()
|
||||
tag_button.text = ART_TAG_LABELS[index]
|
||||
tag_button.toggle_mode = true
|
||||
tag_button.custom_minimum_size = Vector2(122.0, 50.0)
|
||||
tag_button.icon = PropInspectorIconsScript.tag(tag_id)
|
||||
tag_button.toggled.connect(_on_art_tag_toggled.bind(tag_id))
|
||||
_style_tag_button(tag_button)
|
||||
rows.add_child(tag_button)
|
||||
_tag_checks[tag_id] = tag_button
|
||||
|
||||
|
||||
func _on_art_tag_toggled(enabled: bool, tag_id: String) -> void:
|
||||
@@ -1285,9 +1436,9 @@ func _refresh_tag_controls() -> void:
|
||||
return
|
||||
_updating_inspector = true
|
||||
for tag_id: String in ART_TAG_IDS:
|
||||
var check: CheckBox = _tag_checks.get(tag_id) as CheckBox
|
||||
if check != null:
|
||||
check.set_pressed_no_signal(_tags.has(tag_id))
|
||||
var tag_button: Button = _tag_checks.get(tag_id) as Button
|
||||
if tag_button != null:
|
||||
tag_button.set_pressed_no_signal(_tags.has(tag_id))
|
||||
_updating_inspector = false
|
||||
|
||||
func _build_anchor_inspector() -> void:
|
||||
@@ -1397,13 +1548,17 @@ func _on_rename_anchor_pressed() -> void:
|
||||
_rename_anchor_source_id = _selected_anchor_id
|
||||
_rename_anchor_edit.text = _rename_anchor_source_id
|
||||
_update_rename_anchor_confirmation(_rename_anchor_edit.text)
|
||||
_rename_anchor_dialog.popup_centered(Vector2i(420, 150))
|
||||
StageDialogStylerScript.popup(_rename_anchor_dialog)
|
||||
_rename_anchor_edit.call_deferred("grab_focus")
|
||||
_rename_anchor_edit.call_deferred("select_all")
|
||||
|
||||
func _update_rename_anchor_confirmation(value: String) -> void:
|
||||
if _rename_anchor_dialog != null:
|
||||
_rename_anchor_dialog.get_ok_button().disabled = value.strip_edges().is_empty()
|
||||
var native_button: Button = _rename_anchor_dialog.get_ok_button()
|
||||
native_button.disabled = value.strip_edges().is_empty()
|
||||
var visible_button: Button = StageDialogStylerScript.visual_button(native_button)
|
||||
if visible_button != native_button:
|
||||
visible_button.disabled = native_button.disabled
|
||||
|
||||
func _commit_anchor_rename() -> void:
|
||||
if _rename_anchor_source_id.is_empty() or not _anchors.has(_rename_anchor_source_id):
|
||||
@@ -1463,10 +1618,10 @@ func _apply_control_theme(node: Node) -> void:
|
||||
|
||||
func _update_physics_labels() -> void:
|
||||
var mat: PhysicsMaterial = PropBlockScript.physics_material(_material_preset)
|
||||
_mass_label.text = "Mass: %.1f" % PropBlockScript.mass_for(_material_preset)
|
||||
_friction_label.text = "Surface grip: %.2f" % mat.friction
|
||||
_mass_value.text = "%.1f" % PropBlockScript.mass_for(_material_preset)
|
||||
_friction_value.text = "%.2f" % mat.friction
|
||||
_friction_label.tooltip_text = "Higher values make sliding harder."
|
||||
_bounce_label.text = "Bounce: %.2f" % mat.bounce
|
||||
_bounce_value.text = "%.2f" % mat.bounce
|
||||
|
||||
|
||||
func _on_preset_selected(index: int) -> void:
|
||||
@@ -1786,14 +1941,16 @@ func _check_autosave_recovery() -> void:
|
||||
|
||||
var dialog := ConfirmationDialog.new()
|
||||
dialog.title = "Restore Prop?"
|
||||
dialog.dialog_text = "An unsaved prop was recovered. Restore it?"
|
||||
dialog.ok_button_text = "🔄 Restore"
|
||||
var discard_button: Button = dialog.add_button("🗑️ Discard", false, "discard")
|
||||
dialog.theme = _ui_theme
|
||||
_editor_visual_theme.style_button_role(dialog.get_ok_button(), &"primary", &"prop")
|
||||
_editor_visual_theme.style_button_role(discard_button, &"destructive", &"prop")
|
||||
_editor_visual_theme.style_button_role(dialog.get_cancel_button(), &"secondary", &"prop")
|
||||
dialog.set_meta(StageDialogStylerScript.ICON_META, "🔄")
|
||||
dialog.dialog_text = "An unsaved prop was recovered. Restore this work?"
|
||||
dialog.ok_button_text = "Restore"
|
||||
var discard_button: Button = dialog.add_button("Discard", false, "discard")
|
||||
dialog.get_cancel_button().text = "Cancel"
|
||||
add_child(dialog)
|
||||
StageDialogStylerScript.apply(
|
||||
dialog, _dialog_theme, _dialog_theme.font(),
|
||||
dialog.get_ok_button(), [discard_button]
|
||||
)
|
||||
dialog.confirmed.connect(func() -> void:
|
||||
_apply_json_data(raw, true)
|
||||
_current_path = source_path
|
||||
@@ -1803,7 +1960,8 @@ func _check_autosave_recovery() -> void:
|
||||
if action == &"discard":
|
||||
_clear_autosave()
|
||||
dialog.queue_free())
|
||||
dialog.popup_centered()
|
||||
dialog.canceled.connect(func() -> void: dialog.queue_free())
|
||||
StageDialogStylerScript.popup(dialog)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1879,7 +2037,7 @@ func _begin_save_request(path: String, with_thumbnail: bool) -> void:
|
||||
_pending_save_path = path
|
||||
_pending_save_with_thumbnail = with_thumbnail
|
||||
if should_warn_off_origin(_canvas.has_shape_geometry(), _canvas.get_shapes_bounds()):
|
||||
_off_origin_save_dialog.popup_centered()
|
||||
StageDialogStylerScript.popup(_off_origin_save_dialog)
|
||||
return
|
||||
await _execute_pending_save(false)
|
||||
|
||||
@@ -1944,4 +2102,4 @@ func _on_load_file_selected(path: String) -> void:
|
||||
|
||||
func _show_error(message: String) -> void:
|
||||
_error_dialog.dialog_text = message
|
||||
_error_dialog.popup_centered()
|
||||
StageDialogStylerScript.popup(_error_dialog)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
class_name PropInspectorDropdown
|
||||
extends EditorSelectionDropdown
|
||||
## Prop-specific icon adapter for the shared editor selection dropdown.
|
||||
|
||||
const PropInspectorIconsScript := preload("res://scripts/editors/prop/prop_inspector_icons.gd")
|
||||
|
||||
|
||||
func add_icon_item(texture: Texture2D, label: String) -> void:
|
||||
add_styled_item(label, texture, PropInspectorIconsScript.popup_item(texture))
|
||||
@@ -0,0 +1 @@
|
||||
uid://bnnujoeli7cwk
|
||||
@@ -0,0 +1,95 @@
|
||||
class_name PropInspectorIcons
|
||||
extends RefCounted
|
||||
## Small flat 2D glyphs for the Prop Editor inspector.
|
||||
## SVG is rasterized at runtime so colors can follow the editor and material tokens.
|
||||
|
||||
const ICON_SIZE := 24
|
||||
|
||||
static func material(preset: int, tint: Color) -> Texture2D:
|
||||
if preset == 0:
|
||||
return _texture(_svg('<circle cx="24" cy="24" r="15" fill="#252B38" stroke="#C4D1E5" stroke-width="4"/>'))
|
||||
return _texture(_svg('<circle cx="24" cy="24" r="15" fill="%s" stroke="#FFFFFF" stroke-opacity="0.35" stroke-width="3"/>' % _hex(tint)))
|
||||
|
||||
|
||||
static func shape(shape_id: String, tint: Color = Color("#22C6FF")) -> Texture2D:
|
||||
var color := _hex(tint)
|
||||
var content := ""
|
||||
match shape_id:
|
||||
"box":
|
||||
content = '<rect x="8" y="8" width="32" height="32" rx="2" fill="%s"/>' % color
|
||||
"plank":
|
||||
content = '<rect x="5" y="16" width="38" height="16" rx="2" fill="%s"/>' % color
|
||||
"triangle":
|
||||
content = '<polygon points="24,6 44,41 4,41" fill="%s"/>' % color
|
||||
"line":
|
||||
content = '<path d="M8 38 L40 10" fill="none" stroke="%s" stroke-width="6" stroke-linecap="round"/>' % color
|
||||
"circle":
|
||||
content = '<circle cx="24" cy="24" r="16" fill="%s"/>' % color
|
||||
_:
|
||||
content = '<rect x="8" y="8" width="32" height="32" rx="2" fill="%s"/>' % color
|
||||
return _texture(_svg(content))
|
||||
|
||||
|
||||
static func section(section_id: String) -> Texture2D:
|
||||
var content := ""
|
||||
match section_id:
|
||||
"material":
|
||||
content = '<rect x="7" y="9" width="14" height="14" rx="3" fill="#F3B65D"/><rect x="27" y="9" width="14" height="14" rx="3" fill="#E76D66"/><rect x="7" y="27" width="14" height="14" rx="3" fill="#66C681"/><rect x="27" y="27" width="14" height="14" rx="3" fill="#70BFE8"/>'
|
||||
"selected_shape":
|
||||
content = '<path d="M9 32 L16 11 L38 15 L35 37 Z" fill="#DDE7F5" stroke="#22C6FF" stroke-width="3" stroke-linejoin="round"/><circle cx="9" cy="32" r="3" fill="#FFFFFF"/><circle cx="16" cy="11" r="3" fill="#FFFFFF"/><circle cx="38" cy="15" r="3" fill="#FFFFFF"/><circle cx="35" cy="37" r="3" fill="#FFFFFF"/>'
|
||||
"add_shape":
|
||||
content = '<rect x="7" y="7" width="34" height="34" fill="none" stroke="#22C6FF" stroke-width="5"/><path d="M24 15 V33 M15 24 H33" stroke="#22C6FF" stroke-width="4" stroke-linecap="round"/>'
|
||||
"tags":
|
||||
content = '<path d="M7 17 L17 7 H34 L42 15 V32 L25 42 L7 24 Z" fill="#FFB84D"/><circle cx="31" cy="16" r="3.5" fill="#252B38"/>'
|
||||
return _texture(_svg(content))
|
||||
|
||||
|
||||
static func tag(tag_id: String) -> Texture2D:
|
||||
var color := "#66C681"
|
||||
var body := ""
|
||||
match tag_id:
|
||||
"nature":
|
||||
color = "#66C681"
|
||||
body = '<path d="M9 35 C9 18 21 8 39 8 C39 26 29 39 13 39 Z" fill="%s"/><path d="M12 38 L33 15" stroke="#E8FFF0" stroke-width="3" stroke-linecap="round"/>' % color
|
||||
"buildings":
|
||||
color = "#F3A653"
|
||||
body = '<path d="M10 40 V10 H29 V18 H38 V40 Z" fill="%s"/><path d="M16 16 H21 M16 23 H21 M16 30 H21 M31 24 H35 M31 31 H35" stroke="#3C2A22" stroke-width="3"/>' % color
|
||||
"walls":
|
||||
color = "#F36B5B"
|
||||
body = '<path d="M5 10 H21 V19 H5 Z M24 10 H42 V19 H24 Z M14 22 H31 V31 H14 Z M4 34 H20 V42 H4 Z M23 34 H42 V42 H23 Z" fill="%s"/><path d="M22 10 V19 M13 22 V31 M31 34 V42" stroke="#733A37" stroke-width="2"/>' % color
|
||||
"roads":
|
||||
color = "#9BAEC9"
|
||||
body = '<path d="M16 7 H32 L42 41 H6 Z" fill="%s"/><path d="M24 12 V19 M24 25 V31 M24 36 V40" stroke="#283247" stroke-width="3" stroke-linecap="round"/>' % color
|
||||
"objects":
|
||||
color = "#E8A96D"
|
||||
body = '<rect x="7" y="11" width="34" height="26" fill="none" stroke="%s" stroke-width="5"/>' % color
|
||||
return _texture(_svg(body))
|
||||
|
||||
|
||||
static func popup_item(texture: Texture2D, size: int = 44) -> Texture2D:
|
||||
if texture == null:
|
||||
return null
|
||||
var source: Image = texture.get_image()
|
||||
var canvas := Image.create(size, size, false, Image.FORMAT_RGBA8)
|
||||
var draw_size: Vector2i = Vector2i(mini(source.get_width(), size), mini(source.get_height(), size))
|
||||
var source_rect := Rect2i(Vector2i.ZERO, draw_size)
|
||||
var destination := Vector2i((size - draw_size.x) / 2, (size - draw_size.y) / 2)
|
||||
canvas.blit_rect(source, source_rect, destination)
|
||||
return ImageTexture.create_from_image(canvas)
|
||||
|
||||
|
||||
static func _svg(content: String) -> String:
|
||||
return '<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" viewBox="0 0 48 48">%s</svg>' % [ICON_SIZE, ICON_SIZE, content]
|
||||
|
||||
|
||||
static func _texture(svg: String) -> Texture2D:
|
||||
var image := Image.new()
|
||||
var error: Error = image.load_svg_from_string(svg)
|
||||
if error != OK:
|
||||
push_warning("Prop Inspector icon could not be rasterized (%s)." % error_string(error))
|
||||
return null
|
||||
return ImageTexture.create_from_image(image)
|
||||
|
||||
|
||||
static func _hex(color: Color) -> String:
|
||||
return "#" + color.to_html(false)
|
||||
@@ -0,0 +1 @@
|
||||
uid://5ac0640i4bj7
|
||||
@@ -426,17 +426,19 @@ func _build_file_menu(parent: HBoxContainer) -> void:
|
||||
var file_popup: PopupMenu = _file_menu.get_popup()
|
||||
var popup_font_size: int = _theme_integer("fonts.sizes.file_menu_popup", 26, 12, 64)
|
||||
file_popup.add_theme_font_size_override("font_size", popup_font_size)
|
||||
file_popup.add_item("New", 0)
|
||||
file_popup.add_item("Open…", 1)
|
||||
file_popup.add_item("🆕 New", 0)
|
||||
file_popup.add_item("📂 Open…", 1)
|
||||
var recent_popup := PopupMenu.new()
|
||||
recent_popup.name = "OpenRecent"
|
||||
recent_popup.add_theme_font_size_override("font_size", popup_font_size)
|
||||
recent_popup.id_pressed.connect(_on_recent_scene_requested)
|
||||
file_popup.add_child(recent_popup)
|
||||
file_popup.add_submenu_item("Open Recent", "OpenRecent")
|
||||
file_popup.add_item("Save", 2)
|
||||
file_popup.add_item("Save As…", 3)
|
||||
file_popup.add_item("Rename Scene…", 4)
|
||||
file_popup.add_submenu_item("🕑 Open Recent", "OpenRecent")
|
||||
file_popup.add_separator()
|
||||
file_popup.add_item("💾 Save", 2)
|
||||
file_popup.add_item("📄 Save As…", 3)
|
||||
file_popup.add_separator()
|
||||
file_popup.add_item("✏️ Rename Scene…", 4)
|
||||
file_popup.id_pressed.connect(_on_file_menu_requested)
|
||||
file_popup.about_to_popup.connect(_on_recent_menu_requested)
|
||||
parent.add_child(_file_menu)
|
||||
|
||||
@@ -64,7 +64,7 @@ const ZOOM_STEP: float = 1.10
|
||||
|
||||
@onready var title_label: Label = %TitleLabel
|
||||
@onready var drawing_area: Control = %DrawingArea
|
||||
@onready var context_menu: PopupMenu = %ContextMenu
|
||||
@onready var context_menu: EditorContextMenu = %ContextMenu
|
||||
@onready var color_picker_popup: PopupPanel = %ColorPickerPopup
|
||||
@onready var color_picker: ColorPicker = %ColorPicker
|
||||
@onready var color_picker_button_row: HBoxContainer = %ButtonRow
|
||||
@@ -127,6 +127,8 @@ func _ready() -> void:
|
||||
cancel_button.pressed.connect(_on_color_picker_cancel)
|
||||
if not context_menu.popup_hide.is_connected(_on_presentation_popup_hidden):
|
||||
context_menu.popup_hide.connect(_on_presentation_popup_hidden)
|
||||
if _editor_visual_theme != null:
|
||||
context_menu.configure(_editor_visual_theme, &"stickman")
|
||||
if not color_picker_popup.popup_hide.is_connected(_on_presentation_popup_hidden):
|
||||
color_picker_popup.popup_hide.connect(_on_presentation_popup_hidden)
|
||||
cancel_button.text = "Cancel"
|
||||
@@ -298,6 +300,8 @@ func set_canvas_tokens(grid_color: Color, selection_core: Color, selection_halo:
|
||||
_hint_color = hint_color
|
||||
_button_target_height = button_target_height
|
||||
_editor_visual_theme = editor_visual_theme
|
||||
if context_menu != null and _editor_visual_theme != null:
|
||||
context_menu.configure(_editor_visual_theme, &"stickman")
|
||||
if drawing_area != null:
|
||||
drawing_area.queue_redraw()
|
||||
|
||||
@@ -421,6 +425,24 @@ func _screen_to_world(pos: Vector2) -> Vector2:
|
||||
return (pos - _pan_offset) / _zoom
|
||||
|
||||
|
||||
func _global_point_target_rect(world_pos: Vector2) -> Rect2:
|
||||
var screen_pos := drawing_area.global_position + _pan_offset + world_pos * _zoom
|
||||
return Rect2(screen_pos - Vector2(6.0, 6.0), Vector2(12.0, 12.0))
|
||||
|
||||
|
||||
func _global_shape_target_rect(sd: Dictionary) -> Rect2:
|
||||
var points: PackedVector2Array = sd.get("points", PackedVector2Array())
|
||||
if points.is_empty():
|
||||
return Rect2()
|
||||
var min_point := points[0]
|
||||
var max_point := points[0]
|
||||
for point: Vector2 in points:
|
||||
min_point = min_point.min(point)
|
||||
max_point = max_point.max(point)
|
||||
var screen_position := drawing_area.global_position + _pan_offset + min_point * _zoom
|
||||
return Rect2(screen_position, (max_point - min_point) * _zoom).grow(4.0)
|
||||
|
||||
|
||||
func _snap_to_grid(pos: Vector2) -> Vector2:
|
||||
var gs := float(_grid_size)
|
||||
if gs <= 0.0:
|
||||
@@ -638,9 +660,9 @@ func _handle_right_click(mb: InputEventMouseButton, world_pos: Vector2) -> void:
|
||||
if hit_idx >= 0:
|
||||
_context_shape_idx = _selected_shape_idx
|
||||
context_menu.clear()
|
||||
context_menu.add_item("Remove Point", 6)
|
||||
context_menu.add_destructive_item("Remove Point", 6)
|
||||
context_menu.set_meta("vertex_index", hit_idx)
|
||||
context_menu.popup_on_parent(Rect2(mb.global_position, Vector2.ONE))
|
||||
context_menu.popup_at(mb.global_position, _global_point_target_rect(world_pos))
|
||||
return
|
||||
|
||||
for i: int in range(shapes.size() - 1, -1, -1):
|
||||
@@ -651,9 +673,9 @@ func _handle_right_click(mb: InputEventMouseButton, world_pos: Vector2) -> void:
|
||||
if hit_idx >= 0:
|
||||
_context_shape_idx = i
|
||||
context_menu.clear()
|
||||
context_menu.add_item("Remove Point", 6)
|
||||
context_menu.add_destructive_item("Remove Point", 6)
|
||||
context_menu.set_meta("vertex_index", hit_idx)
|
||||
context_menu.popup_on_parent(Rect2(mb.global_position, Vector2.ONE))
|
||||
context_menu.popup_at(mb.global_position, _global_point_target_rect(world_pos))
|
||||
return
|
||||
|
||||
# Step 2: Check if right-click is over any shape (reverse order = front first).
|
||||
@@ -677,22 +699,22 @@ func _handle_right_click(mb: InputEventMouseButton, world_pos: Vector2) -> void:
|
||||
context_menu.add_item("Create Point", 3)
|
||||
context_menu.add_separator()
|
||||
|
||||
context_menu.add_item("Color...", 4)
|
||||
context_menu.add_item("Change Color…", 4)
|
||||
context_menu.add_separator()
|
||||
context_menu.add_item("Send Back", 7)
|
||||
context_menu.add_item("Bring Forward", 8)
|
||||
context_menu.add_separator()
|
||||
context_menu.add_item("Copy", 9)
|
||||
context_menu.add_item("Copy Shape", 9)
|
||||
if _has_clipboard:
|
||||
context_menu.add_item("Paste", 10)
|
||||
context_menu.add_item("Paste Shape", 10)
|
||||
context_menu.add_separator()
|
||||
context_menu.add_item("Mirror X", 11)
|
||||
context_menu.add_item("Mirror Y", 12)
|
||||
context_menu.add_item("Flip Left-Right", 11)
|
||||
context_menu.add_item("Flip Up-Down", 12)
|
||||
context_menu.add_separator()
|
||||
context_menu.add_item("Delete", 5)
|
||||
context_menu.add_item("Bring Forward", 8)
|
||||
context_menu.add_item("Send Back", 7)
|
||||
context_menu.add_separator()
|
||||
context_menu.add_destructive_item("Erase Shape", 5)
|
||||
|
||||
context_menu.set_meta("click_position", world_pos)
|
||||
context_menu.popup_on_parent(Rect2(mb.global_position, Vector2.ONE))
|
||||
context_menu.popup_at(mb.global_position, _global_shape_target_rect(found_sd))
|
||||
return
|
||||
|
||||
# Step 3: No shape under mouse -> shape creation menu.
|
||||
@@ -702,9 +724,9 @@ func _handle_right_click(mb: InputEventMouseButton, world_pos: Vector2) -> void:
|
||||
context_menu.add_item("Circle", 2)
|
||||
if _has_clipboard:
|
||||
context_menu.add_separator()
|
||||
context_menu.add_item("Paste", 10)
|
||||
context_menu.add_item("Paste Shape", 10)
|
||||
context_menu.set_meta("click_position", world_pos)
|
||||
context_menu.popup_on_parent(Rect2(mb.global_position, Vector2.ONE))
|
||||
context_menu.popup_at(mb.global_position)
|
||||
|
||||
|
||||
func _handle_left_press(world_pos: Vector2) -> void:
|
||||
|
||||
@@ -17,6 +17,8 @@ extends Control
|
||||
|
||||
const ContentPathsScript := preload("res://scripts/shared/content_paths.gd")
|
||||
const EditorVisualThemeScript := preload("res://scripts/shared/editor_ui/editor_visual_theme.gd")
|
||||
const SandboxThemeScript := preload("res://scripts/stage/sandbox_theme.gd")
|
||||
const StageDialogStylerScript := preload("res://scripts/shared/stage_ui/stage_dialog_styler.gd")
|
||||
|
||||
enum WorkspaceView { OVERVIEW, FOCUS }
|
||||
enum PendingDocumentAction { NONE, LOAD, CLEAR, QUIT }
|
||||
@@ -110,11 +112,10 @@ const WORKSPACE_GAP: float = 8.0
|
||||
@onready var _overview_scroll: ScrollContainer = %OverviewScroll
|
||||
@onready var _focus_host: VBoxContainer = %FocusHost
|
||||
@onready var _focus_panel_host: Control = %FocusPanelHost
|
||||
@onready var _focus_part_label: Label = %FocusPartLabel
|
||||
@onready var _workspace_row: HSplitContainer = %WorkspaceRow
|
||||
@onready var _overview_button: Button = %OverviewButton
|
||||
@onready var _focus_button: Button = %FocusButton
|
||||
@onready var _part_picker: OptionButton = %PartPicker
|
||||
@onready var _part_picker: EditorSelectionDropdown = %PartPicker
|
||||
@onready var _add_shape_button: Button = %AddShapeButton
|
||||
@onready var _color_button: Button = %ColorButton
|
||||
@onready var _whole_stickman_button: Button = %WholeStickmanButton
|
||||
@@ -132,6 +133,7 @@ const WORKSPACE_GAP: float = 8.0
|
||||
@onready var _unsaved_dialog: ConfirmationDialog = %UnsavedChangesDialog
|
||||
|
||||
var _editor_visual_theme: EditorVisualTheme
|
||||
var _dialog_theme: SandboxTheme
|
||||
var _body_part_origins: Dictionary = {}
|
||||
var _workspace_view: WorkspaceView = WorkspaceView.OVERVIEW
|
||||
var _view_choice_was_manual: bool = false
|
||||
@@ -173,6 +175,8 @@ var _view_menu: PopupMenu
|
||||
func _ready() -> void:
|
||||
_editor_visual_theme = EditorVisualThemeScript.new()
|
||||
_editor_visual_theme.load_from(EditorVisualTheme.DEFAULT_PATH)
|
||||
_dialog_theme = SandboxThemeScript.new()
|
||||
_dialog_theme.load_from(SandboxThemeScript.DEFAULT_PATH)
|
||||
theme = _editor_visual_theme.control_theme(&"stickman")
|
||||
_setup_menu_bar()
|
||||
_setup_dialogs()
|
||||
@@ -188,6 +192,7 @@ func _ready() -> void:
|
||||
_editor_visual_theme.stickman_canvas_color(&"selection_halo"),
|
||||
_editor_visual_theme.chrome_color(&"preview", &"stickman")
|
||||
)
|
||||
_whole_preview.set_context_menu_visual_theme(_editor_visual_theme, &"stickman")
|
||||
_stickman_name_edit.text_changed.connect(_on_stickman_name_changed)
|
||||
|
||||
_load_settings()
|
||||
@@ -233,17 +238,18 @@ func _process(_delta: float) -> void:
|
||||
func _setup_menu_bar() -> void:
|
||||
var file_menu: PopupMenu = PopupMenu.new()
|
||||
file_menu.name = "FileMenu"
|
||||
file_menu.add_item("Save", 0)
|
||||
file_menu.add_item("Load", 1)
|
||||
file_menu.add_item("📂 Load…", 1)
|
||||
file_menu.add_separator()
|
||||
file_menu.add_item("Start Over", 2)
|
||||
file_menu.add_item("💾 Save", 0)
|
||||
file_menu.add_separator()
|
||||
file_menu.add_item("🆕 Start Over", 2)
|
||||
file_menu.id_pressed.connect(_on_file_menu_id_pressed)
|
||||
_menu_bar.add_child(file_menu)
|
||||
_menu_bar.set_menu_title(_menu_bar.get_menu_count() - 1, "File")
|
||||
|
||||
_edit_menu = PopupMenu.new()
|
||||
_edit_menu.name = "EditMenu"
|
||||
_edit_menu.add_item("Configure Grid...", 0)
|
||||
_edit_menu.add_item("⚙️ Configure Grid…", 0)
|
||||
_edit_menu.add_separator()
|
||||
_edit_menu.add_item(_snap_menu_label(), 1)
|
||||
_edit_menu.id_pressed.connect(_on_edit_menu_id_pressed)
|
||||
@@ -253,7 +259,7 @@ func _setup_menu_bar() -> void:
|
||||
|
||||
var view_menu: PopupMenu = PopupMenu.new()
|
||||
view_menu.name = "ViewMenu"
|
||||
view_menu.add_item("Reset Views", 0)
|
||||
view_menu.add_item("🔍 Reset Views", 0)
|
||||
view_menu.add_separator()
|
||||
view_menu.add_item(_guide_menu_label(), 1)
|
||||
view_menu.id_pressed.connect(_on_view_menu_id_pressed)
|
||||
@@ -266,10 +272,6 @@ func _setup_menu_bar() -> void:
|
||||
func _setup_dialogs() -> void:
|
||||
_save_dialog.theme = theme
|
||||
_load_dialog.theme = theme
|
||||
_clear_confirm.theme = theme
|
||||
_unsaved_dialog.theme = theme
|
||||
_error_dialog.theme = theme
|
||||
_grid_config_dialog.theme = theme
|
||||
var content_dir: String = ProjectSettings.globalize_path(ContentPathsScript.STICKMEN_DIR)
|
||||
_save_dialog.current_dir = content_dir
|
||||
_load_dialog.current_dir = content_dir
|
||||
@@ -280,17 +282,34 @@ func _setup_dialogs() -> void:
|
||||
_editor_visual_theme.style_button_role(_load_dialog.get_ok_button(), &"primary", &"stickman")
|
||||
_editor_visual_theme.style_button_role(_load_dialog.get_cancel_button(), &"secondary", &"stickman")
|
||||
_clear_confirm.confirmed.connect(_on_clear_confirmed)
|
||||
_clear_confirm.title = "Start Over?"
|
||||
_clear_confirm.dialog_text = "This will clear your current stickman. Start over?"
|
||||
_clear_confirm.ok_button_text = "Start Over"
|
||||
_clear_confirm.set_meta(StageDialogStylerScript.ICON_META, "🧹")
|
||||
_clear_confirm.get_cancel_button().text = "Cancel"
|
||||
_editor_visual_theme.style_button_role(_clear_confirm.get_ok_button(), &"destructive", &"stickman")
|
||||
_editor_visual_theme.style_button_role(_clear_confirm.get_cancel_button(), &"secondary", &"stickman")
|
||||
var discard_button: Button = _unsaved_dialog.add_button("Discard Changes", false, "discard")
|
||||
StageDialogStylerScript.apply(
|
||||
_clear_confirm, _dialog_theme, _dialog_theme.font(),
|
||||
null, [_clear_confirm.get_ok_button()], [], false
|
||||
)
|
||||
_unsaved_dialog.title = "Unsaved Changes"
|
||||
_unsaved_dialog.set_meta(StageDialogStylerScript.ICON_META, "💾")
|
||||
var discard_button: Button = _unsaved_dialog.add_button("Discard", false, "discard")
|
||||
_unsaved_dialog.get_cancel_button().text = "Cancel"
|
||||
_editor_visual_theme.style_button_role(_unsaved_dialog.get_ok_button(), &"primary", &"stickman")
|
||||
_editor_visual_theme.style_button_role(discard_button, &"destructive", &"stickman")
|
||||
_editor_visual_theme.style_button_role(_unsaved_dialog.get_cancel_button(), &"secondary", &"stickman")
|
||||
_editor_visual_theme.style_button_role(_error_dialog.get_ok_button(), &"secondary", &"stickman")
|
||||
_editor_visual_theme.style_button_role(_grid_config_dialog.get_ok_button(), &"primary", &"stickman")
|
||||
_editor_visual_theme.style_button_role(_grid_config_dialog.get_cancel_button(), &"secondary", &"stickman")
|
||||
_unsaved_dialog.ok_button_text = "Save"
|
||||
StageDialogStylerScript.apply(
|
||||
_unsaved_dialog, _dialog_theme, _dialog_theme.font(),
|
||||
_unsaved_dialog.get_ok_button(), [discard_button]
|
||||
)
|
||||
_error_dialog.get_ok_button().text = "Got it"
|
||||
StageDialogStylerScript.apply(
|
||||
_error_dialog, _dialog_theme, _dialog_theme.font(),
|
||||
_error_dialog.get_ok_button()
|
||||
)
|
||||
_grid_config_dialog.set_meta(StageDialogStylerScript.ICON_META, "📏")
|
||||
StageDialogStylerScript.apply(
|
||||
_grid_config_dialog, _dialog_theme, _dialog_theme.font(),
|
||||
_grid_config_dialog.get_ok_button()
|
||||
)
|
||||
_unsaved_dialog.confirmed.connect(_on_unsaved_save_pressed)
|
||||
_unsaved_dialog.custom_action.connect(_on_unsaved_custom_action)
|
||||
_unsaved_dialog.canceled.connect(_cancel_pending_document_action)
|
||||
@@ -303,14 +322,24 @@ func _setup_workspace_controls() -> void:
|
||||
_focus_button.button_group = _view_button_group
|
||||
_overview_button.custom_minimum_size.y = float(_editor_visual_theme.chrome_px(&"button_min_height", &"stickman"))
|
||||
_focus_button.custom_minimum_size.y = float(_editor_visual_theme.chrome_px(&"mode_button_min_height", &"stickman"))
|
||||
_part_picker.custom_minimum_size = Vector2(160.0, float(_editor_visual_theme.chrome_px(&"button_min_height", &"stickman")))
|
||||
_part_picker.custom_minimum_size = Vector2(220.0, float(_editor_visual_theme.chrome_px(&"button_min_height", &"stickman")))
|
||||
_part_picker.configure(
|
||||
theme,
|
||||
_editor_visual_theme.chrome_color(&"surface", &"stickman"),
|
||||
_editor_visual_theme.chrome_color(&"selected_surface", &"stickman"),
|
||||
_editor_visual_theme.chrome_color(&"border", &"stickman"),
|
||||
_editor_visual_theme.chrome_color(&"edit", &"stickman"),
|
||||
_editor_visual_theme.chrome_color(&"focus", &"stickman"),
|
||||
_editor_visual_theme.chrome_color(&"text", &"stickman"),
|
||||
_editor_visual_theme.chrome_px(&"menu_popup", &"stickman"),
|
||||
12
|
||||
)
|
||||
_add_shape_button.custom_minimum_size.y = float(_editor_visual_theme.chrome_px(&"button_min_height", &"stickman"))
|
||||
_editor_visual_theme.style_button_role(_add_shape_button, &"primary", &"stickman")
|
||||
_color_button.custom_minimum_size.y = float(_editor_visual_theme.chrome_px(&"button_min_height", &"stickman"))
|
||||
_editor_visual_theme.style_button_role(_color_button, &"secondary", &"stickman")
|
||||
_whole_stickman_button.custom_minimum_size.y = float(_editor_visual_theme.chrome_px(&"button_min_height", &"stickman"))
|
||||
_back_to_part_button.custom_minimum_size.y = float(_editor_visual_theme.chrome_px(&"button_min_height", &"stickman"))
|
||||
_focus_part_label.add_theme_font_size_override("font_size", _editor_visual_theme.chrome_px(&"section", &"stickman"))
|
||||
_status_cursor_coords.add_theme_font_size_override("font_size", _editor_visual_theme.chrome_px(&"status", &"stickman"))
|
||||
_status_save_status.add_theme_font_size_override("font_size", _editor_visual_theme.chrome_px(&"status", &"stickman"))
|
||||
_status_snap_status.add_theme_font_size_override("font_size", _editor_visual_theme.chrome_px(&"status", &"stickman"))
|
||||
@@ -324,9 +353,9 @@ func _setup_workspace_controls() -> void:
|
||||
_whole_stickman_button.pressed.connect(_on_whole_stickman_button_pressed)
|
||||
_back_to_part_button.pressed.connect(_on_back_to_part_button_pressed)
|
||||
_add_shape_menu = PopupMenu.new()
|
||||
_add_shape_menu.add_item("Line", 0)
|
||||
_add_shape_menu.add_item("Rectangle", 1)
|
||||
_add_shape_menu.add_item("Circle", 2)
|
||||
_add_shape_menu.add_item("〰️ Line", 0)
|
||||
_add_shape_menu.add_item("▭ Rectangle", 1)
|
||||
_add_shape_menu.add_item("⚪ Circle", 2)
|
||||
_add_shape_menu.id_pressed.connect(_on_add_shape_menu_id_pressed)
|
||||
_add_shape_menu.theme = theme
|
||||
add_child(_add_shape_menu)
|
||||
@@ -526,8 +555,6 @@ func _update_active_part_presentation() -> void:
|
||||
var panel: BodyPartPanel = _body_part_panels.get(part_name) as BodyPartPanel
|
||||
if panel != null:
|
||||
panel.set_active_presentation(part_name == _active_part_name, surface, border, accent, section_size)
|
||||
if is_instance_valid(_focus_part_label):
|
||||
_focus_part_label.text = BODY_PART_DISPLAY.get(_active_part_name, _active_part_name)
|
||||
|
||||
|
||||
func _refresh_workspace_controls() -> void:
|
||||
@@ -686,7 +713,7 @@ func _request_document_action(action: PendingDocumentAction) -> void:
|
||||
_unsaved_dialog.dialog_text = "Save changes before starting over with a blank stickman?"
|
||||
PendingDocumentAction.QUIT:
|
||||
_unsaved_dialog.dialog_text = "Save changes before closing Stickman Studio?"
|
||||
_unsaved_dialog.popup_centered()
|
||||
StageDialogStylerScript.popup(_unsaved_dialog)
|
||||
return
|
||||
_continue_document_action(action)
|
||||
|
||||
@@ -696,7 +723,7 @@ func _continue_document_action(action: PendingDocumentAction) -> void:
|
||||
PendingDocumentAction.LOAD:
|
||||
_load_dialog.popup_centered_ratio(0.6)
|
||||
PendingDocumentAction.CLEAR:
|
||||
_clear_confirm.popup_centered()
|
||||
StageDialogStylerScript.popup(_clear_confirm)
|
||||
PendingDocumentAction.QUIT:
|
||||
get_tree().quit()
|
||||
_pending_document_action = PendingDocumentAction.NONE
|
||||
@@ -763,7 +790,7 @@ func _on_edit_menu_id_pressed(id: int) -> void:
|
||||
match id:
|
||||
0: # Configure Grid...
|
||||
_grid_size_spin_box.value = float(_grid_size)
|
||||
_grid_config_dialog.popup_centered()
|
||||
StageDialogStylerScript.popup(_grid_config_dialog)
|
||||
1: # Snap to Grid (checkable)
|
||||
_snap_enabled = not _snap_enabled
|
||||
if _edit_menu:
|
||||
@@ -808,7 +835,7 @@ func _update_guide_menu_item() -> void:
|
||||
|
||||
|
||||
func _guide_menu_label() -> String:
|
||||
return "Hide Pose Guide" if _show_guide else "Show Pose Guide"
|
||||
return "👁️ Hide Pose Guide" if _show_guide else "👁️ Show Pose Guide"
|
||||
|
||||
|
||||
func _on_grid_config_confirmed() -> void:
|
||||
@@ -862,7 +889,7 @@ func _on_clear_confirmed() -> void:
|
||||
|
||||
func _show_error(message: String) -> void:
|
||||
_error_dialog.dialog_text = message
|
||||
_error_dialog.popup_centered()
|
||||
StageDialogStylerScript.popup(_error_dialog)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Signal handlers - body-part panels
|
||||
@@ -1198,7 +1225,7 @@ func _broadcast_settings() -> void:
|
||||
|
||||
|
||||
func _snap_menu_label() -> String:
|
||||
return "[√] Snap to Grid" if _snap_enabled else "Snap to Grid"
|
||||
return "🧲 [√] Snap to Grid" if _snap_enabled else "🧲 Snap to Grid"
|
||||
|
||||
|
||||
func _update_snap_status_label() -> void:
|
||||
|
||||
@@ -26,6 +26,7 @@ signal part_order_changed(part_name: String, new_order: Array[String])
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
const GRID_COLOR := Color(1.0, 1.0, 1.0, 0.15)
|
||||
const EditorContextMenuScript := preload("res://scripts/shared/editor_ui/editor_context_menu.gd")
|
||||
const MIN_ZOOM: float = 0.3
|
||||
const MAX_ZOOM: float = 3.0
|
||||
const ZOOM_STEP: float = 1.10
|
||||
@@ -142,7 +143,7 @@ var _pan_start: Vector2 = Vector2.ZERO
|
||||
|
||||
# Phase 5: z-ordering
|
||||
var _part_order: Array[String] = []
|
||||
var _context_menu: PopupMenu
|
||||
var _context_menu: EditorContextMenu
|
||||
|
||||
# Phase 6: selection gizmos rendered on top
|
||||
var _selected_gizmo_bounds: Rect2 = Rect2()
|
||||
@@ -289,6 +290,11 @@ func set_canvas_tokens(grid_color: Color, selection_core: Color, selection_halo:
|
||||
preview_area.queue_redraw()
|
||||
|
||||
|
||||
func set_context_menu_visual_theme(visual_theme: EditorVisualTheme, editor: StringName) -> void:
|
||||
if _context_menu != null:
|
||||
_context_menu.configure(visual_theme, editor)
|
||||
|
||||
|
||||
## Returns the preview-space position of a guide joint (a master-space
|
||||
## GUIDE_JOINTS entry mapped through _guide_to_preview). Guarded against
|
||||
## unknown joint names.
|
||||
@@ -334,7 +340,7 @@ func _ready() -> void:
|
||||
if not preview_area.gui_input.is_connected(_on_preview_gui_input):
|
||||
preview_area.gui_input.connect(_on_preview_gui_input)
|
||||
|
||||
_context_menu = PopupMenu.new()
|
||||
_context_menu = EditorContextMenuScript.new() as EditorContextMenu
|
||||
_context_menu.name = "PreviewContextMenu"
|
||||
add_child(_context_menu)
|
||||
_context_menu.id_pressed.connect(_on_context_menu_id_pressed)
|
||||
@@ -1024,12 +1030,14 @@ func _handle_right_click(mb: InputEventMouseButton) -> void:
|
||||
if rect.has_point(world_pos):
|
||||
_selected_part = part_name
|
||||
_context_menu.clear()
|
||||
_context_menu.add_item("Send Back", 0)
|
||||
_context_menu.add_item("Bring Forward", 1)
|
||||
_context_menu.add_item("Send Back", 0)
|
||||
_context_menu.add_separator()
|
||||
_context_menu.add_item("Mirror X", 2)
|
||||
_context_menu.add_item("Mirror Y", 3)
|
||||
_context_menu.popup_on_parent(Rect2(mb.global_position, Vector2.ONE))
|
||||
_context_menu.add_item("Flip Left-Right", 2)
|
||||
_context_menu.add_item("Flip Up-Down", 3)
|
||||
var screen_position := preview_area.global_position + _pan_offset + rect.position * _zoom
|
||||
var target_rect := Rect2(screen_position, rect.size * _zoom).grow(4.0)
|
||||
_context_menu.popup_at(mb.global_position, target_rect)
|
||||
preview_area.queue_redraw()
|
||||
return
|
||||
|
||||
|
||||
@@ -0,0 +1,385 @@
|
||||
class_name EditorContextMenu
|
||||
extends PopupPanel
|
||||
## Shared Studio List popup used by right-click menus in both standalone editors.
|
||||
|
||||
signal id_pressed(id: int)
|
||||
|
||||
enum ItemRole { NORMAL, DESTRUCTIVE }
|
||||
|
||||
const DEFAULT_EDITOR := &"prop"
|
||||
const DEFAULT_MIN_WIDTH := 208
|
||||
const DEFAULT_ROW_HEIGHT := 48
|
||||
const DEFAULT_GROUP_GAP := 8
|
||||
const SHELL_INSET := 8
|
||||
const ROW_RADIUS := 6
|
||||
const TARGET_GAP := 12.0
|
||||
|
||||
var _visual_theme: EditorVisualTheme
|
||||
var _editor: StringName = DEFAULT_EDITOR
|
||||
var _font_size_override: int = -1
|
||||
var _shell: MarginContainer
|
||||
var _scroll: ScrollContainer
|
||||
var _items: VBoxContainer
|
||||
var _row_buttons: Array[Button] = []
|
||||
var _row_ids: Dictionary = {}
|
||||
var _keyboard_navigation_active: bool = false
|
||||
var _focused_row_index: int = -1
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_build_contents()
|
||||
if not about_to_popup.is_connected(_reset_keyboard_navigation):
|
||||
about_to_popup.connect(_reset_keyboard_navigation)
|
||||
if not popup_hide.is_connected(_reset_keyboard_navigation):
|
||||
popup_hide.connect(_reset_keyboard_navigation)
|
||||
if _visual_theme != null:
|
||||
_apply_visual_style()
|
||||
|
||||
|
||||
func configure(visual_theme: EditorVisualTheme, editor: StringName) -> void:
|
||||
_visual_theme = visual_theme
|
||||
_editor = editor
|
||||
if is_node_ready():
|
||||
_apply_visual_style()
|
||||
|
||||
|
||||
func set_font_size(size: int) -> void:
|
||||
_font_size_override = maxi(1, size)
|
||||
if is_node_ready():
|
||||
_apply_visual_style()
|
||||
|
||||
|
||||
func clear() -> void:
|
||||
_reset_keyboard_navigation()
|
||||
_row_buttons.clear()
|
||||
_row_ids.clear()
|
||||
if _items == null:
|
||||
return
|
||||
for child: Node in _items.get_children():
|
||||
_items.remove_child(child)
|
||||
child.free()
|
||||
|
||||
|
||||
func add_item(label: String, id: int, role: ItemRole = ItemRole.NORMAL) -> void:
|
||||
if _items == null:
|
||||
_build_contents()
|
||||
var button := Button.new()
|
||||
button.text = label
|
||||
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
button.custom_minimum_size = Vector2(_menu_min_width(), _menu_row_height())
|
||||
button.focus_mode = Control.FOCUS_NONE
|
||||
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||
button.add_theme_font_size_override("font_size", _font_size())
|
||||
button.add_theme_color_override("font_color", _text_color(role))
|
||||
button.add_theme_color_override("font_hover_color", _text_color(role))
|
||||
button.add_theme_color_override("font_pressed_color", _text_color(role))
|
||||
button.add_theme_color_override("font_focus_color", _text_color(role))
|
||||
button.add_theme_stylebox_override("normal", _make_row_box(Color(0.0, 0.0, 0.0, 0.0), Color(0, 0, 0, 0), 0))
|
||||
button.add_theme_stylebox_override("hover", _make_hover_box())
|
||||
button.add_theme_stylebox_override("pressed", _make_hover_box(true))
|
||||
button.add_theme_stylebox_override("focus", _make_focus_box())
|
||||
button.add_theme_stylebox_override("disabled", _make_row_box(Color(0.0, 0.0, 0.0, 0.0), Color(0, 0, 0, 0), 0))
|
||||
button.set_meta("context_menu_id", id)
|
||||
button.set_meta("context_menu_role", role)
|
||||
button.pressed.connect(_on_row_pressed.bind(button))
|
||||
button.mouse_entered.connect(_on_row_mouse_entered.bind(button))
|
||||
_items.add_child(button)
|
||||
_row_buttons.append(button)
|
||||
_row_ids[id] = button
|
||||
|
||||
|
||||
func add_destructive_item(label: String, id: int) -> void:
|
||||
add_item(label, id, ItemRole.DESTRUCTIVE)
|
||||
|
||||
|
||||
func add_separator() -> void:
|
||||
if _items == null:
|
||||
_build_contents()
|
||||
var spacer := MarginContainer.new()
|
||||
spacer.custom_minimum_size.y = float(DEFAULT_GROUP_GAP + 1)
|
||||
spacer.add_theme_constant_override("margin_top", int(DEFAULT_GROUP_GAP / 2))
|
||||
spacer.add_theme_constant_override("margin_bottom", int(DEFAULT_GROUP_GAP / 2))
|
||||
var line := ColorRect.new()
|
||||
line.color = _theme_color(&"border", Color(1, 1, 1, 0.2))
|
||||
line.custom_minimum_size.y = 1.0
|
||||
line.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
spacer.add_child(line)
|
||||
_items.add_child(spacer)
|
||||
|
||||
|
||||
func popup_at(global_position: Vector2, avoid_rect: Rect2 = Rect2()) -> void:
|
||||
if _items == null:
|
||||
_build_contents()
|
||||
var parent_viewport: Viewport = get_parent().get_viewport() if get_parent() != null else get_viewport()
|
||||
var safe_rect: Rect2 = parent_viewport.get_visible_rect()
|
||||
var margin := 8.0
|
||||
var popup_size := _items.get_combined_minimum_size() + Vector2(SHELL_INSET * 2, SHELL_INSET * 2)
|
||||
var border_width := float(_theme_px(&"normal_border", 1))
|
||||
popup_size += Vector2(border_width * 2.0, border_width * 2.0)
|
||||
popup_size.x = maxf(popup_size.x, float(_menu_min_width() + SHELL_INSET * 2))
|
||||
var max_popup_size := Vector2(maxf(1.0, safe_rect.size.x - margin * 2.0), maxf(1.0, safe_rect.size.y - margin * 2.0))
|
||||
popup_size = popup_size.min(max_popup_size)
|
||||
if _scroll != null:
|
||||
var content_height: float = _items.get_combined_minimum_size().y
|
||||
var available_content_height: float = max_popup_size.y - float(SHELL_INSET * 2) - border_width * 2.0
|
||||
_scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO if content_height > available_content_height else ScrollContainer.SCROLL_MODE_DISABLED
|
||||
size = Vector2i(popup_size.ceil())
|
||||
if _scroll != null:
|
||||
_scroll.scroll_vertical = 0
|
||||
var max_x := maxf(safe_rect.position.x + margin, safe_rect.end.x - popup_size.x - margin)
|
||||
var max_y := maxf(safe_rect.position.y + margin, safe_rect.end.y - popup_size.y - margin)
|
||||
var popup_origin := _choose_popup_origin(
|
||||
global_position,
|
||||
avoid_rect,
|
||||
popup_size,
|
||||
safe_rect,
|
||||
margin,
|
||||
max_x,
|
||||
max_y
|
||||
)
|
||||
var popup_position := Vector2i(popup_origin.round())
|
||||
popup_on_parent(Rect2i(popup_position, Vector2i.ONE))
|
||||
# PopupPanel sizes itself from the child layout on popup. Reapply the measured
|
||||
# target size after opening so the scroll region can honor the 48 px rows.
|
||||
size = Vector2i(popup_size.ceil())
|
||||
var actual_size := Vector2(size)
|
||||
if popup_position.x + actual_size.x > safe_rect.end.x - margin:
|
||||
popup_origin.x = global_position.x - actual_size.x - margin
|
||||
if popup_position.y + actual_size.y > safe_rect.end.y - margin:
|
||||
popup_origin.y = global_position.y - actual_size.y - margin
|
||||
var actual_max_x := maxf(safe_rect.position.x + margin, safe_rect.end.x - actual_size.x - margin)
|
||||
var actual_max_y := maxf(safe_rect.position.y + margin, safe_rect.end.y - actual_size.y - margin)
|
||||
popup_origin.x = clampf(popup_origin.x, safe_rect.position.x + margin, actual_max_x)
|
||||
popup_origin.y = clampf(popup_origin.y, safe_rect.position.y + margin, actual_max_y)
|
||||
position = Vector2i(popup_origin.round())
|
||||
|
||||
|
||||
func _choose_popup_origin(
|
||||
global_position: Vector2,
|
||||
avoid_rect: Rect2,
|
||||
popup_size: Vector2,
|
||||
safe_rect: Rect2,
|
||||
margin: float,
|
||||
max_x: float,
|
||||
max_y: float
|
||||
) -> Vector2:
|
||||
var low_x: float = safe_rect.position.x + margin
|
||||
var low_y: float = safe_rect.position.y + margin
|
||||
var cursor_fallback := Vector2(
|
||||
clampf(global_position.x + margin, low_x, max_x),
|
||||
clampf(global_position.y + margin, low_y, max_y)
|
||||
)
|
||||
if not avoid_rect.has_area():
|
||||
return cursor_fallback
|
||||
|
||||
var aligned_y := clampf(global_position.y - 20.0, low_y, max_y)
|
||||
var aligned_x := clampf(global_position.x + margin, low_x, max_x)
|
||||
var candidates: Array[Vector2] = [
|
||||
Vector2(avoid_rect.end.x + TARGET_GAP, aligned_y),
|
||||
Vector2(avoid_rect.position.x - popup_size.x - TARGET_GAP, aligned_y),
|
||||
Vector2(aligned_x, avoid_rect.end.y + TARGET_GAP),
|
||||
Vector2(aligned_x, avoid_rect.position.y - popup_size.y - TARGET_GAP),
|
||||
cursor_fallback,
|
||||
]
|
||||
for candidate: Vector2 in candidates:
|
||||
if candidate.x < low_x or candidate.y < low_y or candidate.x > max_x or candidate.y > max_y:
|
||||
continue
|
||||
var candidate_rect := Rect2(candidate, popup_size)
|
||||
if not candidate_rect.intersects(avoid_rect):
|
||||
return candidate
|
||||
return cursor_fallback
|
||||
|
||||
|
||||
func get_row_button(id: int) -> Button:
|
||||
return _row_ids.get(id) as Button
|
||||
|
||||
|
||||
func _build_contents() -> void:
|
||||
if _items != null:
|
||||
return
|
||||
_shell = MarginContainer.new()
|
||||
_shell.name = "MenuShell"
|
||||
_shell.add_theme_constant_override("margin_left", SHELL_INSET)
|
||||
_shell.add_theme_constant_override("margin_top", SHELL_INSET)
|
||||
_shell.add_theme_constant_override("margin_right", SHELL_INSET)
|
||||
_shell.add_theme_constant_override("margin_bottom", SHELL_INSET)
|
||||
_shell.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_shell.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
add_child(_shell)
|
||||
_scroll = ScrollContainer.new()
|
||||
_scroll.name = "MenuScroll"
|
||||
_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
_shell.add_child(_scroll)
|
||||
_items = VBoxContainer.new()
|
||||
_items.name = "MenuItems"
|
||||
_items.add_theme_constant_override("separation", 0)
|
||||
_items.custom_minimum_size.x = float(_menu_min_width())
|
||||
_scroll.add_child(_items)
|
||||
_apply_visual_style()
|
||||
|
||||
|
||||
func _apply_visual_style() -> void:
|
||||
if _visual_theme == null:
|
||||
return
|
||||
var surface := _theme_color(&"surface", Color("#252B38"))
|
||||
var border := _theme_color(&"border", Color("#FFFFFF33"))
|
||||
var radius := _theme_px(&"panel", 10)
|
||||
var panel := StyleBoxFlat.new()
|
||||
panel.bg_color = surface
|
||||
panel.border_color = border
|
||||
panel.set_border_width_all(_theme_px(&"normal_border", 1))
|
||||
panel.set_corner_radius_all(radius)
|
||||
panel.shadow_color = Color(0.0, 0.0, 0.0, 0.28)
|
||||
panel.shadow_size = 8
|
||||
panel.shadow_offset = Vector2(0.0, 3.0)
|
||||
add_theme_stylebox_override("panel", panel)
|
||||
if _shell != null:
|
||||
_shell.add_theme_constant_override("margin_left", SHELL_INSET)
|
||||
_shell.add_theme_constant_override("margin_top", SHELL_INSET)
|
||||
_shell.add_theme_constant_override("margin_right", SHELL_INSET)
|
||||
_shell.add_theme_constant_override("margin_bottom", SHELL_INSET)
|
||||
if _items != null:
|
||||
_items.custom_minimum_size.x = float(_menu_min_width())
|
||||
for button: Button in _row_buttons:
|
||||
if not is_instance_valid(button):
|
||||
continue
|
||||
button.custom_minimum_size = Vector2(_menu_min_width(), _menu_row_height())
|
||||
button.add_theme_font_size_override("font_size", _font_size())
|
||||
var role := int(button.get_meta("context_menu_role", ItemRole.NORMAL))
|
||||
var item_role: ItemRole = ItemRole.DESTRUCTIVE if role == ItemRole.DESTRUCTIVE else ItemRole.NORMAL
|
||||
button.add_theme_color_override("font_color", _text_color(item_role))
|
||||
button.add_theme_color_override("font_hover_color", _text_color(item_role))
|
||||
button.add_theme_color_override("font_pressed_color", _text_color(item_role))
|
||||
button.add_theme_color_override("font_focus_color", _text_color(item_role))
|
||||
button.add_theme_stylebox_override("normal", _make_row_box(Color(0.0, 0.0, 0.0, 0.0), Color(0, 0, 0, 0), 0))
|
||||
button.add_theme_stylebox_override("hover", _make_hover_box())
|
||||
button.add_theme_stylebox_override("pressed", _make_hover_box(true))
|
||||
button.add_theme_stylebox_override("focus", _make_focus_box())
|
||||
|
||||
|
||||
func _make_row_box(fill: Color, border: Color, border_width: int) -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = fill
|
||||
style.border_color = border
|
||||
style.set_border_width_all(border_width)
|
||||
style.set_corner_radius_all(ROW_RADIUS)
|
||||
style.content_margin_left = 12
|
||||
style.content_margin_right = 12
|
||||
style.content_margin_top = 4
|
||||
style.content_margin_bottom = 4
|
||||
return style
|
||||
|
||||
|
||||
func _make_hover_box(pressed: bool = false) -> StyleBoxFlat:
|
||||
var hover_color := _theme_color(&"surface_hover", Color("#30394A"))
|
||||
if pressed:
|
||||
hover_color = hover_color.darkened(0.1)
|
||||
var style := _make_row_box(hover_color, _theme_color(&"edit", Color("#22C6FF")), 0)
|
||||
style.border_width_left = 3
|
||||
style.content_margin_left = 9
|
||||
return style
|
||||
|
||||
|
||||
func _make_focus_box() -> StyleBoxFlat:
|
||||
var style := _make_row_box(Color(0.0, 0.0, 0.0, 0.0), _theme_color(&"focus", Color("#8BD9FF")), 2)
|
||||
return style
|
||||
|
||||
|
||||
func _on_row_pressed(button: Button) -> void:
|
||||
if not is_instance_valid(button):
|
||||
return
|
||||
var id: int = int(button.get_meta("context_menu_id", -1))
|
||||
id_pressed.emit(id)
|
||||
hide()
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if not visible or not event is InputEventKey:
|
||||
return
|
||||
var key_event := event as InputEventKey
|
||||
if not key_event.pressed:
|
||||
return
|
||||
if key_event.keycode == KEY_DOWN:
|
||||
_move_keyboard_focus(1)
|
||||
get_viewport().set_input_as_handled()
|
||||
elif key_event.keycode == KEY_UP:
|
||||
_move_keyboard_focus(-1)
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _move_keyboard_focus(direction: int) -> void:
|
||||
if _row_buttons.is_empty():
|
||||
return
|
||||
var candidate_index: int
|
||||
if not _keyboard_navigation_active:
|
||||
candidate_index = 0 if direction > 0 else _row_buttons.size() - 1
|
||||
else:
|
||||
candidate_index = _focused_row_index + direction
|
||||
for _attempt: int in range(_row_buttons.size()):
|
||||
candidate_index = posmod(candidate_index, _row_buttons.size())
|
||||
var button: Button = _row_buttons[candidate_index]
|
||||
if is_instance_valid(button) and button.visible and not button.disabled:
|
||||
_enable_keyboard_navigation()
|
||||
_focused_row_index = candidate_index
|
||||
button.grab_focus()
|
||||
return
|
||||
candidate_index += direction
|
||||
|
||||
|
||||
func _enable_keyboard_navigation() -> void:
|
||||
_keyboard_navigation_active = true
|
||||
for button: Button in _row_buttons:
|
||||
if is_instance_valid(button):
|
||||
button.focus_mode = Control.FOCUS_ALL
|
||||
|
||||
|
||||
func _reset_keyboard_navigation() -> void:
|
||||
_keyboard_navigation_active = false
|
||||
_focused_row_index = -1
|
||||
if not is_node_ready():
|
||||
return
|
||||
var focus_owner: Control = get_viewport().gui_get_focus_owner()
|
||||
if focus_owner != null and _row_buttons.has(focus_owner):
|
||||
focus_owner.release_focus()
|
||||
for button: Button in _row_buttons:
|
||||
if is_instance_valid(button):
|
||||
button.focus_mode = Control.FOCUS_NONE
|
||||
|
||||
|
||||
func _on_row_mouse_entered(_button: Button) -> void:
|
||||
_reset_keyboard_navigation()
|
||||
|
||||
|
||||
func _theme_color(role: StringName, fallback: Color) -> Color:
|
||||
if _visual_theme == null:
|
||||
return fallback
|
||||
return _visual_theme.chrome_color(role, _editor)
|
||||
|
||||
|
||||
func _theme_px(role: StringName, fallback: int) -> int:
|
||||
if _visual_theme == null:
|
||||
return fallback
|
||||
return _visual_theme.chrome_px(role, _editor)
|
||||
|
||||
|
||||
func _font_size() -> int:
|
||||
if _font_size_override > 0:
|
||||
return _font_size_override
|
||||
return _theme_px(&"context_menu", 20)
|
||||
|
||||
|
||||
func _menu_row_height() -> int:
|
||||
return maxi(_theme_px(&"context_menu_row_height", DEFAULT_ROW_HEIGHT), DEFAULT_ROW_HEIGHT)
|
||||
|
||||
|
||||
func _menu_min_width() -> int:
|
||||
return maxi(_theme_px(&"context_menu_min_width", DEFAULT_MIN_WIDTH), DEFAULT_MIN_WIDTH)
|
||||
|
||||
|
||||
func _text_color(role: ItemRole) -> Color:
|
||||
if role == ItemRole.DESTRUCTIVE:
|
||||
return _theme_color(&"destructive", Color("#E66B75"))
|
||||
return _theme_color(&"text", Color("#F5F7FA"))
|
||||
@@ -0,0 +1 @@
|
||||
uid://bjidsdsxomilt
|
||||
@@ -0,0 +1,231 @@
|
||||
class_name EditorSelectionDropdown
|
||||
extends MenuButton
|
||||
## Single-choice editor picker with a Studio-style selected row and no radio lane.
|
||||
|
||||
signal item_selected(index: int)
|
||||
|
||||
var selected: int = -1:
|
||||
set(value):
|
||||
_set_selected(value)
|
||||
get:
|
||||
return _selected_index
|
||||
|
||||
var _selected_index: int = -1
|
||||
var _popup_menu: PopupMenu
|
||||
var _field_icons: Array[Texture2D] = []
|
||||
var _control_theme: Theme
|
||||
var _surface_color: Color = Color("#252B38")
|
||||
var _selected_surface_color: Color = Color("#103B4D")
|
||||
var _border_color: Color = Color("#FFFFFF33")
|
||||
var _accent_color: Color = Color("#22C6FF")
|
||||
var _focus_color: Color = Color("#8BD9FF")
|
||||
var _text_color: Color = Color("#F5F7FA")
|
||||
var _font_size: int = 18
|
||||
var _row_separation: int = 6
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
flat = false
|
||||
focus_mode = Control.FOCUS_ALL
|
||||
alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
_popup_menu = get_popup()
|
||||
_popup_menu.shrink_width = false
|
||||
_popup_menu.transparent = false
|
||||
_popup_menu.transparent_bg = false
|
||||
_popup_menu.id_pressed.connect(_on_popup_item_pressed)
|
||||
about_to_popup.connect(_on_about_to_popup)
|
||||
_apply_popup_theme()
|
||||
_apply_field_theme()
|
||||
_sync_selected_item()
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func configure(
|
||||
control_theme: Theme,
|
||||
surface_color: Color,
|
||||
selected_surface_color: Color,
|
||||
border_color: Color,
|
||||
accent_color: Color,
|
||||
focus_color: Color,
|
||||
text_color: Color,
|
||||
font_size: int,
|
||||
row_separation: int = 6
|
||||
) -> void:
|
||||
_control_theme = control_theme
|
||||
_surface_color = surface_color
|
||||
_selected_surface_color = selected_surface_color
|
||||
_border_color = border_color
|
||||
_accent_color = accent_color
|
||||
_focus_color = focus_color
|
||||
_text_color = text_color
|
||||
_font_size = font_size
|
||||
_row_separation = maxi(0, row_separation)
|
||||
if is_node_ready():
|
||||
_apply_popup_theme()
|
||||
|
||||
|
||||
func add_item(label: String, id: int = -1) -> void:
|
||||
var index: int = get_popup().item_count
|
||||
var item_id: int = index if id < 0 else id
|
||||
add_styled_item(label, null, null, item_id)
|
||||
|
||||
|
||||
func add_styled_item(
|
||||
label: String,
|
||||
field_icon: Texture2D = null,
|
||||
popup_icon: Texture2D = null,
|
||||
id: int = -1
|
||||
) -> void:
|
||||
var popup: PopupMenu = get_popup()
|
||||
var index: int = popup.item_count
|
||||
var item_id: int = index if id < 0 else id
|
||||
_field_icons.append(field_icon)
|
||||
if popup_icon != null:
|
||||
popup.add_icon_item(popup_icon, label, item_id)
|
||||
popup.set_item_icon_max_width(index, 44)
|
||||
else:
|
||||
popup.add_item(label, item_id)
|
||||
popup.set_item_as_radio_checkable(index, false)
|
||||
if _selected_index < 0:
|
||||
_set_selected(index)
|
||||
|
||||
|
||||
func get_item_text(index: int) -> String:
|
||||
return get_popup().get_item_text(index)
|
||||
|
||||
|
||||
func get_item_icon(index: int) -> Texture2D:
|
||||
return _field_icons[index] if index >= 0 and index < _field_icons.size() else null
|
||||
|
||||
|
||||
func select(index: int) -> void:
|
||||
_set_selected(index)
|
||||
|
||||
|
||||
func clear() -> void:
|
||||
get_popup().clear()
|
||||
_field_icons.clear()
|
||||
_selected_index = -1
|
||||
_sync_selected_item()
|
||||
|
||||
|
||||
func hide_popup() -> void:
|
||||
get_popup().hide()
|
||||
|
||||
|
||||
func _set_selected(index: int) -> void:
|
||||
var count: int = get_popup().item_count
|
||||
if index < 0 or count == 0:
|
||||
_selected_index = -1
|
||||
else:
|
||||
_selected_index = clampi(index, 0, count - 1)
|
||||
_sync_selected_item()
|
||||
|
||||
|
||||
func _sync_selected_item() -> void:
|
||||
if not is_instance_valid(get_popup()):
|
||||
return
|
||||
if _selected_index < 0 or _selected_index >= get_popup().item_count:
|
||||
text = ""
|
||||
icon = null
|
||||
return
|
||||
text = get_popup().get_item_text(_selected_index)
|
||||
icon = _field_icons[_selected_index] if _selected_index < _field_icons.size() else null
|
||||
|
||||
|
||||
func _apply_popup_theme() -> void:
|
||||
if _popup_menu == null:
|
||||
return
|
||||
if _control_theme != null:
|
||||
_popup_menu.theme = _control_theme
|
||||
var panel := StyleBoxFlat.new()
|
||||
var opaque_surface: Color = _surface_color
|
||||
opaque_surface.a = 1.0
|
||||
panel.bg_color = opaque_surface
|
||||
panel.border_color = _border_color
|
||||
panel.set_border_width_all(1)
|
||||
panel.set_corner_radius_all(8)
|
||||
panel.content_margin_left = 6.0
|
||||
panel.content_margin_top = 6.0
|
||||
panel.content_margin_right = 6.0
|
||||
panel.content_margin_bottom = 6.0
|
||||
_popup_menu.add_theme_stylebox_override("panel", panel)
|
||||
var selected_row := StyleBoxFlat.new()
|
||||
selected_row.bg_color = _selected_surface_color
|
||||
selected_row.border_color = _accent_color
|
||||
selected_row.set_border_width_all(2)
|
||||
selected_row.set_corner_radius_all(7)
|
||||
selected_row.content_margin_left = 5.0
|
||||
selected_row.content_margin_top = 12.0
|
||||
selected_row.content_margin_right = 5.0
|
||||
selected_row.content_margin_bottom = 12.0
|
||||
_popup_menu.add_theme_stylebox_override("hover", selected_row)
|
||||
_popup_menu.add_theme_color_override("font_hover_color", _text_color)
|
||||
_popup_menu.add_theme_color_override("font_color", _text_color)
|
||||
_popup_menu.add_theme_font_size_override("font_size", _font_size)
|
||||
_popup_menu.add_theme_constant_override("icon_max_width", 44)
|
||||
_popup_menu.add_theme_constant_override("h_separation", 10)
|
||||
_popup_menu.add_theme_constant_override("v_separation", _row_separation)
|
||||
_popup_menu.add_theme_constant_override("item_start_padding", 4)
|
||||
_popup_menu.add_theme_constant_override("item_end_padding", 4)
|
||||
add_theme_stylebox_override("focus", _make_focus_style())
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _apply_field_theme() -> void:
|
||||
for state: String in ["normal", "hover", "pressed", "disabled"]:
|
||||
var original: StyleBox = get_theme_stylebox(state, "Button")
|
||||
if original == null:
|
||||
continue
|
||||
var field_style: StyleBox = original.duplicate()
|
||||
field_style.content_margin_right = maxf(field_style.content_margin_right, 28.0)
|
||||
add_theme_stylebox_override(state, field_style)
|
||||
|
||||
|
||||
func _make_focus_style() -> StyleBoxFlat:
|
||||
var focus_box := StyleBoxFlat.new()
|
||||
focus_box.bg_color = Color(0.0, 0.0, 0.0, 0.0)
|
||||
focus_box.border_color = _focus_color
|
||||
focus_box.set_border_width_all(2)
|
||||
focus_box.set_corner_radius_all(8)
|
||||
focus_box.content_margin_right = 28.0
|
||||
return focus_box
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
var center := Vector2(size.x - 16.0, size.y * 0.5)
|
||||
var points := PackedVector2Array([
|
||||
center + Vector2(-4.0, -2.0),
|
||||
center + Vector2(0.0, 2.0),
|
||||
center + Vector2(4.0, -2.0),
|
||||
])
|
||||
draw_polyline(points, _text_color.darkened(0.28), 2.0, true)
|
||||
|
||||
|
||||
func _on_about_to_popup() -> void:
|
||||
var target_width: int = maxi(roundi(size.x), _popup_menu.min_size.x)
|
||||
_popup_menu.min_size = Vector2i(target_width, _popup_menu.min_size.y)
|
||||
_popup_menu.size = Vector2i(maxi(_popup_menu.size.x, target_width), _popup_menu.size.y)
|
||||
call_deferred("_sync_popup_size")
|
||||
if _selected_index >= 0:
|
||||
_popup_menu.set_focused_item(_selected_index)
|
||||
else:
|
||||
_popup_menu.set_focused_item(-1)
|
||||
|
||||
|
||||
func _sync_popup_size() -> void:
|
||||
if _popup_menu == null:
|
||||
return
|
||||
var target_width: int = maxi(roundi(size.x), _popup_menu.min_size.x)
|
||||
_popup_menu.min_size = Vector2i(target_width, _popup_menu.min_size.y)
|
||||
_popup_menu.size = Vector2i(maxi(_popup_menu.size.x, target_width), _popup_menu.size.y)
|
||||
if _selected_index >= 0:
|
||||
_popup_menu.set_focused_item(_selected_index)
|
||||
|
||||
|
||||
func _on_popup_item_pressed(id: int) -> void:
|
||||
var index: int = get_popup().get_item_index(id)
|
||||
if index < 0 or index == _selected_index:
|
||||
return
|
||||
_set_selected(index)
|
||||
item_selected.emit(index)
|
||||
@@ -0,0 +1 @@
|
||||
uid://qlg14pj6jfj7
|
||||
@@ -13,6 +13,7 @@ const DEFAULT_CONFIG: Dictionary = {
|
||||
"background": "#171B24F2",
|
||||
"surface": "#252B38FA",
|
||||
"surface_hover": "#30394A",
|
||||
"selected_surface": "#103B4D",
|
||||
"text": "#F5F7FA",
|
||||
"text_muted": "#AAB2C0",
|
||||
"edit": "#22C6FF",
|
||||
@@ -32,6 +33,8 @@ const DEFAULT_CONFIG: Dictionary = {
|
||||
"section": 22,
|
||||
"title": 26,
|
||||
"menu": 20,
|
||||
"menu_bar": 22,
|
||||
"menu_popup": 26,
|
||||
"status": 16,
|
||||
"mode": 24,
|
||||
"prop_name": 20,
|
||||
@@ -40,7 +43,13 @@ const DEFAULT_CONFIG: Dictionary = {
|
||||
"spacing": {"small": 8, "medium": 12, "large": 16},
|
||||
"radii": {"control": 8, "panel": 10},
|
||||
"borders": {"normal": 1, "focus": 2},
|
||||
"targets": {"button_min_height": 44, "row_min_height": 44, "mode_button_min_height": 56},
|
||||
"targets": {
|
||||
"button_min_height": 44,
|
||||
"row_min_height": 44,
|
||||
"mode_button_min_height": 56,
|
||||
"context_menu_row_height": 48,
|
||||
"context_menu_min_width": 208,
|
||||
},
|
||||
},
|
||||
"editors": {"prop": {}, "stickman": {}},
|
||||
"stickman_canvas": {
|
||||
@@ -54,6 +63,7 @@ const COLOR_ROLE_SECTIONS: Dictionary = {
|
||||
"background": "colors",
|
||||
"surface": "colors",
|
||||
"surface_hover": "colors",
|
||||
"selected_surface": "colors",
|
||||
"text": "colors",
|
||||
"text_muted": "colors",
|
||||
"edit": "colors",
|
||||
@@ -73,6 +83,8 @@ const SIZE_ROLE_SECTIONS: Dictionary = {
|
||||
"section": "fonts",
|
||||
"title": "fonts",
|
||||
"menu": "fonts",
|
||||
"menu_bar": "fonts",
|
||||
"menu_popup": "fonts",
|
||||
"status": "fonts",
|
||||
"mode": "fonts",
|
||||
"prop_name": "fonts",
|
||||
@@ -87,6 +99,8 @@ const SIZE_ROLE_SECTIONS: Dictionary = {
|
||||
"button_min_height": "targets",
|
||||
"row_min_height": "targets",
|
||||
"mode_button_min_height": "targets",
|
||||
"context_menu_row_height": "targets",
|
||||
"context_menu_min_width": "targets",
|
||||
}
|
||||
const CONFIG_SIZE_KEYS: Dictionary = {
|
||||
"normal_border": "normal",
|
||||
@@ -208,13 +222,12 @@ func control_theme(editor: StringName) -> Theme:
|
||||
theme.set_font_size("font_size", type_name, chrome_px(&"body", editor))
|
||||
var panel_box: StyleBoxFlat = _make_box(surface, border, chrome_px(&"panel", editor), normal_width)
|
||||
theme.set_stylebox("panel", "PanelContainer", panel_box)
|
||||
theme.set_stylebox("normal", "PopupMenu", panel_box.duplicate())
|
||||
theme.set_stylebox("panel", "MenuBar", panel_box.duplicate())
|
||||
theme.set_font_size("font_size", "Label", chrome_px(&"body", editor))
|
||||
theme.set_color("font_color", "Label", text)
|
||||
theme.set_color("font_hover_color", "Label", text)
|
||||
theme.set_font_size("font_size", "PopupMenu", chrome_px(&"menu", editor))
|
||||
theme.set_font_size("font_size", "MenuBar", chrome_px(&"menu", editor))
|
||||
theme.set_font_size("font_size", "PopupMenu", chrome_px(&"menu_popup", editor))
|
||||
theme.set_font_size("font_size", "MenuBar", chrome_px(&"menu_bar", editor))
|
||||
theme.set_color("font_color", "MenuBar", text)
|
||||
return theme
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class_name StageDialogStyler
|
||||
extends RefCounted
|
||||
## Renders child-facing dialogs as a shared in-stage card while preserving the
|
||||
## native AcceptDialog/ConfirmationDialog signals used by the stage workflows.
|
||||
## Renders child-facing stage and editor dialogs as a shared card while preserving
|
||||
## the native AcceptDialog/ConfirmationDialog signals used by their workflows.
|
||||
|
||||
const THEME_DEFAULTS := preload("res://scripts/stage/sandbox_theme.gd")
|
||||
const OVERLAY_META := &"stage_dialog_overlay"
|
||||
@@ -20,7 +20,9 @@ const ICON_META := &"stage_dialog_icon"
|
||||
const ICON_LABEL_META := &"stage_dialog_icon_label"
|
||||
const ICON_BADGE_META := &"stage_dialog_icon_badge"
|
||||
const PRIMARY_META := &"stage_dialog_primary_button"
|
||||
const SECONDARY_META := &"stage_dialog_secondary_buttons"
|
||||
const DESTRUCTIVE_META := &"stage_dialog_destructive_buttons"
|
||||
const DEFAULT_PRIMARY_META := &"stage_dialog_default_primary"
|
||||
const OPEN_META := &"stage_dialog_open"
|
||||
|
||||
|
||||
@@ -29,7 +31,9 @@ static func apply(
|
||||
stage_theme: SandboxTheme,
|
||||
ui_font: Font,
|
||||
primary_button: Button = null,
|
||||
destructive_buttons: Array = []
|
||||
destructive_buttons: Array = [],
|
||||
secondary_buttons: Array = [],
|
||||
default_ok_is_primary: bool = true
|
||||
) -> void:
|
||||
if dialog == null:
|
||||
return
|
||||
@@ -37,10 +41,14 @@ static func apply(
|
||||
if not dialog.has_meta(OVERLAY_META):
|
||||
_install_dialog_surface(dialog, active_theme, ui_font)
|
||||
var ok_button: Button = (dialog as AcceptDialog).get_ok_button() if dialog is AcceptDialog else null
|
||||
var primary: Button = primary_button if primary_button != null else ok_button
|
||||
var primary: Button = primary_button
|
||||
if primary == null and default_ok_is_primary and not _contains_button(destructive_buttons, ok_button):
|
||||
primary = ok_button
|
||||
dialog.set_meta(PRIMARY_META, primary)
|
||||
dialog.set_meta(SECONDARY_META, secondary_buttons.duplicate())
|
||||
dialog.set_meta(DESTRUCTIVE_META, destructive_buttons.duplicate())
|
||||
_refresh_dialog_surface(dialog, active_theme, ui_font, primary, destructive_buttons)
|
||||
dialog.set_meta(DEFAULT_PRIMARY_META, default_ok_is_primary)
|
||||
_refresh_dialog_surface(dialog, active_theme, ui_font, primary, destructive_buttons, secondary_buttons, default_ok_is_primary)
|
||||
|
||||
|
||||
static func popup(dialog: Window) -> void:
|
||||
@@ -48,9 +56,11 @@ static func popup(dialog: Window) -> void:
|
||||
return
|
||||
var active_theme: SandboxTheme = _theme_for(dialog)
|
||||
var font: Font = dialog.get_meta(&"stage_dialog_font") as Font if dialog.has_meta(&"stage_dialog_font") else null
|
||||
var primary: Button = dialog.get_meta(PRIMARY_META, null) as Button
|
||||
var primary: Button = dialog.get_meta(PRIMARY_META) as Button if dialog.has_meta(PRIMARY_META) else null
|
||||
var secondary_buttons: Array = dialog.get_meta(SECONDARY_META, []) as Array
|
||||
var destructive_buttons: Array = dialog.get_meta(DESTRUCTIVE_META, []) as Array
|
||||
_refresh_dialog_surface(dialog, active_theme, font, primary, destructive_buttons)
|
||||
var default_ok_is_primary: bool = bool(dialog.get_meta(DEFAULT_PRIMARY_META, true))
|
||||
_refresh_dialog_surface(dialog, active_theme, font, primary, destructive_buttons, secondary_buttons, default_ok_is_primary)
|
||||
var overlay: Control = dialog.get_meta(OVERLAY_META) as Control
|
||||
if overlay == null:
|
||||
return
|
||||
@@ -71,8 +81,10 @@ static func popup(dialog: Window) -> void:
|
||||
if is_instance_valid(tree):
|
||||
tree.process_frame.connect(fit_after_layout, Object.CONNECT_ONE_SHOT)
|
||||
tree.process_frame.connect(defer_fit_one_frame, Object.CONNECT_ONE_SHOT)
|
||||
var focus_primary: Button = dialog.get_meta(PRIMARY_META, null) as Button
|
||||
var focus_primary: Button = dialog.get_meta(PRIMARY_META) as Button if dialog.has_meta(PRIMARY_META) else null
|
||||
var focus_target: Button = visual_button(focus_primary)
|
||||
if focus_target == null and dialog is ConfirmationDialog:
|
||||
focus_target = visual_button((dialog as ConfirmationDialog).get_cancel_button())
|
||||
if focus_target != null and focus_target.visible:
|
||||
focus_target.call_deferred("grab_focus")
|
||||
|
||||
@@ -232,6 +244,7 @@ static func _install_dialog_surface(dialog: Window, stage_theme: SandboxTheme, u
|
||||
var overlay := Control.new()
|
||||
overlay.name = "StageDialogOverlay"
|
||||
overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
overlay.theme = stage_theme.control_theme()
|
||||
overlay.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
overlay.z_index = 100
|
||||
overlay.z_as_relative = false
|
||||
@@ -385,7 +398,9 @@ static func _refresh_dialog_surface(
|
||||
stage_theme: SandboxTheme,
|
||||
ui_font: Font,
|
||||
primary_button: Button = null,
|
||||
destructive_buttons: Array = []
|
||||
destructive_buttons: Array = [],
|
||||
secondary_buttons: Array = [],
|
||||
default_ok_is_primary: bool = true
|
||||
) -> void:
|
||||
if not dialog.has_meta(OVERLAY_META):
|
||||
return
|
||||
@@ -458,16 +473,24 @@ static func _refresh_dialog_surface(
|
||||
for old_button: Node in action_row.get_children():
|
||||
action_row.remove_child(old_button)
|
||||
old_button.queue_free()
|
||||
var primary: Button = primary_button if primary_button != null else ok_button
|
||||
var primary: Button = primary_button
|
||||
if primary == null and default_ok_is_primary and not _contains_button(destructive_buttons, ok_button):
|
||||
primary = ok_button
|
||||
if ok_button != null:
|
||||
ok_button.visible = false
|
||||
if cancel_button != null:
|
||||
cancel_button.visible = false
|
||||
for raw_secondary: Variant in secondary_buttons:
|
||||
if raw_secondary is Button:
|
||||
(raw_secondary as Button).visible = false
|
||||
for raw_button: Variant in destructive_buttons:
|
||||
if raw_button is Button:
|
||||
(raw_button as Button).visible = false
|
||||
var ordered: Array[Button] = []
|
||||
_append_unique_button(ordered, primary)
|
||||
for raw_secondary: Variant in secondary_buttons:
|
||||
if raw_secondary is Button:
|
||||
_append_unique_button(ordered, raw_secondary as Button)
|
||||
for raw_destructive: Variant in destructive_buttons:
|
||||
if raw_destructive is Button:
|
||||
_append_unique_button(ordered, raw_destructive as Button)
|
||||
@@ -476,6 +499,8 @@ static func _refresh_dialog_surface(
|
||||
for original: Button in ordered:
|
||||
var visual: Button = Button.new()
|
||||
visual.text = original.text
|
||||
visual.disabled = original.disabled
|
||||
visual.tooltip_text = original.tooltip_text
|
||||
visual.name = "Action_%s" % String(original.name)
|
||||
visual.size_flags_horizontal = Control.SIZE_SHRINK_END
|
||||
var role: StringName = &"secondary"
|
||||
@@ -535,18 +560,18 @@ static func _find_overlay_host(dialog: Window) -> Node:
|
||||
return ancestor
|
||||
ancestor = ancestor.get_parent()
|
||||
var tree: SceneTree = dialog.get_tree()
|
||||
var scene: Node = tree.current_scene if tree != null else null
|
||||
var scene: Node = tree.current_scene if tree != null and tree.current_scene != null else (tree.root if tree != null else null)
|
||||
if scene != null:
|
||||
var ui_layer: Node = scene.get_node_or_null("UI")
|
||||
if ui_layer is CanvasLayer:
|
||||
return ui_layer
|
||||
var existing: Node = scene.get_node_or_null("StageDialogLayer")
|
||||
var existing: Node = scene.find_child("StageDialogLayer", true, false)
|
||||
if existing is CanvasLayer:
|
||||
return existing
|
||||
var layer := CanvasLayer.new()
|
||||
layer.name = "StageDialogLayer"
|
||||
layer.layer = 100
|
||||
scene.add_child(layer)
|
||||
scene.add_child.call_deferred(layer)
|
||||
return layer
|
||||
return null
|
||||
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
extends SceneTree
|
||||
|
||||
const EDITOR_VISUAL_THEME := preload("res://scripts/shared/editor_ui/editor_visual_theme.gd")
|
||||
const EDITOR_CONTEXT_MENU := preload("res://scripts/shared/editor_ui/editor_context_menu.gd")
|
||||
const STAGE_DIALOG_STYLER := preload("res://scripts/shared/stage_ui/stage_dialog_styler.gd")
|
||||
const PROP_SCENE := preload("res://scenes/editors/prop/prop_editor.tscn")
|
||||
const STICKMAN_SCENE := preload("res://scenes/main.tscn")
|
||||
|
||||
var _checks: int = 0
|
||||
var _failures: int = 0
|
||||
var _last_pressed_id: int = -1
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
root.size = Vector2i(1280, 800)
|
||||
root.gui_embed_subwindows = true
|
||||
var visual_theme: EditorVisualTheme = EDITOR_VISUAL_THEME.new()
|
||||
check("shared editor theme loads", visual_theme.load_from(EDITOR_VISUAL_THEME.DEFAULT_PATH))
|
||||
|
||||
var menu: EditorContextMenu = EDITOR_CONTEXT_MENU.new()
|
||||
root.add_child(menu)
|
||||
menu.configure(visual_theme, &"prop")
|
||||
menu.add_item("Copy Shape", 1)
|
||||
menu.add_separator()
|
||||
menu.add_destructive_item("Erase Shape", 2)
|
||||
menu.id_pressed.connect(_on_menu_item_pressed)
|
||||
var copy_button: Button = menu.get_row_button(1)
|
||||
var erase_button: Button = menu.get_row_button(2)
|
||||
check("normal and destructive rows are created", copy_button != null and erase_button != null)
|
||||
check("rows meet the Studio List target height", copy_button != null and copy_button.custom_minimum_size.y == 48.0)
|
||||
check("menu rows do not show a keyboard selector on popup", copy_button != null and copy_button.focus_mode == Control.FOCUS_NONE and not copy_button.has_focus())
|
||||
check("destructive label uses semantic danger color", erase_button != null and erase_button.get_theme_color("font_color").is_equal_approx(visual_theme.chrome_color(&"destructive", &"prop")))
|
||||
var panel_style: StyleBoxFlat = menu.get_theme_stylebox("panel") as StyleBoxFlat
|
||||
var hover_style: StyleBoxFlat = copy_button.get_theme_stylebox("hover") as StyleBoxFlat
|
||||
var focus_style: StyleBoxFlat = copy_button.get_theme_stylebox("focus") as StyleBoxFlat
|
||||
check("popup shell uses shared raised surface", panel_style != null and panel_style.bg_color.is_equal_approx(visual_theme.chrome_color(&"surface", &"prop")))
|
||||
check("hover row uses a cyan side cue", hover_style != null and hover_style.border_width_left == 3 and hover_style.border_color.is_equal_approx(visual_theme.chrome_color(&"edit", &"prop")))
|
||||
check("keyboard focus has a distinct focus outline", focus_style != null and focus_style.border_width_left == 2 and focus_style.border_color.is_equal_approx(visual_theme.chrome_color(&"focus", &"prop")))
|
||||
|
||||
root.size = Vector2i(900, 600)
|
||||
menu.popup_at(Vector2(root.size) - Vector2(4.0, 4.0))
|
||||
await process_frame
|
||||
check("popup opens at the requested edge", menu.visible)
|
||||
check("popup stays inside the viewport", menu.position.x >= 0 and menu.position.y >= 0 and menu.position.x + menu.size.x <= root.size.x and menu.position.y + menu.size.y <= root.size.y)
|
||||
var down_event := InputEventKey.new()
|
||||
down_event.keycode = KEY_DOWN
|
||||
down_event.pressed = true
|
||||
menu.call("_input", down_event)
|
||||
await process_frame
|
||||
check("Down reveals focus on the first row", copy_button.has_focus())
|
||||
var up_event := InputEventKey.new()
|
||||
up_event.keycode = KEY_UP
|
||||
up_event.pressed = true
|
||||
menu.call("_input", up_event)
|
||||
await process_frame
|
||||
check("Up moves focus to the last row", erase_button.has_focus())
|
||||
menu.call("_on_row_mouse_entered", copy_button)
|
||||
await process_frame
|
||||
check("pointer entry hides the keyboard selector", not copy_button.has_focus() and not erase_button.has_focus() and copy_button.focus_mode == Control.FOCUS_NONE)
|
||||
menu.hide()
|
||||
var avoid_rect := Rect2(340.0, 240.0, 220.0, 120.0)
|
||||
menu.popup_at(avoid_rect.get_center(), avoid_rect)
|
||||
await process_frame
|
||||
check("popup keeps the clicked target unobstructed when room is available", not Rect2(Vector2(menu.position), Vector2(menu.size)).intersects(avoid_rect))
|
||||
erase_button.emit_signal("pressed")
|
||||
await process_frame
|
||||
check("activating an item emits its id and closes the popup", _last_pressed_id == 2 and not menu.visible)
|
||||
menu.queue_free()
|
||||
await process_frame
|
||||
root.size = Vector2i(1280, 800)
|
||||
await _test_prop_context_menu()
|
||||
await _test_stickman_context_menus()
|
||||
|
||||
print("----------------------------------------")
|
||||
if _failures == 0:
|
||||
print("RESULT: ALL PASSED (%d assertions)" % _checks)
|
||||
quit(0)
|
||||
else:
|
||||
print("RESULT: %d FAILURE(S) out of %d assertions" % [_failures, _checks])
|
||||
quit(1)
|
||||
|
||||
|
||||
func _test_prop_context_menu() -> void:
|
||||
var editor: Control = PROP_SCENE.instantiate() as Control
|
||||
root.add_child(editor)
|
||||
await process_frame
|
||||
await process_frame
|
||||
_dismiss_fixture_recovery_prompt(editor)
|
||||
var canvas: PropCanvas = editor.get("_canvas") as PropCanvas
|
||||
canvas.add_primitive("box", Vector2.ZERO)
|
||||
var shape: Dictionary = (canvas.get("shapes") as Array)[0] as Dictionary
|
||||
var bounds: Rect2 = canvas.call("_shape_bbox", shape) as Rect2
|
||||
var center: Vector2 = bounds.get_center()
|
||||
var zoom: float = float(canvas.get("_zoom"))
|
||||
var pan: Vector2 = canvas.get("_pan_offset") as Vector2
|
||||
var click := InputEventMouseButton.new()
|
||||
click.global_position = canvas.global_position + center * zoom + pan
|
||||
canvas.call("_handle_transform_right_click", click, center)
|
||||
await process_frame
|
||||
var menu: EditorContextMenu = canvas.get("_context_menu") as EditorContextMenu
|
||||
check("Prop opens the shared Studio List menu", menu.visible)
|
||||
check("Prop uses child-readable shape action labels", menu.get_row_button(3) != null and menu.get_row_button(3).text == "Copy Shape" and menu.get_row_button(5).text == "Erase Shape")
|
||||
check("Prop destructive action uses danger color", menu.get_row_button(5) != null and menu.get_row_button(5).get_theme_color("font_color").is_equal_approx((editor.get("_editor_visual_theme") as EditorVisualTheme).chrome_color(&"destructive", &"prop")))
|
||||
check("Prop menu remains inside the viewport", menu.position.x >= 0 and menu.position.y >= 0 and menu.position.x + menu.size.x <= root.size.x and menu.position.y + menu.size.y <= root.size.y)
|
||||
var prop_target: Rect2 = canvas.call("_global_shape_target_rect", 0) as Rect2
|
||||
check("Prop menu leaves the clicked shape visible", not Rect2(Vector2(menu.position), Vector2(menu.size)).intersects(prop_target))
|
||||
check("Prop menu avoids an unnecessary scrollbar", not (menu.get_node("MenuShell/MenuScroll") as ScrollContainer).get_v_scroll_bar().visible)
|
||||
await _save_capture("context_menu_prop.png")
|
||||
menu.hide()
|
||||
editor.queue_free()
|
||||
await process_frame
|
||||
|
||||
|
||||
func _test_stickman_context_menus() -> void:
|
||||
var main_scene: Control = STICKMAN_SCENE.instantiate() as Control
|
||||
root.add_child(main_scene)
|
||||
await process_frame
|
||||
await process_frame
|
||||
var editor: Control = main_scene.find_child("StickmanEditor", true, false) as Control
|
||||
var panels: Dictionary = editor.get("_body_part_panels") as Dictionary
|
||||
var head: BodyPartPanel = panels.get("head") as BodyPartPanel
|
||||
head.add_shape(&"rectangle", head.call("drawing_center_world"))
|
||||
var shape: Dictionary = (head.get("shapes") as Array)[0] as Dictionary
|
||||
var points: PackedVector2Array = shape.get("points", PackedVector2Array()) as PackedVector2Array
|
||||
var center := Vector2.ZERO
|
||||
for point: Vector2 in points:
|
||||
center += point
|
||||
if not points.is_empty():
|
||||
center /= float(points.size())
|
||||
var click := InputEventMouseButton.new()
|
||||
var drawing_area: Control = head.get("drawing_area") as Control
|
||||
var head_zoom: float = float(head.get("_zoom"))
|
||||
var head_pan: Vector2 = head.get("_pan_offset") as Vector2
|
||||
click.global_position = drawing_area.global_position + head_pan + center * head_zoom
|
||||
head.call("_handle_right_click", click, center)
|
||||
await process_frame
|
||||
var part_menu: EditorContextMenu = head.get("context_menu") as EditorContextMenu
|
||||
check("Stickman body-part panels open the shared Studio List menu", part_menu.visible)
|
||||
check("Stickman shape menu uses the shared shape wording", part_menu.get_row_button(9) != null and part_menu.get_row_button(9).text == "Copy Shape" and part_menu.get_row_button(5).text == "Erase Shape")
|
||||
check("Stickman Erase Shape is coral", part_menu.get_row_button(5) != null and part_menu.get_row_button(5).get_theme_color("font_color").is_equal_approx((editor.get("_editor_visual_theme") as EditorVisualTheme).chrome_color(&"destructive", &"stickman")))
|
||||
var part_target: Rect2 = head.call("_global_shape_target_rect", shape) as Rect2
|
||||
check("Stickman part menu leaves the clicked shape visible", not Rect2(Vector2(part_menu.position), Vector2(part_menu.size)).intersects(part_target))
|
||||
check("Stickman part menu avoids an unnecessary scrollbar", not (part_menu.get_node("MenuShell/MenuScroll") as ScrollContainer).get_v_scroll_bar().visible)
|
||||
await _save_capture("context_menu_stickman_part.png")
|
||||
part_menu.hide()
|
||||
|
||||
var preview: WholeStickmanPreview = editor.find_child("WholeStickmanPreview", true, false) as WholeStickmanPreview
|
||||
var preview_area: Control = preview.get("preview_area") as Control
|
||||
await process_frame
|
||||
var part_bounds: Dictionary = preview.get("_part_bounds") as Dictionary
|
||||
var head_bounds: Rect2 = part_bounds.get("head", Rect2()) as Rect2
|
||||
var preview_zoom: float = float(preview.get("_zoom"))
|
||||
var preview_pan: Vector2 = preview.get("_pan_offset") as Vector2
|
||||
var preview_click := InputEventMouseButton.new()
|
||||
preview_click.position = head_bounds.get_center() * preview_zoom + preview_pan
|
||||
preview_click.global_position = preview_area.global_position + preview_click.position
|
||||
preview.call("_handle_right_click", preview_click)
|
||||
await process_frame
|
||||
var preview_menu: EditorContextMenu = preview.get("_context_menu") as EditorContextMenu
|
||||
check("Whole Stickman preview opens the shared Studio List menu", preview_menu.visible)
|
||||
check("Whole Stickman uses child-readable flip labels", preview_menu.get_row_button(2) != null and preview_menu.get_row_button(2).text == "Flip Left-Right" and preview_menu.get_row_button(3).text == "Flip Up-Down")
|
||||
var preview_target := Rect2(preview_area.global_position + preview_pan + head_bounds.position * preview_zoom, head_bounds.size * preview_zoom).grow(4.0)
|
||||
check("Whole Stickman menu leaves the clicked part visible", not Rect2(Vector2(preview_menu.position), Vector2(preview_menu.size)).intersects(preview_target))
|
||||
await _save_capture("context_menu_stickman_preview.png")
|
||||
preview_menu.hide()
|
||||
main_scene.queue_free()
|
||||
await process_frame
|
||||
|
||||
|
||||
func _save_capture(file_name: String) -> void:
|
||||
if DisplayServer.get_name() == "headless":
|
||||
print("SKIP visual capture under headless renderer: %s" % file_name)
|
||||
return
|
||||
var viewport_texture: ViewportTexture = root.get_texture()
|
||||
if viewport_texture == null:
|
||||
check("visual capture texture exists: %s" % file_name, false)
|
||||
return
|
||||
var image: Image = viewport_texture.get_image()
|
||||
if image == null or image.is_empty():
|
||||
check("visual capture image is available: %s" % file_name, false)
|
||||
return
|
||||
var path: String = ProjectSettings.globalize_path("res://.godot/" + file_name)
|
||||
var error: Error = image.save_png(path)
|
||||
check("visual capture saved: %s" % file_name, error == OK and not image.is_empty())
|
||||
|
||||
|
||||
func _dismiss_fixture_recovery_prompt(editor: Control) -> void:
|
||||
# A real user autosave may be present in user://. Hide the prompt for clean
|
||||
# visual captures without choosing Restore or Discard or touching that file.
|
||||
for child: Node in editor.get_children():
|
||||
if child is ConfirmationDialog and (child as ConfirmationDialog).title == "Restore Prop?":
|
||||
STAGE_DIALOG_STYLER.hide(child as ConfirmationDialog)
|
||||
|
||||
|
||||
func _on_menu_item_pressed(id: int) -> void:
|
||||
_last_pressed_id = id
|
||||
|
||||
|
||||
func check(label: String, condition: bool) -> void:
|
||||
_checks += 1
|
||||
if condition:
|
||||
print("PASS: %s" % label)
|
||||
else:
|
||||
_failures += 1
|
||||
push_error("FAIL: %s" % label)
|
||||
@@ -0,0 +1 @@
|
||||
uid://crigocq28ory
|
||||
@@ -2,6 +2,7 @@ extends SceneTree
|
||||
|
||||
const STICKMAN_SCENE := preload("res://scenes/main.tscn")
|
||||
const PROP_SCENE := preload("res://scenes/editors/prop/prop_editor.tscn")
|
||||
const STAGE_DIALOG_STYLER := preload("res://scripts/shared/stage_ui/stage_dialog_styler.gd")
|
||||
|
||||
var _checks: int = 0
|
||||
var _failures: int = 0
|
||||
@@ -171,7 +172,7 @@ func _test_stickman_workspace_and_transactions() -> void:
|
||||
check("second shape can be authored", bool(head.call("add_shape", &"line", head.call("drawing_center_world"))))
|
||||
editor.call("_on_file_menu_id_pressed", 2)
|
||||
var unsaved_dialog: ConfirmationDialog = editor.get("_unsaved_dialog") as ConfirmationDialog
|
||||
check("Start Over shows a save/discard/cancel transaction", unsaved_dialog.visible)
|
||||
check("Start Over shows a save/discard/cancel transaction", STAGE_DIALOG_STYLER.is_open(unsaved_dialog))
|
||||
unsaved_dialog.get_cancel_button().emit_signal("pressed")
|
||||
await process_frame
|
||||
check("Cancel leaves dirty content in place", bool(editor.call("has_unsaved_changes")) and (head.get("shapes") as Array).size() == 2)
|
||||
@@ -197,7 +198,50 @@ func _test_prop_reshape_and_chrome() -> void:
|
||||
check("Prop action target meets 44 px minimum", (editor.get("_create_point_button") as Button).custom_minimum_size.y >= 44.0)
|
||||
check("new Prop has an explicit clean save status", str(save_status.text).ends_with("Not saved yet"))
|
||||
check("whole-prop action uses approved discoverable wording", str((editor.get("_whole_prop_resize_button") as Button).text) == "Move, Turn, or Resize Prop")
|
||||
check("material inspector uses child-facing grip language", str((editor.get("_friction_label") as Label).text).begins_with("Surface grip:"))
|
||||
check("material physics tile uses child-facing grip language", str((editor.get("_friction_label") as Label).text) == "Surface grip")
|
||||
var preset_option: PropInspectorDropdown = editor.get("_preset_option") as PropInspectorDropdown
|
||||
check("material choices show colored swatches in field and popup", preset_option.item_count == 5 and preset_option.get_item_icon(0) != null and preset_option.get_item_icon(4) != null and preset_option.icon != null)
|
||||
var material_selected_row: StyleBoxFlat = preset_option.get_popup().get_theme_stylebox("hover") as StyleBoxFlat
|
||||
var material_items_plain: bool = true
|
||||
for index: int in preset_option.item_count:
|
||||
material_items_plain = material_items_plain and not preset_option.get_popup().is_item_checkable(index)
|
||||
check("material popup uses the full-row selection highlight without radio items", material_items_plain and material_selected_row.bg_color.is_equal_approx(Color("#103B4D")) and material_selected_row.border_color.is_equal_approx(Color("#22C6FF")))
|
||||
await process_frame
|
||||
preset_option.show_popup()
|
||||
await process_frame
|
||||
check("opening the material popup focuses its current value and spans the field", preset_option.get_popup().get_focused_item() == preset_option.selected and preset_option.get_popup().size.x >= preset_option.size.x)
|
||||
check("dropdown popup icons reserve a 44 px row target", preset_option.get_popup().get_item_icon(0).get_width() >= 44)
|
||||
preset_option.hide_popup()
|
||||
preset_option.get_popup().emit_signal("id_pressed", 1)
|
||||
check("choosing a material updates the field and editor model", preset_option.selected == 1 and int(editor.get("_material_preset")) == 1)
|
||||
preset_option.get_popup().emit_signal("id_pressed", 0)
|
||||
var primitive_option: PropInspectorDropdown = editor.get("_primitive_option") as PropInspectorDropdown
|
||||
check("2D shape choices show flat icons in field and popup", primitive_option.item_count == 5 and primitive_option.get_item_icon(0) != null and primitive_option.get_item_icon(4) != null and primitive_option.icon != null)
|
||||
var shape_items_plain: bool = true
|
||||
for index: int in primitive_option.item_count:
|
||||
shape_items_plain = shape_items_plain and not primitive_option.get_popup().is_item_checkable(index)
|
||||
check("shape popup does not draw radio selectors", shape_items_plain)
|
||||
primitive_option.show_popup()
|
||||
await process_frame
|
||||
check("shape popup highlights its current 2D choice", primitive_option.get_popup().get_focused_item() == primitive_option.selected)
|
||||
primitive_option.hide_popup()
|
||||
primitive_option.get_popup().emit_signal("id_pressed", 2)
|
||||
check("choosing a 2D shape updates its parameters", primitive_option.selected == 2 and (editor.get("_param_spins") as Dictionary).has("base"))
|
||||
primitive_option.get_popup().emit_signal("id_pressed", 0)
|
||||
var tag_buttons: Dictionary = editor.get("_tag_checks") as Dictionary
|
||||
var nature_button: Button = tag_buttons.get("nature") as Button
|
||||
check("artwork tag chip has a colored icon and a wide touch target", nature_button != null and nature_button.icon != null and nature_button.custom_minimum_size.y >= 48.0)
|
||||
var nature_icon: Texture2D = nature_button.icon
|
||||
nature_button.set_pressed_no_signal(true)
|
||||
nature_button.emit_signal("toggled", true)
|
||||
var selected_tag_style := nature_button.get_theme_stylebox("pressed") as StyleBoxFlat
|
||||
check("tag selection updates its model", (editor.get("_tags") as Array).has("nature"))
|
||||
check("selected tag keeps its original colored icon without a badge", nature_button.icon == nature_icon)
|
||||
check("selected tag style uses a cyan-tinted highlight", selected_tag_style.bg_color.is_equal_approx(Color("#103B4D")))
|
||||
nature_button.set_pressed_no_signal(false)
|
||||
nature_button.emit_signal("toggled", false)
|
||||
check("clearing a tag removes it from the tag model", not (editor.get("_tags") as Array).has("nature"))
|
||||
check("inspector sections use themed cards", editor.get("_material_section") is PanelContainer and editor.get("_add_section") is PanelContainer and editor.get("_tag_section") is PanelContainer)
|
||||
check("anchor layer control uses the approved label", (editor.get("_anchor_z") as SpinBox).get_parent().get_child(0) is Label and str(((editor.get("_anchor_z") as SpinBox).get_parent().get_child(0) as Label).text) == "Held prop layer:")
|
||||
|
||||
canvas.call("add_primitive", "box", Vector2(150.0, 150.0))
|
||||
|
||||
@@ -15,6 +15,9 @@ func _run() -> void:
|
||||
check("committed shared theme loads", defaults.load_from(EDITOR_VISUAL_THEME.DEFAULT_PATH))
|
||||
check("default body font is 18", defaults.chrome_px(&"body", &"prop") == 18)
|
||||
check("default touch target is 44 px", defaults.chrome_px(&"button_min_height", &"stickman") == 44)
|
||||
check("selected surface has a shared semantic token", defaults.chrome_color(&"selected_surface", &"prop").is_equal_approx(Color("#103B4D")))
|
||||
check("Studio List rows are 48 px", defaults.chrome_px(&"context_menu_row_height", &"stickman") == 48)
|
||||
check("Studio List minimum width is 208 px", defaults.chrome_px(&"context_menu_min_width", &"prop") == 208)
|
||||
check("preview role preserves alpha", is_equal_approx(defaults.chrome_color(&"preview").a, 0.4))
|
||||
|
||||
var custom_path: String = "user://editor_visual_theme_test.json"
|
||||
|
||||