1422 lines
63 KiB
Markdown
1422 lines
63 KiB
Markdown
# Editor UI Architecture Refactor Proposal
|
|
|
|
## Executive Summary
|
|
|
|
This document proposes a refactoring of yaze's editor panel system to eliminate naming confusion, centralize lifecycle management, and reduce boilerplate. The key change is **renaming "Card" terminology to "Panel"**—a more precise term used by professional IDEs like VSCode, JetBrains, and Xcode.
|
|
|
|
---
|
|
|
|
## Part 1: Terminology Decision
|
|
|
|
### Why "Panel" Instead of "Card"?
|
|
|
|
| Term | Associations | Precision |
|
|
|:-----|:-------------|:----------|
|
|
| **Card** | Material Design cards, Kanban boards, mobile UI | Vague—cards are typically static content containers |
|
|
| **Panel** | VSCode Side Panel, JetBrains Tool Windows, Xcode Inspectors | Precise—panels are dockable, resizable tool windows |
|
|
| **Pane** | Split views, editor areas | Typically refers to divisions within a window |
|
|
| **Tool Window** | JetBrains, Visual Studio | Verbose, but very precise |
|
|
|
|
**Decision**: Use **"Panel"** as the primary term:
|
|
- `gui::EditorCard` → `gui::PanelWindow`
|
|
- `CardInfo` → `PanelDescriptor`
|
|
- `EditorCardRegistry` → `PanelManager`
|
|
- `*Card` classes → `*Panel` classes
|
|
|
|
---
|
|
|
|
## Part 2: Current Architecture Analysis
|
|
|
|
### 2.1 Key Components (As-Is)
|
|
|
|
| Component | Location | Current Name | Proposed Name |
|
|
|:----------|:---------|:-------------|:--------------|
|
|
| ImGui window wrapper | `editor_layout.h` | `gui::EditorCard` | `gui::PanelWindow` |
|
|
| Metadata struct | `editor_card_registry.h` | `CardInfo` | `PanelDescriptor` |
|
|
| Central registry | `editor_card_registry.h` | `EditorCardRegistry` | `PanelManager` |
|
|
| Sidebar UI | `activity_bar.h` | `ActivityBar` | `ActivityBar` (unchanged) |
|
|
| Layout builder | `layout_manager.h` | `LayoutManager` | `LayoutManager` (unchanged) |
|
|
| Presets | `layout_presets.h` | `LayoutPresets` | `LayoutPresets` (unchanged) |
|
|
|
|
### 2.2 Current Workflow (Dual Registration Problem)
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ CURRENT WORKFLOW │
|
|
├─────────────────────────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ Editor::Initialize() Editor::Update() │
|
|
│ ─────────────────── ───────────────── │
|
|
│ │
|
|
│ 1. Register CardInfo metadata 1. Create gui::EditorCard instance │
|
|
│ with registry (per-frame or static) │
|
|
│ │
|
|
│ card_registry->RegisterCard({ gui::EditorCard my_panel( │
|
|
│ .card_id = MakeCardId("x.foo"), MakeCardTitle("Foo"), │
|
|
│ .display_name = "Foo", ICON_MD_FOO); │
|
|
│ .window_title = " Foo", │
|
|
│ .visibility_flag = &show_foo_, if (my_panel.Begin(&show_foo_)) { │
|
|
│ ... DrawFooContent(); │
|
|
│ }); } │
|
|
│ my_panel.End(); │
|
|
│ │
|
|
│ PROBLEM: Two separate steps, titles can diverge, no centralized drawing │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### 2.3 Identified Issues
|
|
|
|
1. **Naming Collision**: `gui::EditorCard` vs conceptual "editor card" vs `*Card` classes
|
|
2. **Dual Registration**: Editors register metadata AND manually draw—no central control
|
|
3. **Title Mismatch Risk**: `CardInfo.window_title` vs `gui::EditorCard` constructor title
|
|
4. **No Cross-Editor Panel Support**: Panels are tied to their parent editor
|
|
5. **Inconsistent Instantiation**: Static vs per-frame vs unique_ptr member patterns
|
|
|
|
---
|
|
|
|
## Part 3: Complete Editor Inventory
|
|
|
|
### 3.1 Overworld Editor
|
|
|
|
#### Tool Panels (Static)
|
|
| Panel ID | Display Name | Icon | Purpose | Default Visible |
|
|
|:---------|:-------------|:-----|:--------|:----------------|
|
|
| `overworld.canvas` | Overworld Canvas | MAP | Main map editing canvas with toolset | ✅ |
|
|
| `overworld.tile16_selector` | Tile16 Selector | GRID_ON | Tile palette for painting | ✅ |
|
|
| `overworld.tile8_selector` | Tile8 Selector | GRID_3X3 | Low-level 8x8 tile editing | ❌ |
|
|
| `overworld.area_graphics` | Area Graphics | IMAGE | GFX sheet preview for current area | ✅ |
|
|
| `overworld.scratch` | Scratch Workspace | DRAW | Layout planning/clipboard area | ❌ |
|
|
| `overworld.gfx_groups` | GFX Groups | FOLDER | Graphics group configuration | ❌ |
|
|
| `overworld.usage_stats` | Usage Statistics | ANALYTICS | Tile usage analysis across all maps | ✅ |
|
|
| `overworld.v3_settings` | v3 Settings | SETTINGS | ZSCustomOverworld configuration | ❌ |
|
|
| `overworld.properties` | Map Properties | TUNE | Per-map settings (palette, GFX, etc.) | ✅ |
|
|
| `overworld.debug` | Debug Window | BUG_REPORT | Internal debug information | ❌ |
|
|
|
|
#### Resource Panels (Dynamic per-map)
|
|
| Panel ID Pattern | Display Name | Purpose |
|
|
|:-----------------|:-------------|:--------|
|
|
| `overworld.map_{id}` | Map {id} ({world}) | Focused view of specific overworld map |
|
|
| `overworld.map_{id}.entities` | Map {id} Entities | Entity list (entrances, exits, items, sprites) |
|
|
|
|
**Note**: `overworld.usage_stats` is useful cross-editor—see Section 5.
|
|
|
|
### 3.2 Dungeon Editor (V2)
|
|
|
|
#### Tool Panels (Static)
|
|
| Panel ID | Display Name | Icon | Purpose | Default Visible |
|
|
|:---------|:-------------|:-----|:--------|:----------------|
|
|
| `dungeon.control_panel` | Dungeon Controls | CASTLE | Mode and tool selection | ✅ |
|
|
| `dungeon.room_selector` | Room Selector | LIST | Room list with search/filter (296 rooms) | ✅ |
|
|
| `dungeon.entrance_list` | Entrance List | DOOR_FRONT | Entrance list with search/filter | ✅ |
|
|
| `dungeon.room_matrix` | Room Matrix | GRID_VIEW | Visual 16x16 room layout | ✅ |
|
|
| `dungeon.entrances` | Entrance Properties | DOOR_SLIDING | Selected entrance property editor | ❌ |
|
|
| `dungeon.room_graphics` | Room Graphics | IMAGE | Room GFX sheet preview | ✅ |
|
|
| `dungeon.object_editor` | Object Editor | CONSTRUCTION | Object placement/editing | ✅ |
|
|
| `dungeon.palette_editor` | Palette Editor | PALETTE | Room palette selection | ❌ |
|
|
| `dungeon.debug_controls` | Debug Controls | BUG_REPORT | Debug tools and state inspection | ❌ |
|
|
| `dungeon.emulator_preview` | SNES Object Preview | MONITOR | Live emulator object preview | ❌ |
|
|
|
|
#### Resource Panels (Dynamic per-room)
|
|
| Panel ID Pattern | Display Name | Purpose |
|
|
|:-----------------|:-------------|:--------|
|
|
| `dungeon.room_{id}` | Room {id} | Canvas for editing specific room (0-295) |
|
|
| `dungeon.room_{id}.objects` | Room {id} Objects | Object list for specific room |
|
|
|
|
**Architecture**: Dungeon Editor is pure panel-based—no "main canvas" concept. All panels are peers. **Resource panels** (room-specific) are created on-demand when a room is opened.
|
|
|
|
### 3.3 Graphics Editor
|
|
|
|
#### Tool Panels (Static)
|
|
| Panel ID | Display Name | Icon | Purpose | Default Visible |
|
|
|:---------|:-------------|:-----|:--------|:----------------|
|
|
| `graphics.sheet_browser_v2` | Sheet Browser | VIEW_LIST | Navigate all 223 graphics sheets | ✅ |
|
|
| `graphics.pixel_editor` | Pixel Editor | DRAW | 8x8/16x16 tile pixel editing | ✅ |
|
|
| `graphics.palette_controls` | Palette Controls | PALETTE | Palette selection for editing | ✅ |
|
|
| `graphics.link_sprite_editor` | Link Sprite Editor | PERSON | Edit Link's sprite frames | ❌ |
|
|
| `graphics.polyhedral_editor` | 3D Objects | VIEW_IN_AR | Edit rupees, crystals, triforce | ✅ |
|
|
| `graphics.sheet_editor` | Sheet Editor (Legacy) | EDIT | Older sheet editing interface | ❌ |
|
|
| `graphics.sheet_browser` | Asset Browser (Legacy) | VIEW_LIST | Older asset browsing | ❌ |
|
|
| `graphics.player_animations` | Player Animations | PERSON | View Link animation sequences | ❌ |
|
|
| `graphics.prototype_viewer` | Prototype Viewer | CONSTRUCTION | Experimental feature viewer | ❌ |
|
|
|
|
#### Resource Panels (Dynamic per-sheet)
|
|
| Panel ID Pattern | Display Name | Purpose |
|
|
|:-----------------|:-------------|:--------|
|
|
| `graphics.sheet_{id}` | Sheet {id} | Dedicated editor for specific GFX sheet |
|
|
|
|
### 3.4 Palette Editor
|
|
|
|
TODO: Remove control panel and fold into activity bar for useful actions? Resource management
|
|
|
|
| Panel ID | Display Name | Icon | Purpose | Default Visible |
|
|
|:---------|:-------------|:-----|:--------|:----------------|
|
|
| `palette.control_panel` | Palette Controls | PALETTE | Group manager and quick actions | ✅ |
|
|
| `palette.ow_main` | Overworld Main | LANDSCAPE | 6 overworld area palettes | ✅ |
|
|
| `palette.ow_animated` | Overworld Animated | WATER | Water, lava animation palettes | ❌ |
|
|
| `palette.dungeon_main` | Dungeon Main | CASTLE | 20 dungeon room palettes | ✅ |
|
|
| `palette.sprites` | Global Sprite Palettes | PETS | Main sprite color sets | ✅ |
|
|
| `palette.sprites_aux1` | Sprites Aux 1 | FILTER_1 | Auxiliary sprite colors 1 | ❌ |
|
|
| `palette.sprites_aux2` | Sprites Aux 2 | FILTER_2 | Auxiliary sprite colors 2 | ❌ |
|
|
| `palette.sprites_aux3` | Sprites Aux 3 | FILTER_3 | Auxiliary sprite colors 3 | ❌ |
|
|
| `palette.equipment` | Equipment Palettes | SHIELD | Link's tunic/equipment colors | ❌ |
|
|
| `palette.quick_access` | Quick Access | COLOR_LENS | Color harmony tools | ✅ |
|
|
| `palette.custom` | Custom Palette | BRUSH | Custom palette editing | ❌ |
|
|
|
|
### 3.5 Music Editor
|
|
|
|
#### Tool Panels (Static)
|
|
| Panel ID | Display Name | Icon | Purpose | Default Visible |
|
|
|:---------|:-------------|:-----|:--------|:----------------|
|
|
| `music.song_browser` | Song Browser | LIBRARY_MUSIC | Navigate all game songs | ✅ |
|
|
| `music.tracker` | Playback Control | PLAY_CIRCLE | Transport controls and BPM | ✅ |
|
|
| `music.piano_roll` | Piano Roll | PIANO | Visual note editing | ❌ |
|
|
| `music.instrument_editor` | Instrument Editor | SPEAKER | Edit instrument samples | ✅ |
|
|
| `music.sample_editor` | Sample Editor | WAVES | Edit BRR audio samples | ❌ |
|
|
| `music.assembly` | Assembly View | CODE | View music as 65816 assembly | ❌ |
|
|
| `music.help` | Help | HELP | Music editor documentation | ❌ |
|
|
|
|
#### Resource Panels (Dynamic per-song)
|
|
| Panel ID Pattern | Display Name | Purpose |
|
|
|:-----------------|:-------------|:--------|
|
|
| `music.song_{index}` | Song: {name} | Full song editor for a specific track |
|
|
| `music.song_{index}.piano_roll` | {name} Piano Roll | Piano roll for specific song |
|
|
| `music.song_{index}.channels` | {name} Channels | Channel mixer for specific song |
|
|
|
|
**Dynamic Panels**: Music Editor creates per-song panels on demand. Multiple songs can be open simultaneously for comparison/copying.
|
|
|
|
### 3.6 Screen Editor
|
|
|
|
| Panel ID | Display Name | Icon | Purpose | Default Visible |
|
|
|:---------|:-------------|:-----|:--------|:----------------|
|
|
| `screen.dungeon_maps` | Dungeon Maps | MAP | Edit dungeon map screens | ✅ |
|
|
| `screen.inventory_menu` | Inventory Menu | INVENTORY | Edit inventory/pause menu | ✅ |
|
|
| `screen.overworld_map` | Overworld Map | PUBLIC | Edit overworld map screen | ❌ |
|
|
| `screen.title_screen` | Title Screen | TITLE | Edit title screen graphics | ✅ |
|
|
| `screen.naming_screen` | Naming Screen | EDIT | Edit save file naming screen | ❌ |
|
|
|
|
### 3.7 Sprite Editor
|
|
|
|
| Panel ID | Display Name | Icon | Purpose | Default Visible |
|
|
|:---------|:-------------|:-----|:--------|:----------------|
|
|
| `sprite.vanilla_editor` | Vanilla Sprites | SMART_TOY | View/edit built-in sprites | ✅ |
|
|
| `sprite.custom_editor` | Custom Sprites | ADD_CIRCLE | Import/edit custom ZSM sprites | ❌ |
|
|
|
|
### 3.8 Message Editor
|
|
|
|
| Panel ID | Display Name | Icon | Purpose | Default Visible |
|
|
|:---------|:-------------|:-----|:--------|:----------------|
|
|
| `message.message_list` | Message List | LIST | Navigate all game messages | ✅ |
|
|
| `message.message_editor` | Message Editor | EDIT | Edit message text and formatting | ✅ |
|
|
| `message.font_atlas` | Font Atlas | FONT_DOWNLOAD | View/edit font graphics | ✅ |
|
|
| `message.dictionary` | Dictionary | BOOK | Edit compression dictionary | ❌ |
|
|
|
|
### 3.9 Assembly Editor
|
|
|
|
| Panel ID | Display Name | Icon | Purpose | Default Visible |
|
|
|:---------|:-------------|:-----|:--------|:----------------|
|
|
| `assembly.editor` | Assembly Editor | CODE | 65816 assembly code editor | ✅ |
|
|
| `assembly.file_browser` | File Browser | FOLDER_OPEN | Navigate ASM project files | ✅ |
|
|
|
|
**Note**: Assembly Editor uses file browser integration for project navigation.
|
|
|
|
### 3.10 Agent Editor
|
|
|
|
| Panel ID | Display Name | Icon | Purpose | Default Visible |
|
|
|:---------|:-------------|:-----|:--------|:----------------|
|
|
| `agent.chat` | Agent Chat | CHAT | Main AI chat interface | ✅ |
|
|
| `agent.configuration` | AI Configuration | SETTINGS | API keys, model selection | ❌ |
|
|
| `agent.status` | Agent Status | INFO | Connection status, context info | ❌ |
|
|
| `agent.prompt_editor` | Prompt Editor | EDIT | Edit system prompts | ❌ |
|
|
| `agent.profiles` | Bot Profiles | FOLDER | Manage bot personalities | ❌ |
|
|
| `agent.history` | Chat History | HISTORY | View past conversations | ❌ |
|
|
| `agent.metrics` | Metrics Dashboard | ANALYTICS | Token usage, response times | ❌ |
|
|
| `agent.builder` | Agent Builder | AUTO_FIX_HIGH | Create custom agents | ❌ |
|
|
|
|
### 3.11 Emulator (Registered by EditorManager)
|
|
|
|
TODO: Consolidate some panels into PpuViewer nav as a canvas of sorts
|
|
|
|
| Panel ID | Display Name | Icon | Purpose | Default Visible |
|
|
|:---------|:-------------|:-----|:--------|:----------------|
|
|
| `emulator.cpu_debugger` | CPU Debugger | BUG_REPORT | CPU registers, stepping | ✅ |
|
|
| `emulator.ppu_viewer` | PPU Viewer | VIDEOGAME_ASSET | Graphics layer debugging | ✅ |
|
|
| `emulator.memory_viewer` | Memory Viewer | MEMORY | RAM/VRAM inspection | ❌ |
|
|
| `emulator.breakpoints` | Breakpoints | STOP | Breakpoint management | ❌ |
|
|
| `emulator.performance` | Performance | SPEED | Frame timing, FPS | ✅ |
|
|
| `emulator.ai_agent` | AI Agent | SMART_TOY | AI-assisted debugging | ❌ |
|
|
| `emulator.save_states` | Save States | SAVE | Save/load state management | ✅ |
|
|
| `emulator.keyboard_config` | Keyboard Config | KEYBOARD | Input configuration | ✅ |
|
|
| `emulator.virtual_controller` | Virtual Controller | SPORTS_ESPORTS | On-screen gamepad | ✅ |
|
|
| `emulator.apu_debugger` | APU Debugger | AUDIOTRACK | Audio processor debugging | ❌ |
|
|
| `emulator.audio_mixer` | Audio Mixer | AUDIO_FILE | Channel volume control | ❌ |
|
|
|
|
### 3.12 Memory (Registered by EditorManager)
|
|
|
|
TODO: Add more panels to help with hex editing operations?
|
|
|
|
| Panel ID | Display Name | Icon | Purpose | Default Visible |
|
|
|:---------|:-------------|:-----|:--------|:----------------|
|
|
| `memory.hex_editor` | Hex Editor | MEMORY | Raw ROM hex editing | ✅ |
|
|
|
|
---
|
|
|
|
## Part 4: Cross-Editor Panel Visibility
|
|
|
|
### 4.1 The Problem
|
|
|
|
Currently, panels are tightly coupled to their parent editor. When switching editors, panels are hidden. But some panels are useful across editors:
|
|
|
|
| Panel | Useful When... |
|
|
|:------|:---------------|
|
|
| `overworld.usage_stats` | Editing dungeons to check tile availability |
|
|
| `palette.ow_main` | Editing overworld tiles to preview palette effects |
|
|
| `emulator.cpu_debugger` | Any editing to test changes in real-time |
|
|
| `graphics.pixel_editor` | Editing sprites, dungeons, or messages |
|
|
|
|
### 4.2 Panel Categories
|
|
|
|
We propose three panel categories based on lifecycle behavior:
|
|
|
|
| Category | Behavior | Examples |
|
|
|:---------|:---------|:---------|
|
|
| **Editor-Bound** | Hidden when switching away from parent editor | `dungeon.room_selector`, `music.piano_roll` |
|
|
| **Persistent** | Remains visible across editor switches | `emulator.cpu_debugger`, `palette.quick_access` |
|
|
| **Cross-Editor** | Can be pinned to stay visible (user choice) | `overworld.usage_stats`, `graphics.pixel_editor` |
|
|
|
|
### 4.3 Implementation: Pin-to-Persist
|
|
|
|
Add a "pin" button to panel headers:
|
|
|
|
```cpp
|
|
class PanelWindow {
|
|
public:
|
|
// ... existing methods ...
|
|
|
|
/// If pinned, panel stays visible when switching editors
|
|
void SetPinned(bool pinned) { pinned_ = pinned; }
|
|
bool IsPinned() const { return pinned_; }
|
|
|
|
private:
|
|
bool pinned_ = false;
|
|
};
|
|
```
|
|
|
|
The `PanelManager` respects pinned state:
|
|
|
|
```cpp
|
|
void PanelManager::OnEditorSwitch(EditorType from, EditorType to) {
|
|
// Hide non-pinned panels from the previous editor
|
|
for (auto& [id, panel] : panels_) {
|
|
if (panel->GetCategory() == GetCategoryForEditor(from) && !IsPinned(id)) {
|
|
HidePanel(id);
|
|
}
|
|
}
|
|
|
|
// Show default panels for new editor
|
|
auto defaults = LayoutPresets::GetDefaultPanels(to);
|
|
for (const auto& id : defaults) {
|
|
ShowPanel(id);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 4.4 Related Panel Cascade (Optional)
|
|
|
|
For **Editor-Bound** panels only, we can define parent-child relationships:
|
|
|
|
```cpp
|
|
struct PanelDescriptor {
|
|
// ... existing fields ...
|
|
|
|
/// If set, this panel closes when parent panel closes
|
|
std::string parent_panel_id;
|
|
|
|
/// If true, closing this panel also closes children
|
|
bool cascade_close = false;
|
|
};
|
|
```
|
|
|
|
**Example**: Closing `dungeon.control_panel` could cascade-close `dungeon.object_editor` if they're defined as related.
|
|
|
|
**Documentation Requirement**: Any cascade behavior MUST be documented in `LayoutPresets` comments.
|
|
|
|
---
|
|
|
|
## Part 5: Resource Panels & Multi-Session Support
|
|
|
|
### 5.1 The Resource Panel Concept
|
|
|
|
Some panels represent specific **resources** within the ROM—not generic tools, but windows for editing a particular piece of data:
|
|
|
|
| Editor | Resource Type | Example Panel IDs |
|
|
|:-------|:--------------|:------------------|
|
|
| Dungeon | Rooms (0-295) | `dungeon.room_42`, `dungeon.room_128` |
|
|
| Music | Songs (0-63) | `music.song_5`, `music.song_12` |
|
|
| Overworld | Maps (0-159) | `overworld.map_0`, `overworld.map_64` |
|
|
| Graphics | Sheets (0-222) | `graphics.sheet_100`, `graphics.sheet_212` |
|
|
| Sprite | Sprites (0-255) | `sprite.vanilla_42`, `sprite.custom_3` |
|
|
| Message | Messages (0-395) | `message.text_42`, `message.text_100` |
|
|
|
|
### 5.2 Resource Panel Lifecycle
|
|
|
|
```cpp
|
|
/// Base class for resource-specific panels
|
|
class ResourcePanel : public EditorPanel {
|
|
public:
|
|
/// The resource ID this panel edits (room_id, song_index, etc.)
|
|
virtual int GetResourceId() const = 0;
|
|
|
|
/// Resource type for grouping
|
|
virtual std::string GetResourceType() const = 0;
|
|
|
|
/// Can have multiple instances open simultaneously
|
|
virtual bool AllowMultipleInstances() const { return true; }
|
|
|
|
// Resource panels are always EditorBound by default
|
|
PanelCategory GetPanelCategory() const override {
|
|
return PanelCategory::EditorBound;
|
|
}
|
|
};
|
|
```
|
|
|
|
### 5.3 Resource Panel ID Format
|
|
|
|
```
|
|
{session}.{category}.{resource_type}_{resource_id}[.{subpanel}]
|
|
|
|
Examples:
|
|
s0.dungeon.room_42 -- Room 42 in session 0
|
|
s1.dungeon.room_42 -- Room 42 in session 1 (different ROM)
|
|
s0.music.song_5 -- Song 5 in session 0
|
|
s0.music.song_5.piano_roll -- Piano roll for song 5
|
|
s0.overworld.map_64.entities -- Entity list for map 64
|
|
```
|
|
|
|
### 5.4 Multi-Session Awareness
|
|
|
|
When multiple ROMs are loaded (multi-session editing), resource panels must be uniquely identified:
|
|
|
|
```cpp
|
|
class PanelManager {
|
|
public:
|
|
/// Create a resource panel for the current session
|
|
std::string CreateResourcePanel(const std::string& category,
|
|
const std::string& resource_type,
|
|
int resource_id) {
|
|
std::string panel_id = MakeResourcePanelId(
|
|
active_session_, category, resource_type, resource_id);
|
|
|
|
// Check if already exists
|
|
if (panels_.contains(panel_id)) {
|
|
ShowPanel(panel_id); // Just bring to front
|
|
return panel_id;
|
|
}
|
|
|
|
// Create new resource panel
|
|
auto panel = CreateResourcePanelImpl(category, resource_type, resource_id);
|
|
RegisterPanel(std::move(panel));
|
|
ShowPanel(panel_id);
|
|
|
|
return panel_id;
|
|
}
|
|
|
|
/// Generate session-aware resource panel ID
|
|
std::string MakeResourcePanelId(size_t session_id,
|
|
const std::string& category,
|
|
const std::string& resource_type,
|
|
int resource_id) const {
|
|
if (session_count_ > 1) {
|
|
return absl::StrFormat("s%zu.%s.%s_%d",
|
|
session_id, category, resource_type, resource_id);
|
|
}
|
|
return absl::StrFormat("%s.%s_%d", category, resource_type, resource_id);
|
|
}
|
|
|
|
/// Get all resource panels for a session
|
|
std::vector<EditorPanel*> GetResourcePanelsInSession(size_t session_id);
|
|
|
|
/// Close all resource panels when a session closes
|
|
void CloseSessionResourcePanels(size_t session_id);
|
|
};
|
|
```
|
|
|
|
### 5.5 Multi-ROM Side-by-Side Editing
|
|
|
|
With session-aware IDs, users can:
|
|
|
|
1. **Open two ROMs** (vanilla + hack)
|
|
2. **View same room side-by-side**:
|
|
- `s0.dungeon.room_42` (vanilla)
|
|
- `s1.dungeon.room_42` (hack)
|
|
3. **Compare and copy** between them
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ MULTI-SESSION RESOURCE PANELS │
|
|
├─────────────────────────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ Session 0 (vanilla.sfc) Session 1 (myhack.sfc) │
|
|
│ ┌─────────────────────┐ ┌─────────────────────┐ │
|
|
│ │ s0.dungeon.room_42 │ │ s1.dungeon.room_42 │ │
|
|
│ │ Room 42 │ │ Room 42 │ │
|
|
│ │ [Session 0] │ ◄─────► │ [Session 1] │ │
|
|
│ │ │ Compare │ │ │
|
|
│ │ ┌───────────────┐ │ │ ┌───────────────┐ │ │
|
|
│ │ │ Canvas │ │ │ │ Canvas │ │ │
|
|
│ │ │ (vanilla) │ │ │ │ (modified) │ │ │
|
|
│ │ └───────────────┘ │ │ └───────────────┘ │ │
|
|
│ └─────────────────────┘ └─────────────────────┘ │
|
|
│ │
|
|
│ Window Title Format: "Room 42 [S0]" "Room 42 [S1]" │
|
|
│ │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### 5.6 Resource Panel Window Titles
|
|
|
|
```cpp
|
|
std::string PanelManager::GetResourcePanelTitle(const std::string& panel_id) const {
|
|
auto* panel = GetPanel(panel_id);
|
|
if (!panel) return "";
|
|
|
|
auto* resource_panel = dynamic_cast<ResourcePanel*>(panel);
|
|
if (!resource_panel) {
|
|
return GetWindowTitle(panel_id); // Fallback to normal
|
|
}
|
|
|
|
std::string title = absl::StrFormat("%s %s %d",
|
|
resource_panel->GetIcon(),
|
|
resource_panel->GetResourceType(),
|
|
resource_panel->GetResourceId());
|
|
|
|
// Add session suffix for multi-session
|
|
if (session_count_ > 1) {
|
|
size_t session_id = GetSessionFromPanelId(panel_id);
|
|
title += absl::StrFormat(" [S%zu]", session_id);
|
|
}
|
|
|
|
return title;
|
|
}
|
|
```
|
|
|
|
### 5.7 Resource Panel Limits
|
|
|
|
To prevent memory bloat, enforce limits:
|
|
|
|
```cpp
|
|
struct ResourcePanelLimits {
|
|
static constexpr size_t kMaxRoomPanels = 8; // Max open rooms
|
|
static constexpr size_t kMaxSongPanels = 4; // Max open songs
|
|
static constexpr size_t kMaxSheetPanels = 6; // Max open GFX sheets
|
|
static constexpr size_t kMaxTotalResourcePanels = 20;
|
|
};
|
|
```
|
|
|
|
When limit is reached:
|
|
1. **Option A**: Auto-close oldest panel (LRU)
|
|
2. **Option B**: Warn user and prevent new panel
|
|
3. **Option C**: Prompt to close one (user choice)
|
|
|
|
**Recommendation**: Option A with undo (can reopen from sidebar).
|
|
|
|
---
|
|
|
|
## Part 6: Proposed Architecture
|
|
|
|
### 6.1 New Terminology Mapping
|
|
|
|
| Current | Proposed | Description |
|
|
|:--------|:---------|:------------|
|
|
| `gui::EditorCard` | `gui::PanelWindow` | Pure ImGui window wrapper |
|
|
| `CardInfo` | `PanelDescriptor` | Panel metadata (immutable after registration) |
|
|
| `EditorCardRegistry` | `PanelManager` | Owns panels, manages lifecycle, draws all |
|
|
| `*Card` classes | `*Panel` classes | Logical component implementations |
|
|
|
|
### 6.2 Interface Definitions
|
|
|
|
#### `gui::PanelWindow`
|
|
|
|
```cpp
|
|
namespace yaze::gui {
|
|
|
|
/// Pure ImGui window wrapper for dockable panels
|
|
class PanelWindow {
|
|
public:
|
|
enum class Position { Free, Left, Right, Bottom, Top, Floating };
|
|
|
|
PanelWindow(const char* title, const char* icon = nullptr);
|
|
|
|
// Configuration
|
|
void SetDefaultSize(float width, float height);
|
|
void SetPosition(Position pos);
|
|
void SetMinimizable(bool minimizable);
|
|
void SetClosable(bool closable);
|
|
void SetDockingAllowed(bool allowed);
|
|
void SetPinnable(bool pinnable); // NEW: Allow pin-to-persist
|
|
|
|
// Header buttons (drawn in title bar)
|
|
void AddHeaderButton(const char* icon, const char* tooltip,
|
|
std::function<void()> callback);
|
|
|
|
// ImGui lifecycle
|
|
bool Begin(bool* p_open = nullptr);
|
|
void End();
|
|
|
|
// State
|
|
void SetPinned(bool pinned);
|
|
bool IsPinned() const;
|
|
void Focus();
|
|
bool IsFocused() const;
|
|
const char* GetWindowName() const;
|
|
};
|
|
|
|
} // namespace yaze::gui
|
|
```
|
|
|
|
#### `editor::EditorPanel` (New Interface)
|
|
|
|
```cpp
|
|
namespace yaze::editor {
|
|
|
|
/// Category for panel lifecycle behavior
|
|
enum class PanelCategory {
|
|
EditorBound, // Hidden on editor switch (default)
|
|
Persistent, // Always visible once shown
|
|
CrossEditor // User can pin to persist
|
|
};
|
|
|
|
/// Base interface for all logical panel components
|
|
class EditorPanel {
|
|
public:
|
|
virtual ~EditorPanel() = default;
|
|
|
|
// ========== Identity ==========
|
|
virtual std::string GetId() const = 0; // e.g., "dungeon.room_selector"
|
|
virtual std::string GetDisplayName() const = 0; // e.g., "Room Selector"
|
|
virtual std::string GetIcon() const = 0; // e.g., ICON_MD_LIST
|
|
virtual std::string GetEditorCategory() const = 0; // e.g., "Dungeon"
|
|
|
|
// ========== Drawing ==========
|
|
virtual void Draw(bool* p_open) = 0; // Called when visible
|
|
|
|
// ========== Lifecycle ==========
|
|
virtual void OnOpen() {} // Panel becomes visible
|
|
virtual void OnClose() {} // Panel becomes hidden
|
|
virtual void OnFocus() {} // Panel receives focus
|
|
|
|
// ========== Behavior ==========
|
|
virtual PanelCategory GetPanelCategory() const {
|
|
return PanelCategory::EditorBound;
|
|
}
|
|
virtual bool IsEnabled() const { return true; }
|
|
virtual std::string GetDisabledTooltip() const { return ""; }
|
|
virtual std::string GetShortcutHint() const { return ""; }
|
|
virtual int GetPriority() const { return 50; }
|
|
|
|
// ========== Relationships ==========
|
|
virtual std::string GetParentPanelId() const { return ""; }
|
|
virtual bool CascadeCloseChildren() const { return false; }
|
|
};
|
|
|
|
} // namespace yaze::editor
|
|
```
|
|
|
|
#### `editor::PanelManager`
|
|
|
|
```cpp
|
|
namespace yaze::editor {
|
|
|
|
/// Central manager for all EditorPanel instances
|
|
class PanelManager {
|
|
public:
|
|
// ========== Registration ==========
|
|
void RegisterPanel(std::unique_ptr<EditorPanel> panel);
|
|
|
|
template <typename T, typename... Args>
|
|
T* EmplacePanel(Args&&... args);
|
|
|
|
void UnregisterPanel(const std::string& panel_id);
|
|
|
|
// ========== Visibility ==========
|
|
void ShowPanel(const std::string& panel_id);
|
|
void HidePanel(const std::string& panel_id);
|
|
void TogglePanel(const std::string& panel_id);
|
|
bool IsPanelVisible(const std::string& panel_id) const;
|
|
|
|
void ShowAllInCategory(const std::string& category);
|
|
void HideAllInCategory(const std::string& category);
|
|
|
|
// ========== Central Drawing ==========
|
|
void DrawAllVisiblePanels(); // Call once per frame
|
|
|
|
// ========== Editor Switching ==========
|
|
void OnEditorSwitch(EditorType from, EditorType to);
|
|
void SetActiveEditor(EditorType type);
|
|
|
|
// ========== Pin Management ==========
|
|
void SetPanelPinned(const std::string& panel_id, bool pinned);
|
|
bool IsPanelPinned(const std::string& panel_id) const;
|
|
std::vector<std::string> GetPinnedPanels() const;
|
|
|
|
// ========== Query ==========
|
|
EditorPanel* GetPanel(const std::string& panel_id);
|
|
std::vector<EditorPanel*> GetPanelsInCategory(const std::string& category);
|
|
std::vector<std::string> GetAllCategories() const;
|
|
|
|
// ========== Session Support ==========
|
|
void SetActiveSession(size_t session_id);
|
|
size_t GetActiveSession() const;
|
|
|
|
// ========== Persistence ==========
|
|
void SaveVisibilityState();
|
|
void LoadVisibilityState();
|
|
void SavePinnedState();
|
|
void LoadPinnedState();
|
|
|
|
private:
|
|
std::unordered_map<std::string, std::unique_ptr<EditorPanel>> panels_;
|
|
std::unordered_map<std::string, bool> visibility_;
|
|
std::unordered_map<std::string, bool> pinned_;
|
|
EditorType active_editor_ = EditorType::kUnknown;
|
|
size_t active_session_ = 0;
|
|
};
|
|
|
|
} // namespace yaze::editor
|
|
```
|
|
|
|
### 6.3 Architecture Diagram
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ PROPOSED ARCHITECTURE │
|
|
├─────────────────────────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
|
|
│ │ PanelManager │ │
|
|
│ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │
|
|
│ │ │ panels_: map<string, unique_ptr<EditorPanel>> │ │ │
|
|
│ │ │ │ │ │
|
|
│ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │
|
|
│ │ │ │ RoomList │ │ ObjectEditor │ │ UsageStats │ ... │ │ │
|
|
│ │ │ │ Panel │ │ Panel │ │ Panel │ │ │ │
|
|
│ │ │ │ [EditorBound]│ │ [EditorBound]│ │ [CrossEditor]│ │ │ │
|
|
│ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │
|
|
│ │ └─────────────────────────────────────────────────────────────────────┘ │ │
|
|
│ │ │ │
|
|
│ │ DrawAllVisiblePanels() { │ │
|
|
│ │ for (auto& [id, panel] : panels_) { │ │
|
|
│ │ if (!IsVisible(id)) continue; │ │
|
|
│ │ │ │
|
|
│ │ gui::PanelWindow window(GetWindowTitle(id), panel->GetIcon()); │ │
|
|
│ │ if (IsPinnable(id)) window.SetPinnable(true); │ │
|
|
│ │ │ │
|
|
│ │ if (window.Begin(&visibility_[id])) { │ │
|
|
│ │ panel->Draw(&visibility_[id]); │ │
|
|
│ │ } │ │
|
|
│ │ window.End(); │ │
|
|
│ │ } │ │
|
|
│ │ } │ │
|
|
│ └─────────────────────────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ ┌──────────────┐ ┌───────────────┐ ┌───────────────────┐ │
|
|
│ │ ActivityBar │────▶│ PanelManager │◀────│ LayoutManager │ │
|
|
│ │ (Sidebar UI) │ │ (Ownership) │ │ (DockBuilder) │ │
|
|
│ └──────────────┘ └───────────────┘ └───────────────────┘ │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ ┌──────────────┐ │
|
|
│ │LayoutPresets │ │
|
|
│ │(Default vis) │ │
|
|
│ └──────────────┘ │
|
|
│ │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## Part 7: Migration Plan & Current Status
|
|
|
|
### Current Status (December 2024)
|
|
|
|
**✅ PHASES 1-8 COMPLETE** - Full panel system migration complete including resource panels and cross-editor features.
|
|
|
|
| Phase | Status | Description |
|
|
|:------|:-------|:------------|
|
|
| Phase 1 | ✅ Complete | UI widget renamed (`EditorCard` → `PanelWindow`) |
|
|
| Phase 2 | ✅ Complete | Registry renamed (`EditorCardRegistry` deleted, `PanelManager` created) |
|
|
| Phase 3 | ✅ Complete | `EditorPanel` interface defined with central drawing |
|
|
| Phase 4 | ✅ Complete | Dungeon Editor migrated (9 panels) |
|
|
| Phase 5 | ✅ Complete | All remaining editors migrated (40 panels) |
|
|
| Phase 6 | ✅ Complete | Resource panels with LRU logic |
|
|
| Phase 7 | ✅ Complete | Cross-editor features (Pin-to-Persist, OnEditorSwitch) |
|
|
| Phase 8 | ✅ Complete | Multi-session verification and testing |
|
|
|
|
### Phase 5 Completion Details
|
|
|
|
All editors now use the EditorPanel architecture:
|
|
|
|
| Editor | Static Panels | Resource Panels | Status | Pattern Used |
|
|
|:-------|:--------------|:----------------|:-------|:-------------|
|
|
| **Dungeon Editor** | 10 | `dungeon.room_{id}` | ✅ Complete | Callback + Resource |
|
|
| **Graphics Editor** | 5 | — | ✅ Complete | Direct interface |
|
|
| **Music Editor** | 7 | `music.song_{id}`, `music.piano_roll_{id}` | ✅ Complete | Callback + Resource |
|
|
| **Palette Editor** | 11 | — | ✅ Complete | Callback pattern |
|
|
| **Agent Editor** | 8 | — | ✅ Complete | Callback pattern |
|
|
| **Sprite Editor** | 2 | — | ✅ Complete | Callback pattern |
|
|
| **Screen Editor** | 5 | — | ✅ Complete | Callback pattern |
|
|
| **Message Editor** | 4 | — | ✅ Complete | Callback pattern |
|
|
| **Overworld Editor** | 9 | — | ✅ Complete | Direct pointer pattern |
|
|
|
|
**Total: 61 static EditorPanel implementations + dynamic resource panels**
|
|
|
|
### Overworld Editor Migration Details
|
|
|
|
The Overworld Editor migration was the most complex, using a **direct pointer pattern** with separate `.cc` files for maintainability:
|
|
|
|
#### Panels Created (9 total)
|
|
|
|
**High-Priority Panels:**
|
|
| Panel | ID | Purpose | File |
|
|
|:------|:---|:--------|:-----|
|
|
| AreaGraphicsPanel | `overworld.area_graphics` | GFX sheet preview for current area | `area_graphics_panel.h/.cc` |
|
|
| Tile16SelectorPanel | `overworld.tile16_selector` | Tile palette for painting | `tile16_selector_panel.h/.cc` |
|
|
| MapPropertiesPanel | `overworld.properties` | Per-map settings editor | `map_properties_panel.h/.cc` |
|
|
|
|
**Medium-Priority Panels:**
|
|
| Panel | ID | Purpose | File |
|
|
|:------|:---|:--------|:-----|
|
|
| ScratchSpacePanel | `overworld.scratch` | Layout planning workspace | `scratch_space_panel.h/.cc` |
|
|
| UsageStatisticsPanel | `overworld.usage_stats` | Tile usage analysis | `usage_statistics_panel.h/.cc` |
|
|
|
|
**Low-Priority Panels:**
|
|
| Panel | ID | Purpose | File |
|
|
|:------|:---|:--------|:-----|
|
|
| Tile8SelectorPanel | `overworld.tile8_selector` | 8x8 tile editing | `tile8_selector_panel.h/.cc` |
|
|
| DebugWindowPanel | `overworld.debug` | Debug information | `debug_window_panel.h/.cc` |
|
|
| GfxGroupsPanel | `overworld.gfx_groups` | Graphics group configuration | `gfx_groups_panel.h/.cc` |
|
|
| V3SettingsPanel | `overworld.v3_settings` | ZSCustomOverworld settings | `v3_settings_panel.h/.cc` |
|
|
|
|
#### Architecture Pattern
|
|
|
|
```cpp
|
|
// Panel header (area_graphics_panel.h)
|
|
class AreaGraphicsPanel : public EditorPanel {
|
|
public:
|
|
explicit AreaGraphicsPanel(OverworldEditor* editor);
|
|
|
|
std::string GetId() const override { return "overworld.area_graphics"; }
|
|
std::string GetDisplayName() const override { return "Area Graphics"; }
|
|
std::string GetIcon() const override { return ICON_MD_IMAGE; }
|
|
std::string GetEditorCategory() const override { return "Overworld"; }
|
|
void Draw(bool* p_open) override;
|
|
|
|
private:
|
|
OverworldEditor* editor_;
|
|
};
|
|
|
|
// Registration in overworld_editor.cc
|
|
void OverworldEditor::Initialize() {
|
|
panel_manager->RegisterEditorPanel(
|
|
std::make_unique<AreaGraphicsPanel>(this));
|
|
// ... all 9 panels registered once
|
|
}
|
|
```
|
|
|
|
### Git Commits (16 atomic commits)
|
|
|
|
```
|
|
1cd667a93f docs: update architecture documentation for EditorPanel system
|
|
a436321bcc refactor(wasm): update control API for panel system compatibility
|
|
28ca469cb5 fix(emu): improve audio timing and SPC700 cycle accuracy
|
|
604667e47f feat(editor): integrate EditorManager and Activity Bar with panel system
|
|
060857179c refactor(ui): update settings and coordinator systems for panel architecture
|
|
e7c65a1c46 feat(overworld-editor): migrate to EditorPanel system
|
|
7df3980222 feat(message-editor): migrate to EditorPanel system
|
|
a91f08928a feat(screen-editor): migrate to EditorPanel system
|
|
081af10c80 feat(sprite-editor): migrate to EditorPanel system
|
|
36c8bee040 feat(agent-editor): migrate to EditorPanel system
|
|
862a69965d feat(palette-editor): migrate to EditorPanel system
|
|
d4f0dccf10 feat(music-editor): migrate to EditorPanel system
|
|
195cf8ac5e feat(graphics-editor): migrate existing panels to EditorPanel interface
|
|
a5daccce8e feat(dungeon-editor): migrate to EditorPanel system
|
|
e9e1123a57 refactor(layout): update layout system to use PanelManager
|
|
b652636618 refactor(editor): remove deprecated EditorCardRegistry
|
|
6007a1086d feat(editor): add EditorPanel system for unified panel management
|
|
```
|
|
|
|
### Benefits Achieved
|
|
|
|
- ✅ **Centralized Management** - All panels managed by `PanelManager`
|
|
- ✅ **Activity Bar Integration** - Panels automatically appear in sidebar
|
|
- ✅ **Efficient Rendering** - Panels created once in `Initialize()`, not per-frame
|
|
- ✅ **Layout Persistence** - Panel arrangements can be saved/restored
|
|
- ✅ **Session Support** - Proper scoping for multi-ROM editing
|
|
- ✅ **Consistent Architecture** - All editors follow same pattern
|
|
|
|
---
|
|
|
|
### Phase 6-8 Completion Details
|
|
|
|
### Phase 6: Resource Panels ✅ Complete
|
|
|
|
**Implemented Features:**
|
|
- `ResourcePanel` base class in `resource_panel.h`
|
|
- Dynamic room panels (`DungeonRoomPanel`) created on-demand when rooms selected
|
|
- Dynamic song panels (`MusicSongPanel`) and piano roll panels for music editor
|
|
- Session-aware ID generation via `MakePanelId()`
|
|
- LRU eviction in `EnforceResourceLimits()` respects pinned panels
|
|
- Resource panels appear in Activity Bar under their editor category
|
|
- Room selector properly limited to 296 rooms (kNumberOfRooms)
|
|
|
|
**Key Implementation Details:**
|
|
```cpp
|
|
// Resource panels use BASE IDs - PanelManager handles session prefixing
|
|
std::string card_id = absl::StrFormat("dungeon.room_%d", room_id);
|
|
panel_manager->RegisterPanel({.card_id = card_id, ...});
|
|
panel_manager->ShowPanel(card_id); // Sets visibility immediately
|
|
```
|
|
|
|
**Important:** Do NOT use `MakeCardId()` for resource panels - it causes double-prefixing since `RegisterPanel()` already calls `MakePanelId()` internally.
|
|
|
|
### Phase 7: Cross-Editor Features ✅ Complete
|
|
|
|
**Implemented Features:**
|
|
1. **Pin-to-Persist UI** - Pin button in Activity Bar sidebar (not title bar due to ImGui limitations)
|
|
2. **`OnEditorSwitch()`** - Hides non-pinned panels from previous category, shows defaults for new
|
|
3. **Dashboard Category** - `kDashboardCategory` suppresses all panels until editor selected
|
|
4. **Category Filtering** - Resource panels only visible when their category is active OR pinned
|
|
|
|
**Key Implementation Details:**
|
|
```cpp
|
|
// In PanelManager::DrawAllVisiblePanels()
|
|
if (active_category_.empty() || active_category_ == kDashboardCategory) {
|
|
return; // Suppress panels when no editor selected
|
|
}
|
|
|
|
// Resource panels check category + pin status
|
|
if (panel->GetEditorCategory() == active_category_ ||
|
|
IsPanelPinned(panel_id) ||
|
|
panel->GetPanelCategory() == PanelCategory::Persistent) {
|
|
// Draw panel
|
|
}
|
|
```
|
|
|
|
### Phase 8: Multi-Session Support ✅ Complete
|
|
|
|
**Verified Behaviors:**
|
|
- Session-aware panel IDs: `s0.dungeon.room_42` vs `s1.dungeon.room_42`
|
|
- Resource panels properly scoped to their session
|
|
- Fixed double-prefix bug (resource panels use base IDs, not `MakeCardId()`)
|
|
- Window titles include session suffix when multiple ROMs open
|
|
|
|
**Dungeon Editor Panels (11 total):**
|
|
| Panel | ID | Type |
|
|
|:------|:---|:-----|
|
|
| Control Panel | `dungeon.control_panel` | Static |
|
|
| Room Selector | `dungeon.room_selector` | Static |
|
|
| Entrance List | `dungeon.entrance_list` | Static |
|
|
| Room Matrix | `dungeon.room_matrix` | Static |
|
|
| Entrances Properties | `dungeon.entrances` | Static |
|
|
| Room Graphics | `dungeon.room_graphics` | Static |
|
|
| Object Editor | `dungeon.object_editor` | Static |
|
|
| Debug Controls | `dungeon.debug_controls` | Static |
|
|
| Room {id} | `dungeon.room_{id}` | Resource (dynamic) |
|
|
|
|
**Music Editor Resource Panels:**
|
|
| Panel | ID Pattern | Type |
|
|
|:------|:-----------|:-----|
|
|
| Song Tracker | `music.song_{id}` | Resource (dynamic) |
|
|
| Piano Roll | `music.piano_roll_{id}` | Resource (dynamic) |
|
|
|
|
---
|
|
|
|
### Remaining Work / Future Enhancements
|
|
|
|
The core panel system refactor is complete. The following are optional enhancements:
|
|
|
|
| Enhancement | Priority | Effort | Description |
|
|
|:------------|:---------|:-------|:------------|
|
|
| Overworld Resource Panels | Medium | 4-6h | Create `overworld.map_{id}` panels for per-map editing |
|
|
| Graphics Resource Panels | Medium | 4-6h | Create `graphics.sheet_{id}` panels for per-sheet editing |
|
|
| Sprite Resource Panels | Low | 2-4h | Create `sprite.vanilla_{id}` panels |
|
|
| Cascade Close | Low | 2-3h | Implement parent-child panel relationships |
|
|
| Panel State Persistence | Low | 2-3h | Save/restore pinned state and visibility to config |
|
|
| Keyboard Shortcuts | Low | 1-2h | Add configurable shortcuts for common panels |
|
|
|
|
**Known Limitations:**
|
|
- Pin button is in Activity Bar sidebar, not panel title bar (ImGui docking limitation)
|
|
- Resource panel limits are enforced but may need tuning based on user feedback
|
|
- Some editors (Assembly, Agent) have minimal panel integration
|
|
|
|
---
|
|
|
|
### Troubleshooting: Common Panel Visibility Issues
|
|
|
|
When panels don't respect visibility or appear duplicated, check for these common issues:
|
|
|
|
#### Issue 1: Duplicate Panel Drawing (DUPLICATE DETECTED warnings)
|
|
|
|
**Symptom:** Console shows `[PanelWindow] DUPLICATE DETECTED: 'Panel Name' Begin() called twice`
|
|
|
|
**Cause:** Panel is being drawn by BOTH:
|
|
- Central `PanelManager::DrawAllVisiblePanels()` (via EditorPanel)
|
|
- Local `gui::PanelWindow` code in the editor's `Update()` method
|
|
|
|
**Fix:** Remove the local drawing code. When using `RegisterEditorPanel()`, the central drawing handles everything:
|
|
|
|
```cpp
|
|
// WRONG - draws twice
|
|
void MyEditor::Initialize() {
|
|
panel_manager->RegisterEditorPanel(std::make_unique<MyPanel>(...));
|
|
}
|
|
void MyEditor::Update() {
|
|
gui::PanelWindow panel("My Panel", ICON);
|
|
if (panel.Begin(&show_panel_)) { DrawContent(); }
|
|
panel.End();
|
|
}
|
|
|
|
// CORRECT - draws once via central system
|
|
void MyEditor::Initialize() {
|
|
panel_manager->RegisterEditorPanel(std::make_unique<MyPanel>([this]() {
|
|
DrawContent();
|
|
}));
|
|
}
|
|
void MyEditor::Update() {
|
|
// No local drawing - handled by PanelManager::DrawAllVisiblePanels()
|
|
return absl::OkStatus();
|
|
}
|
|
```
|
|
|
|
#### Issue 2: Duplicate Registration (RegisterPanel + RegisterEditorPanel)
|
|
|
|
**Symptom:** Panel appears twice in Activity Bar, or metadata conflicts
|
|
|
|
**Cause:** Both `RegisterPanel()` AND `RegisterEditorPanel()` called for same panel
|
|
|
|
**Fix:** Use only `RegisterEditorPanel()` - the EditorPanel class provides all metadata:
|
|
|
|
```cpp
|
|
// WRONG - duplicate registration
|
|
panel_manager->RegisterPanel({.card_id = "editor.my_panel", ...});
|
|
panel_manager->RegisterEditorPanel(std::make_unique<MyPanel>(...));
|
|
|
|
// CORRECT - EditorPanel provides metadata via GetId(), GetDisplayName(), etc.
|
|
panel_manager->RegisterEditorPanel(std::make_unique<MyPanel>(...));
|
|
```
|
|
|
|
#### Issue 3: Resource Panel Double-Prefixing
|
|
|
|
**Symptom:** Resource panels (rooms, songs) don't appear or have wrong IDs like `s0.s0.dungeon.room_42`
|
|
|
|
**Cause:** Using `MakeCardId()` for resource panels when `RegisterPanel()` already adds session prefix
|
|
|
|
**Fix:** Use base IDs for resource panels - `RegisterPanel()` handles prefixing:
|
|
|
|
```cpp
|
|
// WRONG - double prefix
|
|
std::string card_id = MakeCardId(absl::StrFormat("dungeon.room_%d", room_id));
|
|
panel_manager->RegisterPanel({.card_id = card_id, ...});
|
|
|
|
// CORRECT - let RegisterPanel handle prefixing
|
|
std::string card_id = absl::StrFormat("dungeon.room_%d", room_id);
|
|
panel_manager->RegisterPanel({.card_id = card_id, ...});
|
|
panel_manager->ShowPanel(card_id); // Uses same base ID
|
|
```
|
|
|
|
#### Issue 4: Context Menu Popups Don't Open
|
|
|
|
**Symptom:** Clicking context menu items doesn't open the expected popup (e.g., entity editor)
|
|
|
|
**Cause:** `ImGui::OpenPopup()` called from within another popup's callback doesn't work
|
|
|
|
**Fix:** Use deferred popup pattern - store request and process outside popup context:
|
|
|
|
```cpp
|
|
// WRONG - OpenPopup inside context menu callback fails
|
|
entity_menu.callback = [this]() {
|
|
InsertEntity();
|
|
ImGui::OpenPopup("Entity Editor"); // Won't work!
|
|
};
|
|
|
|
// CORRECT - defer popup opening
|
|
void MyEditor::HandleEntityInsert(const std::string& type) {
|
|
pending_insert_type_ = type; // Store for later
|
|
}
|
|
|
|
void MyEditor::Update() {
|
|
ProcessPendingInsert(); // Called outside popup context
|
|
// ... draw popups here, OpenPopup() works now
|
|
}
|
|
```
|
|
|
|
#### Issue 5: Panels Visible Before Editor Selected
|
|
|
|
**Symptom:** Panels from other editors appear when on Dashboard
|
|
|
|
**Cause:** `active_category_` not set to Dashboard, or missing category check
|
|
|
|
**Fix:** Ensure `SetActiveCategory(kDashboardCategory)` when ROM loaded but no editor selected:
|
|
|
|
```cpp
|
|
void EditorManager::LoadRom() {
|
|
// After loading...
|
|
panel_manager_.SetActiveCategory(PanelManager::kDashboardCategory);
|
|
}
|
|
|
|
void EditorManager::SwitchToEditor(EditorType type) {
|
|
panel_manager_.SetActiveCategory(EditorRegistry::GetEditorCategory(type));
|
|
}
|
|
```
|
|
|
|
#### Issue 6: Resource Panels Always Visible (Act Like Pinned)
|
|
|
|
**Symptom:** Resource panels (rooms, songs) don't hide when switching editors
|
|
|
|
**Cause:** Missing category filtering in the editor's drawing loop
|
|
|
|
**Fix:** Add category + pin check before drawing resource panels:
|
|
|
|
```cpp
|
|
// In editor's DrawLayout() for dynamic resource panels:
|
|
for (auto& resource : active_resources_) {
|
|
std::string card_id = absl::StrFormat("editor.resource_%d", resource.id);
|
|
|
|
// Skip if not in category AND not pinned
|
|
if (panel_manager->GetActiveCategory() != "MyEditor" &&
|
|
!panel_manager->IsPanelPinned(card_id)) {
|
|
continue;
|
|
}
|
|
|
|
// Draw the resource panel...
|
|
}
|
|
```
|
|
|
|
### Checklist for Migrating an Editor to EditorPanel System
|
|
|
|
1. **Create EditorPanel classes** in `panels/` subdirectory
|
|
- Implement `GetId()`, `GetDisplayName()`, `GetIcon()`, `GetEditorCategory()`, `GetPriority()`
|
|
- Implement `Draw(bool* p_open)` with content drawing
|
|
|
|
2. **Update Initialize()**
|
|
- Call `RegisterEditorPanel()` for each panel
|
|
- Remove any `RegisterPanel()` calls (EditorPanel provides metadata)
|
|
- Call `ShowPanel()` for default-visible panels
|
|
|
|
3. **Update Update()**
|
|
- Remove ALL local `gui::PanelWindow` drawing code
|
|
- Central `PanelManager::DrawAllVisiblePanels()` handles drawing
|
|
- Keep only non-panel logic (popup modals, shortcuts, etc.)
|
|
|
|
4. **For Resource Panels** (dynamic, per-resource)
|
|
- Use base IDs (no `MakeCardId()`)
|
|
- Add category filtering before drawing
|
|
- Register on-demand when resource opened
|
|
- Unregister when resource closed
|
|
|
|
5. **Test**
|
|
- Verify no DUPLICATE DETECTED warnings
|
|
- Verify panels appear/hide on editor switch
|
|
- Verify Activity Bar shows correct panels
|
|
- Verify pinning works across editor switches
|
|
|
|
---
|
|
|
|
### Original Migration Plan (For Reference)
|
|
|
|
### Phase 1: Rename UI Widget (2-3 hours) ✅ COMPLETE
|
|
|
|
1. Rename `gui::EditorCard` → `gui::PanelWindow`
|
|
2. Update all 24 files that use `gui::EditorCard`
|
|
3. Update documentation
|
|
|
|
### Phase 2: Rename Registry (3-4 hours) ✅ COMPLETE
|
|
|
|
1. Rename `CardInfo` → `PanelDescriptor`
|
|
2. Rename `EditorCardRegistry` → `PanelManager`
|
|
3. Update all registration calls
|
|
4. Update `LayoutPresets` constants
|
|
|
|
### Phase 3: Create EditorPanel Interface (4-6 hours) ✅ COMPLETE
|
|
|
|
1. Define `EditorPanel` base class with `PanelCategory`
|
|
2. Add pin support to `PanelWindow`
|
|
3. Implement central drawing in `PanelManager`
|
|
|
|
### Phase 4: Migrate Dungeon Editor (Proof of Concept, 8-12 hours) ✅ COMPLETE
|
|
|
|
1. Convert all 9 Dungeon panels to `EditorPanel` implementations
|
|
2. Register with `PanelManager::EmplacePanel<>`
|
|
3. Remove manual drawing from `DungeonEditorV2::Update()`
|
|
4. Verify LayoutManager docking works
|
|
|
|
### Phase 5: Migrate Remaining Editors (2-4 hours each) ✅ COMPLETE
|
|
|
|
Priority order based on panel count:
|
|
1. **Palette Editor** (11 panels) ✅ - Has `PaletteGroupPanel` hierarchy
|
|
2. **Overworld Editor** (9 panels) ✅ - Most complex, has main canvas
|
|
3. **Graphics Editor** (5 panels) ✅ - Mix of legacy and new
|
|
4. **Agent Editor** (8 panels) ✅ - Pure panel-based
|
|
5. **Music Editor** (7 panels + dynamic) ✅ - Has dynamic song panels
|
|
6. **Screen Editor** (5 panels) ✅ - Straightforward
|
|
7. **Message Editor** (4 panels) ✅ - Straightforward
|
|
8. **Sprite Editor** (2 panels) ✅ - Simple
|
|
9. **Assembly Editor** (2 panels) - Deferred (project-based workflow)
|
|
|
|
---
|
|
|
|
## Part 8: Summary Statistics & Default Visibility
|
|
|
|
### 8.1 Panel Counts by Category
|
|
|
|
| Category | Static Panels | Resource Panels | Default Visible | Notes |
|
|
|:---------|:--------------|:----------------|:----------------|:------|
|
|
| Overworld | 10 | per-map | 5 | Canvas, Tile16, Area GFX, Usage, Properties |
|
|
| Dungeon | 9 | per-room | 5 | Controls, Selector, Matrix, GFX, Object Editor |
|
|
| Graphics | 9 | per-sheet | 4 | Browser, Pixel, Palette, 3D Objects |
|
|
| Palette | 11 | — | 5 | Controls, OW Main, Dungeon, Sprites, Quick |
|
|
| Music | 7 | per-song | 3 | Browser, Tracker, Instruments |
|
|
| Screen | 5 | — | 3 | Dungeon Maps, Inventory, Title |
|
|
| Sprite | 2 | per-sprite | 1 | Vanilla Editor |
|
|
| Message | 4 | — | 3 | List, Editor, Font Atlas |
|
|
| Assembly | 2 | per-file | 1 | User-initiated only |
|
|
| Agent | 8 | — | 1 | User-initiated only |
|
|
| Emulator | 11 | — | 6 | CPU, PPU, Perf, States, Keys, Controller |
|
|
| Memory | 1 | — | 1 | User-initiated only |
|
|
| **TOTAL** | **~80** | **dynamic** | **35** | ~44% visible by default |
|
|
|
|
### 8.2 Default Visibility Rationale
|
|
|
|
**Philosophy**: Show panels that provide immediate value without overwhelming the user.
|
|
|
|
| Default ON | Reason |
|
|
|:-----------|:-------|
|
|
| Main canvas/selector | Core editing workflow |
|
|
| Graphics preview | Visual feedback while editing |
|
|
| Object/entity editors | Primary editing task |
|
|
| Usage statistics | Optimization guidance |
|
|
| Playback controls | Audio editors need transport |
|
|
|
|
| Default OFF | Reason |
|
|
|:------------|:-------|
|
|
| Debug panels | Developer-focused |
|
|
| Legacy panels | Replaced by newer versions |
|
|
| Advanced tools | Power users enable as needed |
|
|
| Agent/AI panels | Requires configuration first |
|
|
| Assembly panels | Project-based workflow |
|
|
|
|
### 8.3 First-Time User Experience
|
|
|
|
When a ROM is first loaded, show a balanced workspace:
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ RECOMMENDED DEFAULT LAYOUT │
|
|
├─────────────────────────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ ┌──────────┐ ┌────────────────────────────────────┐ ┌──────────────────┐ │
|
|
│ │ Activity │ │ │ │ Properties │ │
|
|
│ │ Bar │ │ Main Canvas │ │ Panel │ │
|
|
│ │ │ │ (Overworld/Room) │ │ │ │
|
|
│ │ [OW] │ │ │ │ - Map Info │ │
|
|
│ │ [Dun] │ │ │ │ - Palette │ │
|
|
│ │ [Gfx] │ │ │ │ - GFX Group │ │
|
|
│ │ [Pal] │ │ │ │ │ │
|
|
│ │ [Mus] │ ├────────────────────────────────────┤ ├──────────────────┤ │
|
|
│ │ [Scr] │ │ Tile Selector │ Usage Stats │ │ Graphics │ │
|
|
│ │ [Spr] │ │ │ │ │ Preview │ │
|
|
│ │ [Msg] │ │ [Tile16 Grid] │ [Heat Map] │ │ │ │
|
|
│ │ [Asm] │ │ │ │ │ [Current Sheet] │ │
|
|
│ │ │ └──────────────────┴────────────────┘ └──────────────────┘ │
|
|
│ │ ──────── │ │
|
|
│ │ [Emu] │ Status Bar: ROM Name | Session | Unsaved Changes │
|
|
│ │ [Set] │ │
|
|
│ └──────────┘ │
|
|
│ │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## Appendix A: File Changes Summary
|
|
|
|
| File | Changes Required |
|
|
|:-----|:-----------------|
|
|
| `src/app/gui/app/editor_layout.h` | Rename class, add pin support |
|
|
| `src/app/gui/app/editor_layout.cc` | Update implementation |
|
|
| `src/app/editor/system/editor_card_registry.h` | Rename to panel_manager.h |
|
|
| `src/app/editor/system/editor_card_registry.cc` | Rename, add central drawing |
|
|
| `src/app/editor/ui/layout_presets.h` | Rename Card → Panel constants |
|
|
| `src/app/editor/menu/activity_bar.cc` | Update to use PanelManager |
|
|
| `src/app/editor/*/[editor].cc` (24 files) | Update registrations |
|
|
|
|
---
|
|
|
|
## Appendix B: Naming Convention
|
|
|
|
### Static Panel IDs
|
|
- Format: `{category}.{name}` (lowercase, underscores)
|
|
- Examples: `dungeon.room_selector`, `palette.ow_main`
|
|
|
|
### Resource Panel IDs
|
|
- Format: `[s{session}.]{category}.{resource}_{id}[.{subpanel}]`
|
|
- Session prefix only when `session_count_ > 1`
|
|
|
|
| Pattern | Example | Description |
|
|
|:--------|:--------|:------------|
|
|
| `{cat}.{res}_{id}` | `dungeon.room_42` | Single session |
|
|
| `s{n}.{cat}.{res}_{id}` | `s0.dungeon.room_42` | Multi-session |
|
|
| `s{n}.{cat}.{res}_{id}.{sub}` | `s1.music.song_5.piano_roll` | Subpanel |
|
|
|
|
### Window Titles
|
|
- **Static panels**: `{icon} {Display Name}`
|
|
- **Resource panels**: `{icon} {Resource Type} {ID}`
|
|
- **Session suffix**: ` [S{n}]` when multi-session
|
|
|
|
| Panel Type | Single Session | Multi-Session |
|
|
|:-----------|:---------------|:--------------|
|
|
| Static | `🏰 Room Selector` | `🏰 Room Selector [S0]` |
|
|
| Resource | `🚪 Room 42` | `🚪 Room 42 [S1]` |
|
|
| Subpanel | `🎹 Song 5 Piano Roll` | `🎹 Song 5 Piano Roll [S0]` |
|
|
|
|
### Categories (for ActivityBar grouping)
|
|
- Match `EditorType`: Overworld, Dungeon, Graphics, Palette, Music, Screen, Sprite, Message, Assembly, Agent, Emulator, Memory
|
|
|
|
### Resource Types (for resource panels)
|
|
| Category | Resource Types |
|
|
|:---------|:---------------|
|
|
| Dungeon | `room` |
|
|
| Music | `song`, `instrument`, `sample` |
|
|
| Overworld | `map` |
|
|
| Graphics | `sheet` |
|
|
| Sprite | `vanilla`, `custom` |
|
|
| Message | `text` |
|
|
| Assembly | `file` |
|
|
|
|
---
|
|
|
|
## Appendix C: ResourcePanel Interface
|
|
|
|
```cpp
|
|
namespace yaze::editor {
|
|
|
|
/**
|
|
* @class ResourcePanel
|
|
* @brief Base class for panels that edit specific ROM resources
|
|
*
|
|
* A ResourcePanel represents a window for editing a specific piece of
|
|
* data within a ROM, such as a dungeon room, a song, or a graphics sheet.
|
|
*
|
|
* Key Features:
|
|
* - Session-aware: Can distinguish between same resource in different ROMs
|
|
* - Multi-instance: Multiple resources can be open simultaneously
|
|
* - LRU managed: Oldest panels auto-close when limit reached
|
|
*
|
|
* Subclasses:
|
|
* - DungeonRoomPanel: Edits a specific room (0-295)
|
|
* - MusicSongPanel: Edits a specific song with tracker/piano roll
|
|
* - GraphicsSheetPanel: Edits a specific GFX sheet
|
|
* - OverworldMapPanel: Edits a specific overworld map
|
|
*/
|
|
class ResourcePanel : public EditorPanel {
|
|
public:
|
|
// ========== Resource Identity ==========
|
|
|
|
/// The numeric ID of the resource (room_id, song_index, sheet_id, etc.)
|
|
virtual int GetResourceId() const = 0;
|
|
|
|
/// The resource type name (e.g., "room", "song", "sheet")
|
|
virtual std::string GetResourceType() const = 0;
|
|
|
|
/// Human-readable resource name (e.g., "Hyrule Castle Entrance")
|
|
virtual std::string GetResourceName() const {
|
|
return absl::StrFormat("%s %d", GetResourceType(), GetResourceId());
|
|
}
|
|
|
|
// ========== Panel Identity (from EditorPanel) ==========
|
|
|
|
std::string GetId() const override {
|
|
// Generated from resource type and ID
|
|
return absl::StrFormat("%s.%s_%d",
|
|
GetEditorCategory(), GetResourceType(), GetResourceId());
|
|
}
|
|
|
|
std::string GetDisplayName() const override {
|
|
return GetResourceName();
|
|
}
|
|
|
|
// ========== Behavior ==========
|
|
|
|
/// Resource panels are always editor-bound
|
|
PanelCategory GetPanelCategory() const override {
|
|
return PanelCategory::EditorBound;
|
|
}
|
|
|
|
/// Allow multiple resource panels of same type
|
|
virtual bool AllowMultipleInstances() const { return true; }
|
|
|
|
/// Get the session ID this resource belongs to
|
|
virtual size_t GetSessionId() const { return session_id_; }
|
|
|
|
// ========== Lifecycle ==========
|
|
|
|
/// Called when resource data changes externally
|
|
virtual void OnResourceModified() {}
|
|
|
|
/// Called when resource is deleted from ROM
|
|
virtual void OnResourceDeleted() {
|
|
// Default: close the panel
|
|
}
|
|
|
|
protected:
|
|
size_t session_id_ = 0;
|
|
};
|
|
|
|
/**
|
|
* @brief Example: Dungeon Room Panel
|
|
*/
|
|
class DungeonRoomPanel : public ResourcePanel {
|
|
public:
|
|
DungeonRoomPanel(size_t session_id, int room_id,
|
|
zelda3::Room* room, DungeonCanvasViewer* viewer)
|
|
: room_id_(room_id), room_(room), viewer_(viewer) {
|
|
session_id_ = session_id;
|
|
}
|
|
|
|
int GetResourceId() const override { return room_id_; }
|
|
std::string GetResourceType() const override { return "room"; }
|
|
std::string GetIcon() const override { return ICON_MD_DOOR_FRONT; }
|
|
std::string GetEditorCategory() const override { return "Dungeon"; }
|
|
|
|
void Draw(bool* p_open) override {
|
|
// Draw room canvas with objects, sprites, etc.
|
|
viewer_->DrawRoom(room_id_, p_open);
|
|
}
|
|
|
|
private:
|
|
int room_id_;
|
|
zelda3::Room* room_;
|
|
DungeonCanvasViewer* viewer_;
|
|
};
|
|
|
|
} // namespace yaze::editor
|
|
```
|
|
|
|
---
|
|
|
|
## Appendix D: Recommended Default Panels by Editor
|
|
|
|
| Editor | Panels Visible by Default | Rationale |
|
|
|:-------|:--------------------------|:----------|
|
|
| **Overworld** | Canvas, Tile16, Area GFX, Usage Stats, Properties | Full tile editing workflow |
|
|
| **Dungeon** | Controls, Selector, Matrix, Room GFX, Object Editor | Room editing + visual navigation |
|
|
| **Graphics** | Sheet Browser, Pixel Editor, Palette Controls, 3D Objects | Complete pixel art workflow |
|
|
| **Palette** | Controls, OW Main, Dungeon Main, Sprites, Quick Access | Color editing across contexts |
|
|
| **Music** | Song Browser, Tracker, Instrument Editor | Music composition workflow |
|
|
| **Screen** | Dungeon Maps, Inventory, Title Screen | Most commonly edited screens |
|
|
| **Sprite** | Vanilla Editor | View-only by default, editing opt-in |
|
|
| **Message** | Message List, Message Editor, Font Atlas | Text editing workflow |
|
|
| **Assembly** | *None* | Project-based, user-initiated |
|
|
| **Agent** | *None* | Requires API config first |
|
|
| **Emulator** | CPU Debugger, PPU, Performance, Save States, Keys, Controller | Debugging + playback |
|
|
| **Memory** | *None* | Power user feature |
|